From a0734a15ce663d0373ddb886b5776aee9ff8b555 Mon Sep 17 00:00:00 2001 From: Dominic Sidiropoulos Date: Mon, 7 Feb 2022 00:50:29 +0100 Subject: [PATCH 01/33] chore (.gitignore): ignore .vscode folder and any *.code-workspace files --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index d1ee4e5..891b313 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,8 @@ _testmain.go /dist /nats-top + +# VSCode + +.vscode/** +*.code-workspace From 979f84663a3e385a4102874cba801061e6b216f7 Mon Sep 17 00:00:00 2001 From: Dominic Sidiropoulos Date: Mon, 7 Feb 2022 00:55:56 +0100 Subject: [PATCH 02/33] clean (toputils.go): consolidate 1024-related constants used in Psize() into kibibytes, mebibytes, kibibytes which are well-known, human-readable units of measurement --- util/toputils.go | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/util/toputils.go b/util/toputils.go index baf3cd0..784ee81 100644 --- a/util/toputils.go +++ b/util/toputils.go @@ -249,18 +249,22 @@ type Rates struct { OutBytesRate float64 } +const kibibyte = 1024 +const mebibyte = 1024 * 1024 +const gibibyte = 1024 * 1024 * 1024 + // Psize takes a float and returns a human readable string. func Psize(s int64) string { size := float64(s) - if size < 1024 { + if size < kibibyte { return fmt.Sprintf("%.0f", size) - } else if size < (1024 * 1024) { - return fmt.Sprintf("%.1fK", size/1024) - } else if size < (1024 * 1024 * 1024) { - return fmt.Sprintf("%.1fM", size/1024/1024) - } else if size >= (1024 * 1024 * 1024) { - return fmt.Sprintf("%.1fG", size/1024/1024/1024) + } else if size < mebibyte { + return fmt.Sprintf("%.1fK", size/kibibyte) + } else if size < gibibyte { + return fmt.Sprintf("%.1fM", size/mebibyte) + } else if size >= gibibyte { + return fmt.Sprintf("%.1fG", size/gibibyte) } else { return "NA" } From c235b19eaa0de30b351a0193065849a019dc95f9 Mon Sep 17 00:00:00 2001 From: Dominic Sidiropoulos Date: Mon, 7 Feb 2022 01:00:01 +0100 Subject: [PATCH 03/33] clean (toputils.go): neutral cleanups in Psize() to make it more readable - the if-else structure was not really needed and it has been simplified to use plain 'ifs' - the "NA" case at the bottom would never be reached because all possible cases are already covered from a numerical perspective --- util/toputils.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/util/toputils.go b/util/toputils.go index 784ee81..9808e24 100644 --- a/util/toputils.go +++ b/util/toputils.go @@ -259,13 +259,15 @@ func Psize(s int64) string { if size < kibibyte { return fmt.Sprintf("%.0f", size) - } else if size < mebibyte { + } + + if size < mebibyte { return fmt.Sprintf("%.1fK", size/kibibyte) - } else if size < gibibyte { + } + + if size < gibibyte { return fmt.Sprintf("%.1fM", size/mebibyte) - } else if size >= gibibyte { - return fmt.Sprintf("%.1fG", size/gibibyte) - } else { - return "NA" } + + return fmt.Sprintf("%.1fG", size/gibibyte) } From 4de3db541cc4cd20e14650b905e9af57fe7e4781 Mon Sep 17 00:00:00 2001 From: Dominic Sidiropoulos Date: Mon, 7 Feb 2022 02:09:38 +0100 Subject: [PATCH 04/33] feat (cli flags): add new flag '-b' which causes all traffic to be printed in raw bytes --- nats-top.go | 35 ++++++++++++++++++----------------- readme.md | 6 +++++- util/toputils.go | 4 ++-- util/toputils_test.go | 32 ++++++++++++++++++++++++++++---- 4 files changed, 53 insertions(+), 24 deletions(-) diff --git a/nats-top.go b/nats-top.go index a1ce1ca..40fd2c6 100644 --- a/nats-top.go +++ b/nats-top.go @@ -18,13 +18,14 @@ import ( const version = "0.4.0" var ( - host = flag.String("s", "127.0.0.1", "The nats server host.") - port = flag.Int("m", 8222, "The NATS server monitoring port.") - conns = flag.Int("n", 1024, "Maximum number of connections to poll.") - delay = flag.Int("d", 1, "Refresh interval in seconds.") - sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") - showVersion = flag.Bool("v", false, "Show nats-top version.") - lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") + host = flag.String("s", "127.0.0.1", "The nats server host.") + port = flag.Int("m", 8222, "The NATS server monitoring port.") + conns = flag.Int("n", 1024, "Maximum number of connections to poll.") + delay = flag.Int("d", 1, "Refresh interval in seconds.") + sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") + lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") + showVersion = flag.Bool("v", false, "Show nats-top version.") + displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") // Secure options httpsPort = flag.Int("ms", 0, "The NATS server secure monitoring port.") @@ -50,7 +51,7 @@ var ( usageHelp = ` usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-sort by] - [-cert FILE] [-key FILE ][-cacert FILE] [-k] + [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] ` // cache for reducing DNS lookups in case enabled @@ -169,15 +170,15 @@ func generateParagraph( serverVersion = stats.Varz.Version } - mem := top.Psize(memVal) - inMsgs := top.Psize(inMsgsVal) - outMsgs := top.Psize(outMsgsVal) - inBytes := top.Psize(inBytesVal) - outBytes := top.Psize(outBytesVal) + mem := top.Psize(false, memVal) //memory is exempt from the rawbytes flag + inMsgs := top.Psize(*displayRawBytes, inMsgsVal) + outMsgs := top.Psize(*displayRawBytes, outMsgsVal) + inBytes := top.Psize(*displayRawBytes, inBytesVal) + outBytes := top.Psize(*displayRawBytes, outBytesVal) inMsgsRate := stats.Rates.InMsgsRate outMsgsRate := stats.Rates.OutMsgsRate - inBytesRate := top.Psize(int64(stats.Rates.InBytesRate)) - outBytesRate := top.Psize(int64(stats.Rates.OutBytesRate)) + inBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.InBytesRate)) + outBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.OutBytesRate)) info := "NATS server version %s (uptime: %s) %s" info += "\nServer:\n Load: CPU: %.1f%% Memory: %s Slow Consumers: %d\n" @@ -318,8 +319,8 @@ func generateParagraph( } connLineInfo = append(connLineInfo, conn.NumSubs) - connLineInfo = append(connLineInfo, top.Psize(int64(conn.Pending)), top.Psize(conn.OutMsgs), top.Psize(conn.InMsgs)) - connLineInfo = append(connLineInfo, top.Psize(conn.OutBytes), top.Psize(conn.InBytes)) + connLineInfo = append(connLineInfo, top.Psize(*displayRawBytes, int64(conn.Pending)), top.Psize(*displayRawBytes, conn.OutMsgs), top.Psize(*displayRawBytes, conn.InMsgs)) + connLineInfo = append(connLineInfo, top.Psize(*displayRawBytes, conn.OutBytes), top.Psize(*displayRawBytes, conn.InBytes)) connLineInfo = append(connLineInfo, conn.Lang, conn.Version) connLineInfo = append(connLineInfo, conn.Uptime, conn.LastActivity) diff --git a/readme.md b/readme.md index 786b296..024767f 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ and releases of the binary are also [available](https://github.com/nats-io/nats- ``` usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-sort by] - [-cert FILE] [-key FILE ][-cacert FILE] [-k] + [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] ``` - `-m http_port`, `-ms https_port` @@ -68,6 +68,10 @@ usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] Configure to skip verification of certificate. +- `-b` + + Displays traffic in raw bytes. + ## Commands While in top view, it is possible to use the following commands: diff --git a/util/toputils.go b/util/toputils.go index 9808e24..7683e9e 100644 --- a/util/toputils.go +++ b/util/toputils.go @@ -254,10 +254,10 @@ const mebibyte = 1024 * 1024 const gibibyte = 1024 * 1024 * 1024 // Psize takes a float and returns a human readable string. -func Psize(s int64) string { +func Psize(displayRawValue bool, s int64) string { size := float64(s) - if size < kibibyte { + if displayRawValue || size < kibibyte { return fmt.Sprintf("%.0f", size) } diff --git a/util/toputils_test.go b/util/toputils_test.go index d318eda..0f93681 100644 --- a/util/toputils_test.go +++ b/util/toputils_test.go @@ -102,25 +102,49 @@ func TestFetchingStatz(t *testing.T) { func TestPsize(t *testing.T) { expected := "1023" - got := Psize(1023) + got := Psize(false, 1023) if got != expected { t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) } expected = "1.0K" - got = Psize(1024) + got = Psize(false, kibibyte) if got != expected { t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) } expected = "1.0M" - got = Psize(1024 * 1024) + got = Psize(false, mebibyte) if got != expected { t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) } expected = "1.0G" - got = Psize(1024 * 1024 * 1024) + got = Psize(false, gibibyte) + if got != expected { + t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + } + + expected = "1023" + got = Psize(true, 1023) + if got != expected { + t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + } + + expected = fmt.Sprintf("%d", kibibyte) + got = Psize(true, kibibyte) + if got != expected { + t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + } + + expected = fmt.Sprintf("%d", mebibyte) + got = Psize(true, mebibyte) + if got != expected { + t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + } + + expected = fmt.Sprintf("%d", gibibyte) + got = Psize(true, gibibyte) if got != expected { t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) } From 1e0be58e008401b3de68a6ae943c3c40f22ca420 Mon Sep 17 00:00:00 2001 From: Dominic Sidiropoulos Date: Mon, 7 Feb 2022 02:32:38 +0100 Subject: [PATCH 05/33] clean (Psize unit tests): refactor the test harness to have it employ t.Run() instead of raw tests --- util/toputils_test.go | 121 ++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 41 deletions(-) diff --git a/util/toputils_test.go b/util/toputils_test.go index 0f93681..54e5b1f 100644 --- a/util/toputils_test.go +++ b/util/toputils_test.go @@ -101,52 +101,91 @@ func TestFetchingStatz(t *testing.T) { func TestPsize(t *testing.T) { - expected := "1023" - got := Psize(false, 1023) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + type Args struct { + displayRawBytes bool + input int64 } - expected = "1.0K" - got = Psize(false, kibibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + testcases := []struct { + description string + args Args + want string + }{ + { + description: "given input=1023 and display_raw_bytes=false expect return value string to be '1023'", + args: Args{ + input: int64(1023), + displayRawBytes: false, + }, + want: "1023", + }, + { + description: "given input=kibibyte and display_raw_bytes=false expect return value string to be '1.0K'", + args: Args{ + input: int64(kibibyte), + displayRawBytes: false, + }, + want: "1.0K", + }, + { + description: "given input=mebibyte and display_raw_bytes=false expect return value string to be '1.0M'", + args: Args{ + input: int64(mebibyte), + displayRawBytes: false, + }, + want: "1.0M", + }, + { + description: "given input=gibibyte and display_raw_bytes=false expect return value string to be '1.0G'", + args: Args{ + input: int64(gibibyte), + displayRawBytes: false, + }, + want: "1.0G", + }, + + { + description: "given input=1023 and display_raw_bytes=true expect return value string to be '1023'", + args: Args{ + input: int64(1023), + displayRawBytes: true, + }, + want: "1023", + }, + { + description: "given input=kibibyte and display_raw_bytes=true expect return value string to be '1048576'", + args: Args{ + input: int64(kibibyte), + displayRawBytes: true, + }, + want: fmt.Sprintf("%d", kibibyte), + }, + { + description: "given input=mebibyte and display_raw_bytes=true expect return value string to be '1048576'", + args: Args{ + input: int64(mebibyte), + displayRawBytes: true, + }, + want: fmt.Sprintf("%d", mebibyte), + }, + { + description: "given input=gibibyte and display_raw_bytes=true expect return value string to be '1073741824'", + args: Args{ + input: int64(gibibyte), + displayRawBytes: true, + }, + want: fmt.Sprintf("%d", gibibyte), + }, } - expected = "1.0M" - got = Psize(false, mebibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } - - expected = "1.0G" - got = Psize(false, gibibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } - - expected = "1023" - got = Psize(true, 1023) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } - - expected = fmt.Sprintf("%d", kibibyte) - got = Psize(true, kibibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } - - expected = fmt.Sprintf("%d", mebibyte) - got = Psize(true, mebibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } + for _, testcase := range testcases { + t.Run(testcase.description, func(t *testing.T) { + got := Psize(testcase.args.displayRawBytes, testcase.args.input) - expected = fmt.Sprintf("%d", gibibyte) - got = Psize(true, gibibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + if got != testcase.want { + t.Errorf("%s wanted \"%s\", got \"%s\"", testcase.description, testcase.want, got) + } + }) } } From 2f2292228e3774f9ac7fd8c41a7f959a6190f8e6 Mon Sep 17 00:00:00 2001 From: Dominic Sidiropoulos Date: Mon, 7 Feb 2022 02:32:38 +0100 Subject: [PATCH 06/33] clean (Psize unit tests): refactor the test harness to have it employ t.Run() instead of raw tests --- util/toputils_test.go | 121 ++++++++++++++++++++++++++++-------------- 1 file changed, 80 insertions(+), 41 deletions(-) diff --git a/util/toputils_test.go b/util/toputils_test.go index 0f93681..9e76697 100644 --- a/util/toputils_test.go +++ b/util/toputils_test.go @@ -101,52 +101,91 @@ func TestFetchingStatz(t *testing.T) { func TestPsize(t *testing.T) { - expected := "1023" - got := Psize(false, 1023) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + type Args struct { + displayRawBytes bool + input int64 } - expected = "1.0K" - got = Psize(false, kibibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + testcases := []struct { + description string + args Args + want string + }{ + { + description: "given input 1023 and display_raw_bytes false expect return value string to be '1023'", + args: Args{ + input: int64(1023), + displayRawBytes: false, + }, + want: "1023", + }, + { + description: "given input kibibyte and display_raw_bytes false expect return value string to be '1.0K'", + args: Args{ + input: int64(kibibyte), + displayRawBytes: false, + }, + want: "1.0K", + }, + { + description: "given input mebibyte and display_raw_bytes false expect return value string to be '1.0M'", + args: Args{ + input: int64(mebibyte), + displayRawBytes: false, + }, + want: "1.0M", + }, + { + description: "given input gibibyte and display_raw_bytes false expect return value string to be '1.0G'", + args: Args{ + input: int64(gibibyte), + displayRawBytes: false, + }, + want: "1.0G", + }, + + { + description: "given input 1023 and display_raw_bytes true expect return value string to be '1023'", + args: Args{ + input: int64(1023), + displayRawBytes: true, + }, + want: "1023", + }, + { + description: "given input kibibyte and display_raw_bytes true expect return value string to be '1048576'", + args: Args{ + input: int64(kibibyte), + displayRawBytes: true, + }, + want: fmt.Sprintf("%d", kibibyte), + }, + { + description: "given input mebibyte and display_raw_bytes true expect return value string to be '1048576'", + args: Args{ + input: int64(mebibyte), + displayRawBytes: true, + }, + want: fmt.Sprintf("%d", mebibyte), + }, + { + description: "given input gibibyte and display_raw_bytes true expect return value string to be '1073741824'", + args: Args{ + input: int64(gibibyte), + displayRawBytes: true, + }, + want: fmt.Sprintf("%d", gibibyte), + }, } - expected = "1.0M" - got = Psize(false, mebibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } - - expected = "1.0G" - got = Psize(false, gibibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } - - expected = "1023" - got = Psize(true, 1023) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } - - expected = fmt.Sprintf("%d", kibibyte) - got = Psize(true, kibibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } - - expected = fmt.Sprintf("%d", mebibyte) - got = Psize(true, mebibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } + for _, testcase := range testcases { + t.Run(testcase.description, func(t *testing.T) { + got := Psize(testcase.args.displayRawBytes, testcase.args.input) - expected = fmt.Sprintf("%d", gibibyte) - got = Psize(true, gibibyte) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + if got != testcase.want { + t.Errorf("%s wanted \"%s\", got \"%s\"", testcase.description, testcase.want, got) + } + }) } } From d7ef625625e91c514c4fe8f92db33a42f5df3fcb Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 18:27:22 +0100 Subject: [PATCH 07/33] chore (.gitignore): ignore .vscode folder and any *.code-workspace files --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index d1ee4e5..891b313 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,8 @@ _testmain.go /dist /nats-top + +# VSCode + +.vscode/** +*.code-workspace From 64edada5d2b9605f25a20194f705899325bb1c43 Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 18:31:13 +0100 Subject: [PATCH 08/33] feat (new cli flag): add new flag '-b' to print traffic in raw bytes - consolidate 1024-related constants used in Psize() into kibibytes, mebibytes, kibibytes which are well-known, human-readable units of measurement - the if-else structure was not really needed and it has been simplified to use plain 'ifs' - the "NA" case at the bottom would never be reached because all possible cases are already covered from a numerical perspective - neutral cleanups in Psize() to make it more readable - refactor the test harness to have it employ t.Run() instead of raw tests --- nats-top.go | 35 ++++++++-------- readme.md | 6 ++- util/toputils.go | 26 +++++++----- util/toputils_test.go | 97 +++++++++++++++++++++++++++++++++++-------- 4 files changed, 119 insertions(+), 45 deletions(-) diff --git a/nats-top.go b/nats-top.go index a1ce1ca..40fd2c6 100644 --- a/nats-top.go +++ b/nats-top.go @@ -18,13 +18,14 @@ import ( const version = "0.4.0" var ( - host = flag.String("s", "127.0.0.1", "The nats server host.") - port = flag.Int("m", 8222, "The NATS server monitoring port.") - conns = flag.Int("n", 1024, "Maximum number of connections to poll.") - delay = flag.Int("d", 1, "Refresh interval in seconds.") - sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") - showVersion = flag.Bool("v", false, "Show nats-top version.") - lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") + host = flag.String("s", "127.0.0.1", "The nats server host.") + port = flag.Int("m", 8222, "The NATS server monitoring port.") + conns = flag.Int("n", 1024, "Maximum number of connections to poll.") + delay = flag.Int("d", 1, "Refresh interval in seconds.") + sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") + lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") + showVersion = flag.Bool("v", false, "Show nats-top version.") + displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") // Secure options httpsPort = flag.Int("ms", 0, "The NATS server secure monitoring port.") @@ -50,7 +51,7 @@ var ( usageHelp = ` usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-sort by] - [-cert FILE] [-key FILE ][-cacert FILE] [-k] + [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] ` // cache for reducing DNS lookups in case enabled @@ -169,15 +170,15 @@ func generateParagraph( serverVersion = stats.Varz.Version } - mem := top.Psize(memVal) - inMsgs := top.Psize(inMsgsVal) - outMsgs := top.Psize(outMsgsVal) - inBytes := top.Psize(inBytesVal) - outBytes := top.Psize(outBytesVal) + mem := top.Psize(false, memVal) //memory is exempt from the rawbytes flag + inMsgs := top.Psize(*displayRawBytes, inMsgsVal) + outMsgs := top.Psize(*displayRawBytes, outMsgsVal) + inBytes := top.Psize(*displayRawBytes, inBytesVal) + outBytes := top.Psize(*displayRawBytes, outBytesVal) inMsgsRate := stats.Rates.InMsgsRate outMsgsRate := stats.Rates.OutMsgsRate - inBytesRate := top.Psize(int64(stats.Rates.InBytesRate)) - outBytesRate := top.Psize(int64(stats.Rates.OutBytesRate)) + inBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.InBytesRate)) + outBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.OutBytesRate)) info := "NATS server version %s (uptime: %s) %s" info += "\nServer:\n Load: CPU: %.1f%% Memory: %s Slow Consumers: %d\n" @@ -318,8 +319,8 @@ func generateParagraph( } connLineInfo = append(connLineInfo, conn.NumSubs) - connLineInfo = append(connLineInfo, top.Psize(int64(conn.Pending)), top.Psize(conn.OutMsgs), top.Psize(conn.InMsgs)) - connLineInfo = append(connLineInfo, top.Psize(conn.OutBytes), top.Psize(conn.InBytes)) + connLineInfo = append(connLineInfo, top.Psize(*displayRawBytes, int64(conn.Pending)), top.Psize(*displayRawBytes, conn.OutMsgs), top.Psize(*displayRawBytes, conn.InMsgs)) + connLineInfo = append(connLineInfo, top.Psize(*displayRawBytes, conn.OutBytes), top.Psize(*displayRawBytes, conn.InBytes)) connLineInfo = append(connLineInfo, conn.Lang, conn.Version) connLineInfo = append(connLineInfo, conn.Uptime, conn.LastActivity) diff --git a/readme.md b/readme.md index 786b296..024767f 100644 --- a/readme.md +++ b/readme.md @@ -41,7 +41,7 @@ and releases of the binary are also [available](https://github.com/nats-io/nats- ``` usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-sort by] - [-cert FILE] [-key FILE ][-cacert FILE] [-k] + [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] ``` - `-m http_port`, `-ms https_port` @@ -68,6 +68,10 @@ usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] Configure to skip verification of certificate. +- `-b` + + Displays traffic in raw bytes. + ## Commands While in top view, it is possible to use the following commands: diff --git a/util/toputils.go b/util/toputils.go index baf3cd0..7683e9e 100644 --- a/util/toputils.go +++ b/util/toputils.go @@ -249,19 +249,25 @@ type Rates struct { OutBytesRate float64 } +const kibibyte = 1024 +const mebibyte = 1024 * 1024 +const gibibyte = 1024 * 1024 * 1024 + // Psize takes a float and returns a human readable string. -func Psize(s int64) string { +func Psize(displayRawValue bool, s int64) string { size := float64(s) - if size < 1024 { + if displayRawValue || size < kibibyte { return fmt.Sprintf("%.0f", size) - } else if size < (1024 * 1024) { - return fmt.Sprintf("%.1fK", size/1024) - } else if size < (1024 * 1024 * 1024) { - return fmt.Sprintf("%.1fM", size/1024/1024) - } else if size >= (1024 * 1024 * 1024) { - return fmt.Sprintf("%.1fG", size/1024/1024/1024) - } else { - return "NA" } + + if size < mebibyte { + return fmt.Sprintf("%.1fK", size/kibibyte) + } + + if size < gibibyte { + return fmt.Sprintf("%.1fM", size/mebibyte) + } + + return fmt.Sprintf("%.1fG", size/gibibyte) } diff --git a/util/toputils_test.go b/util/toputils_test.go index d318eda..9e76697 100644 --- a/util/toputils_test.go +++ b/util/toputils_test.go @@ -101,28 +101,91 @@ func TestFetchingStatz(t *testing.T) { func TestPsize(t *testing.T) { - expected := "1023" - got := Psize(1023) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + type Args struct { + displayRawBytes bool + input int64 } - expected = "1.0K" - got = Psize(1024) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + testcases := []struct { + description string + args Args + want string + }{ + { + description: "given input 1023 and display_raw_bytes false expect return value string to be '1023'", + args: Args{ + input: int64(1023), + displayRawBytes: false, + }, + want: "1023", + }, + { + description: "given input kibibyte and display_raw_bytes false expect return value string to be '1.0K'", + args: Args{ + input: int64(kibibyte), + displayRawBytes: false, + }, + want: "1.0K", + }, + { + description: "given input mebibyte and display_raw_bytes false expect return value string to be '1.0M'", + args: Args{ + input: int64(mebibyte), + displayRawBytes: false, + }, + want: "1.0M", + }, + { + description: "given input gibibyte and display_raw_bytes false expect return value string to be '1.0G'", + args: Args{ + input: int64(gibibyte), + displayRawBytes: false, + }, + want: "1.0G", + }, + + { + description: "given input 1023 and display_raw_bytes true expect return value string to be '1023'", + args: Args{ + input: int64(1023), + displayRawBytes: true, + }, + want: "1023", + }, + { + description: "given input kibibyte and display_raw_bytes true expect return value string to be '1048576'", + args: Args{ + input: int64(kibibyte), + displayRawBytes: true, + }, + want: fmt.Sprintf("%d", kibibyte), + }, + { + description: "given input mebibyte and display_raw_bytes true expect return value string to be '1048576'", + args: Args{ + input: int64(mebibyte), + displayRawBytes: true, + }, + want: fmt.Sprintf("%d", mebibyte), + }, + { + description: "given input gibibyte and display_raw_bytes true expect return value string to be '1073741824'", + args: Args{ + input: int64(gibibyte), + displayRawBytes: true, + }, + want: fmt.Sprintf("%d", gibibyte), + }, } - expected = "1.0M" - got = Psize(1024 * 1024) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) - } + for _, testcase := range testcases { + t.Run(testcase.description, func(t *testing.T) { + got := Psize(testcase.args.displayRawBytes, testcase.args.input) - expected = "1.0G" - got = Psize(1024 * 1024 * 1024) - if got != expected { - t.Fatalf("Wrong human readable value. expected: %v, got: %v", expected, got) + if got != testcase.want { + t.Errorf("%s wanted \"%s\", got \"%s\"", testcase.description, testcase.want, got) + } + }) } } From 184d95093221d27e4813bf26218e32fedc6fee9c Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 18:58:53 +0100 Subject: [PATCH 09/33] clean (StartUI): trivial neutral cleanups --- nats-top.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/nats-top.go b/nats-top.go index a06a70a..4d35350 100644 --- a/nats-top.go +++ b/nats-top.go @@ -391,12 +391,9 @@ func StartUI(engine *top.Engine) { update := func() { for { - receivedStats := <-engine.StatsCh - stats := receivedStats + stats := <-engine.StatsCh - // Update top view text - text = generateParagraph(engine, stats) - par.Text = text + par.Text = generateParagraph(engine, stats) // Update top view text redraw <- struct{}{} } From 9ca0311f310ee43a0a3da07af783a14cd82c6668 Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 19:03:51 +0100 Subject: [PATCH 10/33] refa (redraw mech): the redraw channel now records the reason behind each redraw with this change we can support the new '-r N' flag to exit upon refreshing N times --- nats-top.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/nats-top.go b/nats-top.go index 4d35350..eec604a 100644 --- a/nats-top.go +++ b/nats-top.go @@ -346,6 +346,13 @@ const ( HelpViewMode ) +type RedrawCause int + +const ( + DueToNewStats RedrawCause = iota + DueToViewportResize +) + // StartUI periodically refreshes the screen using recent data. func StartUI(engine *top.Engine) { @@ -387,7 +394,7 @@ func StartUI(engine *top.Engine) { viewMode := TopViewMode // Used for pinging the IU to refresh the screen with new values - redraw := make(chan struct{}) + redraw := make(chan RedrawCause) update := func() { for { @@ -395,7 +402,7 @@ func StartUI(engine *top.Engine) { par.Text = generateParagraph(engine, stats) // Update top view text - redraw <- struct{}{} + redraw <- DueToNewStats } } @@ -552,7 +559,7 @@ func StartUI(engine *top.Engine) { if e.Type == ui.EventResize { ui.Body.Width = ui.TermWidth() ui.Body.Align() - go func() { redraw <- struct{}{} }() + go func() { redraw <- DueToViewportResize }() } case <-redraw: From 9edf722901302af21e0a1d9bd2ed3291395a926a Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 19:31:41 +0100 Subject: [PATCH 11/33] feat (cli -r ): new flag "-r " to specify the maximum number of times nats-top should refresh nats-stats before exiting --- nats-top.go | 31 +++++++++++++++++++++---------- readme.md | 6 +++++- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/nats-top.go b/nats-top.go index eec604a..e22d444 100644 --- a/nats-top.go +++ b/nats-top.go @@ -18,14 +18,15 @@ import ( const version = "0.5.0" var ( - host = flag.String("s", "127.0.0.1", "The nats server host.") - port = flag.Int("m", 8222, "The NATS server monitoring port.") - conns = flag.Int("n", 1024, "Maximum number of connections to poll.") - delay = flag.Int("d", 1, "Refresh interval in seconds.") - sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") - lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") - showVersion = flag.Bool("v", false, "Show nats-top version.") - displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") + host = flag.String("s", "127.0.0.1", "The nats server host.") + port = flag.Int("m", 8222, "The NATS server monitoring port.") + conns = flag.Int("n", 1024, "Maximum number of connections to poll.") + delay = flag.Int("d", 1, "Refresh interval in seconds.") + sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") + lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") + showVersion = flag.Bool("v", false, "Show nats-top version.") + displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") + maxStatsRefreshes = flag.Int("r", -1, "Specifies the maximum number of times nats-top should refresh nats-stats before exiting.") // Secure options httpsPort = flag.Int("ms", 0, "The NATS server secure monitoring port.") @@ -50,7 +51,7 @@ var ( defaultRowFormat = "%-6d %-10s %-10s %-10s %-10s %-10s %-7s %-7s %-7s %-40s" usageHelp = ` -usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-sort by] +usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-r max] [-sort by] [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] ` @@ -429,6 +430,7 @@ func StartUI(engine *top.Engine) { go update() + numberOfRedrawsDueToNewStats := 0 for { select { case e := <-evt: @@ -562,8 +564,17 @@ func StartUI(engine *top.Engine) { go func() { redraw <- DueToViewportResize }() } - case <-redraw: + case cause := <-redraw: ui.Render(ui.Body) + + if cause == DueToNewStats { + numberOfRedrawsDueToNewStats += 1 + + if *maxStatsRefreshes > 0 && numberOfRedrawsDueToNewStats >= *maxStatsRefreshes { + close(engine.ShutdownCh) + cleanExit() + } + } } } } diff --git a/readme.md b/readme.md index 024767f..b503ad1 100644 --- a/readme.md +++ b/readme.md @@ -40,7 +40,7 @@ and releases of the binary are also [available](https://github.com/nats-io/nats- ## Usage ``` -usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-sort by] +usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-r max] [-sort by] [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] ``` @@ -56,6 +56,10 @@ usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] Screen refresh interval (default: 1 second). +- `-r max` + + Specify the maximum number of times nats-top should refresh nats-stats before exiting (default: `0` which stands for `"no limit"`). + - `-sort by ` Field to use for sorting the connections. From 6e6ce7b0c140e874010c37ccd8b06712a41cd31b Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 21:45:36 +0100 Subject: [PATCH 12/33] feat (FetchStats()): extracted the fetching logic of MonitorStats() into a new method --- util/toputils.go | 89 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/util/toputils.go b/util/toputils.go index 7683e9e..61e0b8c 100644 --- a/util/toputils.go +++ b/util/toputils.go @@ -192,6 +192,95 @@ func (engine *Engine) MonitorStats() error { } } +func (engine *Engine) FetchStats(isFirstTime bool, lastPollTime time.Time) (*Stats, time.Time) { + var inMsgsDelta int64 + var outMsgsDelta int64 + var inBytesDelta int64 + var outBytesDelta int64 + + var inMsgsLastVal int64 + var outMsgsLastVal int64 + var inBytesLastVal int64 + var outBytesLastVal int64 + + var inMsgsRate float64 + var outMsgsRate float64 + var inBytesRate float64 + var outBytesRate float64 + + stats := &Stats{ + Varz: &server.Varz{}, + Connz: &server.Connz{}, + Rates: &Rates{}, + Error: fmt.Errorf(""), + } + + // Get /varz + { + result, err := engine.Request("/varz") + if err != nil { + stats.Error = err + engine.StatsCh <- stats + return nil, time.Time{} + } + + if varz, ok := result.(*server.Varz); ok { + stats.Varz = varz + } + } + + // Get /connz + { + result, err := engine.Request("/connz") + if err != nil { + stats.Error = err + engine.StatsCh <- stats + return nil, time.Time{} + } + + if connz, ok := result.(*server.Connz); ok { + stats.Connz = connz + } + } + + // Periodic snapshot to get per sec metrics + inMsgsVal := stats.Varz.InMsgs + outMsgsVal := stats.Varz.OutMsgs + inBytesVal := stats.Varz.InBytes + outBytesVal := stats.Varz.OutBytes + + inMsgsDelta = inMsgsVal - inMsgsLastVal + outMsgsDelta = outMsgsVal - outMsgsLastVal + inBytesDelta = inBytesVal - inBytesLastVal + outBytesDelta = outBytesVal - outBytesLastVal + + inMsgsLastVal = inMsgsVal + outMsgsLastVal = outMsgsVal + inBytesLastVal = inBytesVal + outBytesLastVal = outBytesVal + + now := time.Now() + tdelta := now.Sub(lastPollTime) + newLastPollTime := now + + // Calculate rates but the first time + if !isFirstTime { + inMsgsRate = float64(inMsgsDelta) / tdelta.Seconds() + outMsgsRate = float64(outMsgsDelta) / tdelta.Seconds() + inBytesRate = float64(inBytesDelta) / tdelta.Seconds() + outBytesRate = float64(outBytesDelta) / tdelta.Seconds() + } + + stats.Rates = &Rates{ + InMsgsRate: inMsgsRate, + OutMsgsRate: outMsgsRate, + InBytesRate: inBytesRate, + OutBytesRate: outBytesRate, + } + + return stats, newLastPollTime +} + // SetupHTTPS sets up the http client and uri to use for polling. func (engine *Engine) SetupHTTPS(caCertOpt, certOpt, keyOpt string, skipVerifyOpt bool) error { tlsConfig := &tls.Config{} From 6d6ea5893c16532ab07ee3c7974350b2aaa1039f Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 21:48:25 +0100 Subject: [PATCH 13/33] feat (MonitorStats()): simplify the method by having it invoke FetchStats() to fetch new nats-stats --- util/toputils.go | 95 ++++-------------------------------------------- 1 file changed, 7 insertions(+), 88 deletions(-) diff --git a/util/toputils.go b/util/toputils.go index 61e0b8c..f498265 100644 --- a/util/toputils.go +++ b/util/toputils.go @@ -90,103 +90,22 @@ func (engine *Engine) Request(path string) (interface{}, error) { // MonitorStats is ran as a goroutine and takes options // which can modify how poll values then sends to channel. func (engine *Engine) MonitorStats() error { - var pollTime time.Time - - var inMsgsDelta int64 - var outMsgsDelta int64 - var inBytesDelta int64 - var outBytesDelta int64 - - var inMsgsLastVal int64 - var outMsgsLastVal int64 - var inBytesLastVal int64 - var outBytesLastVal int64 - - var inMsgsRate float64 - var outMsgsRate float64 - var inBytesRate float64 - var outBytesRate float64 - - first := true - pollTime = time.Now() - delay := time.Duration(engine.Delay) * time.Second + isFirstTime := true + lastPollTime := time.Now() for { - stats := &Stats{ - Varz: &server.Varz{}, - Connz: &server.Connz{}, - Rates: &Rates{}, - Error: fmt.Errorf(""), - } - select { case <-engine.ShutdownCh: return nil case <-time.After(delay): - // Get /varz - { - result, err := engine.Request("/varz") - if err != nil { - stats.Error = err - engine.StatsCh <- stats - continue - } - if varz, ok := result.(*server.Varz); ok { - stats.Varz = varz - } - } - - // Get /connz - { - result, err := engine.Request("/connz") - if err != nil { - stats.Error = err - engine.StatsCh <- stats - continue - } - if connz, ok := result.(*server.Connz); ok { - stats.Connz = connz - } - } - - // Periodic snapshot to get per sec metrics - inMsgsVal := stats.Varz.InMsgs - outMsgsVal := stats.Varz.OutMsgs - inBytesVal := stats.Varz.InBytes - outBytesVal := stats.Varz.OutBytes - - inMsgsDelta = inMsgsVal - inMsgsLastVal - outMsgsDelta = outMsgsVal - outMsgsLastVal - inBytesDelta = inBytesVal - inBytesLastVal - outBytesDelta = outBytesVal - outBytesLastVal - - inMsgsLastVal = inMsgsVal - outMsgsLastVal = outMsgsVal - inBytesLastVal = inBytesVal - outBytesLastVal = outBytesVal - - now := time.Now() - tdelta := now.Sub(pollTime) - pollTime = now - - // Calculate rates but the first time - if first { - first = false - } else { - inMsgsRate = float64(inMsgsDelta) / tdelta.Seconds() - outMsgsRate = float64(outMsgsDelta) / tdelta.Seconds() - inBytesRate = float64(inBytesDelta) / tdelta.Seconds() - outBytesRate = float64(outBytesDelta) / tdelta.Seconds() - } - - stats.Rates = &Rates{ - InMsgsRate: inMsgsRate, - OutMsgsRate: outMsgsRate, - InBytesRate: inBytesRate, - OutBytesRate: outBytesRate, + stats, newLastPollTime := engine.FetchStats(isFirstTime, lastPollTime) + if stats == nil { + continue } + isFirstTime = false + lastPollTime = newLastPollTime engine.StatsCh <- stats } } From e6ce7c959d4e96677de4367a008ebf305b510e6a Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 22:16:24 +0100 Subject: [PATCH 14/33] clean (FetchStats()): trivial simplification --- util/toputils.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/util/toputils.go b/util/toputils.go index f498265..1d8d883 100644 --- a/util/toputils.go +++ b/util/toputils.go @@ -180,7 +180,6 @@ func (engine *Engine) FetchStats(isFirstTime bool, lastPollTime time.Time) (*Sta now := time.Now() tdelta := now.Sub(lastPollTime) - newLastPollTime := now // Calculate rates but the first time if !isFirstTime { @@ -197,7 +196,7 @@ func (engine *Engine) FetchStats(isFirstTime bool, lastPollTime time.Time) (*Sta OutBytesRate: outBytesRate, } - return stats, newLastPollTime + return stats, now } // SetupHTTPS sets up the http client and uri to use for polling. From 4aa3ad6ecc29d5d790f3dd3ca990bbc7644ce4d0 Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 22:45:19 +0100 Subject: [PATCH 15/33] feat (cli flag -o): new flag to save a snapshot of the output directly into a file --- nats-top.go | 23 +++++++++++++++++++++++ util/toputils.go | 10 ++++++++-- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/nats-top.go b/nats-top.go index e22d444..3d409c9 100644 --- a/nats-top.go +++ b/nats-top.go @@ -24,6 +24,7 @@ var ( delay = flag.Int("d", 1, "Refresh interval in seconds.") sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") + outputFile = flag.String("o", "", "Save the very first nats-top snapshot to the given file and exit.") showVersion = flag.Bool("v", false, "Show nats-top version.") displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") maxStatsRefreshes = flag.Int("r", -1, "Specifies the maximum number of times nats-top should refresh nats-stats before exiting.") @@ -115,6 +116,11 @@ func main() { } engine.SortOpt = sortOpt + if *outputFile != "" { + saveStatsSnapshotToFile(engine, outputFile) + return + } + err = ui.Init() if err != nil { panic(err) @@ -122,9 +128,26 @@ func main() { defer ui.Close() go engine.MonitorStats() + StartUI(engine) } +func saveStatsSnapshotToFile(engine *top.Engine, outputFile *string) { + f, err := os.OpenFile(*outputFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + log.Fatalf("nats-top: failed to open output file '%s': %s\n", *outputFile, err) + } + + stats := engine.FetchStatsSnapshot() + text := generateParagraph(engine, stats) + + if _, err = f.WriteString(text); err != nil { + log.Fatalf("nats-top: failed to write stats-snapshot to output file '%s': %s\n", *outputFile, err) + } + + f.Close() //no point to error check process will exit anyway +} + // clearScreen tries to ensure resetting original state of screen func clearScreen() { fmt.Print("\033[2J\033[1;1H\033[?25l") diff --git a/util/toputils.go b/util/toputils.go index 1d8d883..4405b72 100644 --- a/util/toputils.go +++ b/util/toputils.go @@ -99,7 +99,7 @@ func (engine *Engine) MonitorStats() error { case <-engine.ShutdownCh: return nil case <-time.After(delay): - stats, newLastPollTime := engine.FetchStats(isFirstTime, lastPollTime) + stats, newLastPollTime := engine.fetchStats(isFirstTime, lastPollTime) if stats == nil { continue } @@ -111,7 +111,13 @@ func (engine *Engine) MonitorStats() error { } } -func (engine *Engine) FetchStats(isFirstTime bool, lastPollTime time.Time) (*Stats, time.Time) { +func (engine *Engine) FetchStatsSnapshot() *Stats { + stats, _ := engine.fetchStats(true, time.Now()) + + return stats +} + +func (engine *Engine) fetchStats(isFirstTime bool, lastPollTime time.Time) (*Stats, time.Time) { var inMsgsDelta int64 var outMsgsDelta int64 var inBytesDelta int64 From 7df68685db5612fefab5073ecbcdeb4128362ed3 Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Tue, 8 Feb 2022 23:23:43 +0100 Subject: [PATCH 16/33] feat (cli flag '-o -'): passing '-o -' now prints the snapshot to the standard output --- nats-top.go | 13 +++++++++---- readme.md | 4 ++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/nats-top.go b/nats-top.go index 3d409c9..49f3301 100644 --- a/nats-top.go +++ b/nats-top.go @@ -24,7 +24,7 @@ var ( delay = flag.Int("d", 1, "Refresh interval in seconds.") sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") - outputFile = flag.String("o", "", "Save the very first nats-top snapshot to the given file and exit.") + outputFile = flag.String("o", "", "Save the very first nats-top snapshot to the given file and exit. If '-' is passed then the snapshot is printed the standard output.") showVersion = flag.Bool("v", false, "Show nats-top version.") displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") maxStatsRefreshes = flag.Int("r", -1, "Specifies the maximum number of times nats-top should refresh nats-stats before exiting.") @@ -133,14 +133,19 @@ func main() { } func saveStatsSnapshotToFile(engine *top.Engine, outputFile *string) { + stats := engine.FetchStatsSnapshot() + text := generateParagraph(engine, stats) + + if *outputFile == "-" { + fmt.Print(text) + return + } + f, err := os.OpenFile(*outputFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) if err != nil { log.Fatalf("nats-top: failed to open output file '%s': %s\n", *outputFile, err) } - stats := engine.FetchStatsSnapshot() - text := generateParagraph(engine, stats) - if _, err = f.WriteString(text); err != nil { log.Fatalf("nats-top: failed to write stats-snapshot to output file '%s': %s\n", *outputFile, err) } diff --git a/readme.md b/readme.md index b503ad1..7c76c23 100644 --- a/readme.md +++ b/readme.md @@ -60,6 +60,10 @@ usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] Specify the maximum number of times nats-top should refresh nats-stats before exiting (default: `0` which stands for `"no limit"`). +- `-o file` + + Saves the very first nats-top snapshot to the given file and exits. If '-' is passed then the snapshot is printed to the standard output. + - `-sort by ` Field to use for sorting the connections. From 21acd7c73a13b80feaa4cd146681e2c6a2cc1c12 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Sun, 13 Feb 2022 17:18:55 +0100 Subject: [PATCH 17/33] sidefix (nats-top): replace log.Fatalf(...) that are followed by calls to usage() with calls to fmt.Fprintf(os.stderr, ...) this fix allows the usage message to be actually printed (old was: log.Fatalf(...) would cause the program to os.exit(1) before the usage message could actually be printed) --- nats-top.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nats-top.go b/nats-top.go index 49f3301..553364f 100644 --- a/nats-top.go +++ b/nats-top.go @@ -84,7 +84,7 @@ func main() { engine = top.NewEngine(*host, *httpsPort, *conns, *delay) err := engine.SetupHTTPS(*caCertOpt, *certOpt, *keyOpt, *skipVerifyOpt) if err != nil { - log.Printf("nats-top: %s", err) + fmt.Fprintf(os.Stderr, "nats-top: %s", err) usage() } } else { @@ -93,25 +93,25 @@ func main() { } if engine.Host == "" { - log.Printf("nats-top: invalid monitoring endpoint") + fmt.Fprintf(os.Stderr, "nats-top: invalid monitoring endpoint") usage() } if engine.Port == 0 { - log.Printf("nats-top: invalid monitoring port") + fmt.Fprintf(os.Stderr, "nats-top: invalid monitoring port") usage() } // Smoke test to abort in case can't connect to server since the beginning. _, err := engine.Request("/varz") if err != nil { - log.Printf("nats-top: /varz smoke test failed: %s", err) + fmt.Fprintf(os.Stderr, "nats-top: /varz smoke test failed: %s", err) usage() } sortOpt := server.SortOpt(*sortBy) if !sortOpt.IsValid() { - log.Fatalf("nats-top: invalid option to sort by: %s\n", sortOpt) + fmt.Fprintf(os.Stderr, "nats-top: invalid option to sort by: %s\n", sortOpt) usage() } engine.SortOpt = sortOpt From cd0725c65bc73e93148cb2201a661eef12eaa960 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Sun, 13 Feb 2022 17:20:34 +0100 Subject: [PATCH 18/33] clean (nats-top): remove method exitWithError() since it's not being used --- nats-top.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/nats-top.go b/nats-top.go index 553364f..eef3e4a 100644 --- a/nats-top.go +++ b/nats-top.go @@ -167,15 +167,6 @@ func cleanExit() { os.Exit(0) } -func exitWithError() { - ui.Close() - - // Show cursor once again - fmt.Print("\033[?25h") - - os.Exit(1) -} - // generateParagraph takes an options map and latest Stats // then returns a formatted paragraph ready to be rendered func generateParagraph( From 480150ba49d8ca1d0a1e149c92ae7b419bd540f2 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Sun, 13 Feb 2022 17:36:22 +0100 Subject: [PATCH 19/33] sideclean (refreshOptionHeader): use fmt.Print() instead of fmt.Printf() --- nats-top.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nats-top.go b/nats-top.go index eef3e4a..323e208 100644 --- a/nats-top.go +++ b/nats-top.go @@ -433,14 +433,13 @@ func StartUI(engine *top.Engine) { optionBuf := "" refreshOptionHeader := func() { - // Need to mask what was typed before - clrline := "\033[1;1H\033[6;1H " + clrline := "\033[1;1H\033[6;1H " // Need to mask what was typed before clrline += " " for i := 0; i < len(optionBuf); i++ { clrline += " " } - fmt.Printf(clrline) + fmt.Print(clrline) } evt := ui.EventCh() From dcf20c0cf565cf439b2992dcde2f822cef00bbd0 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Sun, 13 Feb 2022 17:41:26 +0100 Subject: [PATCH 20/33] clean (nats-top): remove 'defaultHeader' as it was not being used anywhere --- nats-top.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/nats-top.go b/nats-top.go index 323e208..b8bbf81 100644 --- a/nats-top.go +++ b/nats-top.go @@ -45,8 +45,6 @@ const ( ) var ( - defaultHeader = []interface{}{"HOST", "CID", "NAME", "SUBS", "PENDING", "MSGS_TO", "MSGS_FROM", "BYTES_TO", "BYTES_FROM", "LANG", "VERSION", "UPTIME", "LAST ACTIVITY"} - // Chopped: HOST CID NAME... defaultHeaderFormat = "%-6s %-10s %-10s %-10s %-10s %-10s %-7s %-7s %-7s %-40s" defaultRowFormat = "%-6d %-10s %-10s %-10s %-10s %-10s %-7s %-7s %-7s %-40s" From 6cd3a89317398941ed4cba4bd05b809f3cd43dbb Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Sun, 13 Feb 2022 17:41:57 +0100 Subject: [PATCH 21/33] clean (nats-top): move comments on the side to conserve vertical space --- nats-top.go | 58 ++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 16 deletions(-) diff --git a/nats-top.go b/nats-top.go index b8bbf81..53c7f64 100644 --- a/nats-top.go +++ b/nats-top.go @@ -17,6 +17,13 @@ import ( const version = "0.5.0" +type outputFormatType string + +const ( + outputFormatCSV = outputFormatType("csv") + outputFormatText = outputFormatType("text") +) + var ( host = flag.String("s", "127.0.0.1", "The nats server host.") port = flag.Int("m", 8222, "The NATS server monitoring port.") @@ -26,6 +33,7 @@ var ( lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") outputFile = flag.String("o", "", "Save the very first nats-top snapshot to the given file and exit. If '-' is passed then the snapshot is printed the standard output.") showVersion = flag.Bool("v", false, "Show nats-top version.") + outputFormat = flag.String("f", string(outputFormatText), "Specifies the format for the output file when the '-o' parameter is used. Can be 'text' (default) or 'csv'.") displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") maxStatsRefreshes = flag.Int("r", -1, "Specifies the maximum number of times nats-top should refresh nats-stats before exiting.") @@ -45,17 +53,16 @@ const ( ) var ( - // Chopped: HOST CID NAME... - defaultHeaderFormat = "%-6s %-10s %-10s %-10s %-10s %-10s %-7s %-7s %-7s %-40s" + defaultHeaderFormat = "%-6s %-10s %-10s %-10s %-10s %-10s %-7s %-7s %-7s %-40s" // Chopped: HOST CID NAME... defaultRowFormat = "%-6d %-10s %-10s %-10s %-10s %-10s %-7s %-7s %-7s %-40s" usageHelp = ` -usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-r max] [-sort by] +usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-r max] [-o FILE] [-f FORMAT] [-sort by] [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] ` - // cache for reducing DNS lookups in case enabled - resolvedHosts = map[string]string{} + + resolvedHosts = map[string]string{} // cache for reducing DNS lookups in case enabled ) func usage() { @@ -115,7 +122,7 @@ func main() { engine.SortOpt = sortOpt if *outputFile != "" { - saveStatsSnapshotToFile(engine, outputFile) + saveStatsSnapshotToFile(engine, outputFile, outputFormatType(*outputFormat)) return } @@ -130,9 +137,9 @@ func main() { StartUI(engine) } -func saveStatsSnapshotToFile(engine *top.Engine, outputFile *string) { +func saveStatsSnapshotToFile(engine *top.Engine, outputFile *string, outputFormat outputFormatType) { stats := engine.FetchStatsSnapshot() - text := generateParagraph(engine, stats) + text := generateParagraph(engine, stats, outputFormat) if *outputFile == "-" { fmt.Print(text) @@ -170,6 +177,7 @@ func cleanExit() { func generateParagraph( engine *top.Engine, stats *top.Stats, + outputFormat outputFormatType, ) string { // Snapshot current stats @@ -198,15 +206,33 @@ func generateParagraph( inBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.InBytesRate)) outBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.OutBytesRate)) - info := "NATS server version %s (uptime: %s) %s" - info += "\nServer:\n Load: CPU: %.1f%% Memory: %s Slow Consumers: %d\n" - info += " In: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s\n" - info += " Out: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s" + info := "" + if outputFormat == outputFormatText || len(outputFormat) == 0 { + info += "NATS server version %s (uptime: %s) %s\n" + info += "Server:\n" + info += " Load: CPU: %.1f%% Memory: %s Slow Consumers: %d\n" + info += " In: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s\n" + info += " Out: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s" + + } else if outputFormat == outputFormatCSV { + info += "NATS server version,%s,uptime:,%s,%s\n" + info += "Server:\n" + info += "Load:,CPU:,%.1f%%,Memory:,%s,Slow Consumers:,%d\n" + info += "In:,Msgs:,%s,Bytes:,%s,Msgs/Sec:,%.1f,Bytes/Sec:,%s\n" + info += "Out:,Msgs:,%s,Bytes:,%s,Msgs/Sec:,%.1f,Bytes/Sec:,%s" - text := fmt.Sprintf(info, serverVersion, uptime, stats.Error, + } else { + panicMsg := fmt.Sprintf("nats-top: unknown output format %q", outputFormat) + panic(panicMsg) + } + + text := fmt.Sprintf( + info, serverVersion, uptime, stats.Error, cpu, mem, slowConsumers, inMsgs, inBytes, inMsgsRate, inBytesRate, - outMsgs, outBytes, outMsgsRate, outBytesRate) + outMsgs, outBytes, outMsgsRate, outBytesRate, + ) + text += fmt.Sprintf("\n\nConnections Polled: %d\n", numConns) displaySubs := engine.DisplaySubs @@ -382,7 +408,7 @@ func StartUI(engine *top.Engine) { } // Show empty values on first display - text := generateParagraph(engine, cleanStats) + text := generateParagraph(engine, cleanStats, outputFormatText) par := ui.NewPar(text) par.Height = ui.TermHeight() par.Width = ui.TermWidth() @@ -418,7 +444,7 @@ func StartUI(engine *top.Engine) { for { stats := <-engine.StatsCh - par.Text = generateParagraph(engine, stats) // Update top view text + par.Text = generateParagraph(engine, stats, outputFormatText) // Update top view text redraw <- DueToNewStats } From 05178d5144869817fc2e4bb38b93a81ec51c8592 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Sun, 13 Feb 2022 18:42:58 +0100 Subject: [PATCH 22/33] clean (nats-top): move comments on the side to conserve vertical space --- nats-top.go | 58 ++++++++++++++++++----------------------------------- 1 file changed, 20 insertions(+), 38 deletions(-) diff --git a/nats-top.go b/nats-top.go index 53c7f64..6495969 100644 --- a/nats-top.go +++ b/nats-top.go @@ -236,27 +236,23 @@ func generateParagraph( text += fmt.Sprintf("\n\nConnections Polled: %d\n", numConns) displaySubs := engine.DisplaySubs - // Dynamically add columns and padding depending - header := make([]interface{}, 0) + header := make([]interface{}, 0) // Dynamically add columns and padding depending hostSize := DEFAULT_HOST_PADDING_SIZE - // Disable name unless we have seen one using it - nameSize := 0 + nameSize := 0 // Disable name unless we have seen one using it for _, conn := range stats.Connz.Conns { var size int var hostname string if *lookupDNS { - // Make a lookup for each one of the ips and memoize - // them for subsequent polls. - if addr, present := resolvedHosts[conn.IP]; !present { + if addr, present := resolvedHosts[conn.IP]; !present { // Make a lookup for each one of the ips and memoize them for subsequent polls addrs, err := net.LookupAddr(conn.IP) if err == nil && len(addrs) > 0 && len(addrs[0]) > 0 { hostname = addrs[0] resolvedHosts[conn.IP] = hostname } else { // Otherwise just continue to use ip:port as resolved host - // can be an empty string even though there were no errors. + // can be an empty string even though there were no errors hostname = fmt.Sprintf("%s:%d", conn.IP, conn.Port) resolvedHosts[conn.IP] = hostname } @@ -267,38 +263,31 @@ func generateParagraph( hostname = fmt.Sprintf("%s:%d", conn.IP, conn.Port) } - // host - size = len(hostname) + size = len(hostname) // host if size > hostSize { hostSize = size + DEFAULT_PADDING_SIZE } - // name - size = len(conn.Name) + size = len(conn.Name) // name if size > nameSize { nameSize = size + DEFAULT_PADDING_SIZE - // If using name, ensure that it is not too small... - minLen := len("NAME") + minLen := len("NAME") // If using name, ensure that it is not too small... if nameSize < minLen { nameSize = minLen } } } - // Initial padding - connHeader := DEFAULT_PADDING + connHeader := DEFAULT_PADDING // Initial padding - // HOST - header = append(header, "HOST") + header = append(header, "HOST") // HOST connHeader += "%-" + fmt.Sprintf("%d", hostSize) + "s " - // CID - header = append(header, "CID") + header = append(header, "CID") // CID connHeader += " %-6s " - // NAME - if nameSize > 0 { + if nameSize > 0 { // NAME header = append(header, "NAME") connHeader += "%-" + fmt.Sprintf("%d", nameSize) + "s " } @@ -308,8 +297,8 @@ func generateParagraph( if displaySubs { connHeader += "%13s" } - // ...LAST ACTIVITY - connHeader += "\n" + + connHeader += "\n" // ...LAST ACTIVITY var connRows string if displaySubs { @@ -319,19 +308,15 @@ func generateParagraph( connRows = fmt.Sprintf(connHeader, header...) } - // Add to screen! - text += connRows + text += connRows // Add to screen! connValues := DEFAULT_PADDING - // HOST: e.g. 192.168.1.1:78901 - connValues += "%-" + fmt.Sprintf("%d", hostSize) + "s " + connValues += "%-" + fmt.Sprintf("%d", hostSize) + "s " // HOST: e.g. 192.168.1.1:78901 - // CID: e.g. 1234 - connValues += " %-6d " + connValues += " %-6d " // CID: e.g. 1234 - // NAME: e.g. hello - if nameSize > 0 { + if nameSize > 0 { // NAME: e.g. hello connValues += "%-" + fmt.Sprintf("%d", nameSize) + "s " } @@ -351,14 +336,12 @@ func generateParagraph( h = fmt.Sprintf("%s:%d", conn.IP, conn.Port) } - // Build the info line - var connLine string + var connLine string // Build the info line connLineInfo := make([]interface{}, 0) connLineInfo = append(connLineInfo, h) connLineInfo = append(connLineInfo, conn.Cid) - // Name not included unless present - if nameSize > 0 { + if nameSize > 0 { // Name not included unless present connLineInfo = append(connLineInfo, conn.Name) } @@ -376,8 +359,7 @@ func generateParagraph( connLine = fmt.Sprintf(connValues, connLineInfo...) } - // Add line to screen! - text += connLine + text += connLine // Add line to screen! } return text From 4f302cc1eefef593124c16f4aa357c6f888f8151 Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Mon, 14 Feb 2022 12:22:19 +0100 Subject: [PATCH 23/33] revert (nats-top.go): revert a hunk that sneaked-in unintentionally in the previous few commits --- nats-top.go | 23 ++++------------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/nats-top.go b/nats-top.go index 6495969..5327dfc 100644 --- a/nats-top.go +++ b/nats-top.go @@ -206,25 +206,10 @@ func generateParagraph( inBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.InBytesRate)) outBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.OutBytesRate)) - info := "" - if outputFormat == outputFormatText || len(outputFormat) == 0 { - info += "NATS server version %s (uptime: %s) %s\n" - info += "Server:\n" - info += " Load: CPU: %.1f%% Memory: %s Slow Consumers: %d\n" - info += " In: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s\n" - info += " Out: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s" - - } else if outputFormat == outputFormatCSV { - info += "NATS server version,%s,uptime:,%s,%s\n" - info += "Server:\n" - info += "Load:,CPU:,%.1f%%,Memory:,%s,Slow Consumers:,%d\n" - info += "In:,Msgs:,%s,Bytes:,%s,Msgs/Sec:,%.1f,Bytes/Sec:,%s\n" - info += "Out:,Msgs:,%s,Bytes:,%s,Msgs/Sec:,%.1f,Bytes/Sec:,%s" - - } else { - panicMsg := fmt.Sprintf("nats-top: unknown output format %q", outputFormat) - panic(panicMsg) - } + info := "NATS server version %s (uptime: %s) %s" + info += "\nServer:\n Load: CPU: %.1f%% Memory: %s Slow Consumers: %d\n" + info += " In: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s\n" + info += " Out: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s" text := fmt.Sprintf( info, serverVersion, uptime, stats.Error, From 813438327fca5abf394d0d151892c0fc58f651a6 Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Mon, 14 Feb 2022 12:31:38 +0100 Subject: [PATCH 24/33] feat (file output formatter): turn the default "plain-text" formatter into its own separate formatter --- nats-top.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/nats-top.go b/nats-top.go index 5327dfc..d6d4266 100644 --- a/nats-top.go +++ b/nats-top.go @@ -180,6 +180,23 @@ func generateParagraph( outputFormat outputFormatType, ) string { + if outputFormat == outputFormatText || len(outputFormat) == 0 { + return generateParagraphPlainText(engine, stats) + } + + // if outputFormat == outputFormatCSV { // TODO + // return generateParagraphCsv(engine, stats, ",") + // } + + panicMsg := fmt.Sprintf("nats-top: unknown output format %q", outputFormat) + panic(panicMsg) +} + +func generateParagraphPlainText( + engine *top.Engine, + stats *top.Stats, +) string { + // Snapshot current stats cpu := stats.Varz.CPU memVal := stats.Varz.Mem From 8deed03ef5c9a8360da247d8ad00f08522534836 Mon Sep 17 00:00:00 2001 From: Kyriakos Sidiropoulos Date: Mon, 14 Feb 2022 16:19:57 +0100 Subject: [PATCH 25/33] feat (generate csv): introduce new method generateParagraphCSV() tailor-made to generate .csv string-output --- nats-top.go | 223 +++++++++++++++++++++++++++++++++++++++++----------- readme.md | 4 + 2 files changed, 181 insertions(+), 46 deletions(-) diff --git a/nats-top.go b/nats-top.go index d6d4266..0866e5a 100644 --- a/nats-top.go +++ b/nats-top.go @@ -17,13 +17,6 @@ import ( const version = "0.5.0" -type outputFormatType string - -const ( - outputFormatCSV = outputFormatType("csv") - outputFormatText = outputFormatType("text") -) - var ( host = flag.String("s", "127.0.0.1", "The nats server host.") port = flag.Int("m", 8222, "The NATS server monitoring port.") @@ -33,7 +26,7 @@ var ( lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") outputFile = flag.String("o", "", "Save the very first nats-top snapshot to the given file and exit. If '-' is passed then the snapshot is printed the standard output.") showVersion = flag.Bool("v", false, "Show nats-top version.") - outputFormat = flag.String("f", string(outputFormatText), "Specifies the format for the output file when the '-o' parameter is used. Can be 'text' (default) or 'csv'.") + outputDelimiter = flag.String("l", "", "Specifies the delimiter to use for the output file when the '-o' parameter is used. By default this option is unset which means that standard grid-like plain-text output will be used.") displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") maxStatsRefreshes = flag.Int("r", -1, "Specifies the maximum number of times nats-top should refresh nats-stats before exiting.") @@ -45,26 +38,12 @@ var ( skipVerifyOpt = flag.Bool("k", false, "Skip verifying server certificate") ) -const ( - DEFAULT_PADDING_SIZE = 2 - DEFAULT_PADDING = " " - - DEFAULT_HOST_PADDING_SIZE = 15 -) - -var ( - defaultHeaderFormat = "%-6s %-10s %-10s %-10s %-10s %-10s %-7s %-7s %-7s %-40s" // Chopped: HOST CID NAME... - defaultRowFormat = "%-6d %-10s %-10s %-10s %-10s %-10s %-7s %-7s %-7s %-40s" - - usageHelp = ` -usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-r max] [-o FILE] [-f FORMAT] [-sort by] +const usageHelp = ` +usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-r max] [-o FILE] [-l DELIMITER] [-sort by] [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] ` - resolvedHosts = map[string]string{} // cache for reducing DNS lookups in case enabled -) - func usage() { log.Fatalf(usageHelp) } @@ -122,7 +101,7 @@ func main() { engine.SortOpt = sortOpt if *outputFile != "" { - saveStatsSnapshotToFile(engine, outputFile, outputFormatType(*outputFormat)) + saveStatsSnapshotToFile(engine, outputFile, *outputDelimiter) return } @@ -137,9 +116,9 @@ func main() { StartUI(engine) } -func saveStatsSnapshotToFile(engine *top.Engine, outputFile *string, outputFormat outputFormatType) { +func saveStatsSnapshotToFile(engine *top.Engine, outputFile *string, outputDelimiter string) { stats := engine.FetchStatsSnapshot() - text := generateParagraph(engine, stats, outputFormat) + text := generateParagraph(engine, stats, outputDelimiter) if *outputFile == "-" { fmt.Print(text) @@ -177,21 +156,32 @@ func cleanExit() { func generateParagraph( engine *top.Engine, stats *top.Stats, - outputFormat outputFormatType, + outputDelimiter string, ) string { - if outputFormat == outputFormatText || len(outputFormat) == 0 { - return generateParagraphPlainText(engine, stats) + if len(outputDelimiter) > 0 { //default + return generateParagraphCSV(engine, stats, outputDelimiter) } - // if outputFormat == outputFormatCSV { // TODO - // return generateParagraphCsv(engine, stats, ",") - // } - - panicMsg := fmt.Sprintf("nats-top: unknown output format %q", outputFormat) - panic(panicMsg) + return generateParagraphPlainText(engine, stats) } +const ( + DEFAULT_PADDING_SIZE = 2 + DEFAULT_PADDING = " " + + DEFAULT_HOST_PADDING_SIZE = 15 +) + +var ( + resolvedHosts = map[string]string{} // cache for reducing DNS lookups in case enabled + + standardHeaders = []interface{}{"SUBS", "PENDING", "MSGS_TO", "MSGS_FROM", "BYTES_TO", "BYTES_FROM", "LANG", "VERSION", "UPTIME", "LAST_ACTIVITY"} + + defaultHeaderColumns = []string{"%-6s", "%-10s", "%-10s", "%-10s", "%-10s", "%-10s", "%-7s", "%-7s", "%-7s", "%-40s"} // Chopped: HOST CID NAME... + defaultRowColumns = []string{"%-6d", "%-10s", "%-10s", "%-10s", "%-10s", "%-10s", "%-7s", "%-7s", "%-7s", "%-40s"} +) + func generateParagraphPlainText( engine *top.Engine, stats *top.Stats, @@ -223,8 +213,9 @@ func generateParagraphPlainText( inBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.InBytesRate)) outBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.OutBytesRate)) - info := "NATS server version %s (uptime: %s) %s" - info += "\nServer:\n Load: CPU: %.1f%% Memory: %s Slow Consumers: %d\n" + info := "NATS server version %s (uptime: %s) %s\n" + info += "Server:\n" + info += " Load: CPU: %.1f%% Memory: %s Slow Consumers: %d\n" info += " In: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s\n" info += " Out: Msgs: %s Bytes: %s Msgs/Sec: %.1f Bytes/Sec: %s" @@ -294,8 +285,9 @@ func generateParagraphPlainText( connHeader += "%-" + fmt.Sprintf("%d", nameSize) + "s " } - header = append(header, "SUBS", "PENDING", "MSGS_TO", "MSGS_FROM", "BYTES_TO", "BYTES_FROM", "LANG", "VERSION", "UPTIME", "LAST ACTIVITY") - connHeader += defaultHeaderFormat + header = append(header, standardHeaders...) + + connHeader += strings.Join(defaultHeaderColumns, " ") if displaySubs { connHeader += "%13s" } @@ -322,7 +314,7 @@ func generateParagraphPlainText( connValues += "%-" + fmt.Sprintf("%d", nameSize) + "s " } - connValues += defaultRowFormat + connValues += strings.Join(defaultRowColumns, " ") if displaySubs { connValues += "%s" } @@ -356,17 +348,156 @@ func generateParagraphPlainText( if displaySubs { subs := strings.Join(conn.Subs, ", ") connLineInfo = append(connLineInfo, subs) - connLine = fmt.Sprintf(connValues, connLineInfo...) - } else { - connLine = fmt.Sprintf(connValues, connLineInfo...) } + connLine = fmt.Sprintf(connValues, connLineInfo...) + text += connLine // Add line to screen! } return text } +func generateParagraphCSV( + engine *top.Engine, + stats *top.Stats, + delimiter string, +) string { + + defaultHeaderAndRowColumnsForCsv := []string{"%s", "%s", "%s", "%s", "%s", "%s", "%s", "%s", "%s", "%s"} // Chopped: HOST CID NAME... + + cpu := stats.Varz.CPU // Snapshot current stats + memVal := stats.Varz.Mem + uptime := stats.Varz.Uptime + numConns := stats.Connz.NumConns + inMsgsVal := stats.Varz.InMsgs + outMsgsVal := stats.Varz.OutMsgs + inBytesVal := stats.Varz.InBytes + outBytesVal := stats.Varz.OutBytes + slowConsumers := stats.Varz.SlowConsumers + + var serverVersion string + if stats.Varz.Version != "" { + serverVersion = stats.Varz.Version + } + + mem := top.Psize(false, memVal) //memory is exempt from the rawbytes flag + inMsgs := top.Psize(*displayRawBytes, inMsgsVal) + outMsgs := top.Psize(*displayRawBytes, outMsgsVal) + inBytes := top.Psize(*displayRawBytes, inBytesVal) + outBytes := top.Psize(*displayRawBytes, outBytesVal) + inMsgsRate := stats.Rates.InMsgsRate + outMsgsRate := stats.Rates.OutMsgsRate + inBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.InBytesRate)) + outBytesRate := top.Psize(*displayRawBytes, int64(stats.Rates.OutBytesRate)) + + info := "NATS server version[__DELIM__]%s[__DELIM__](uptime: %s)[__DELIM__]%s\n" + info += "Server:\n" + info += "Load:[__DELIM__]CPU:[__DELIM__]%.1f%%[__DELIM__]Memory:[__DELIM__]%s[__DELIM__]Slow Consumers:[__DELIM__]%d\n" + info += "In:[__DELIM__]Msgs:[__DELIM__]%s[__DELIM__]Bytes:[__DELIM__]%s[__DELIM__]Msgs/Sec:[__DELIM__]%.1f[__DELIM__]Bytes/Sec:[__DELIM__]%s\n" + info += "Out:[__DELIM__]Msgs:[__DELIM__]%s[__DELIM__]Bytes:[__DELIM__]%s[__DELIM__]Msgs/Sec:[__DELIM__]%.1f[__DELIM__]Bytes/Sec:[__DELIM__]%s" + + text := fmt.Sprintf( + info, serverVersion, uptime, stats.Error, + cpu, mem, slowConsumers, + inMsgs, inBytes, inMsgsRate, inBytesRate, + outMsgs, outBytes, outMsgsRate, outBytesRate, + ) + + text += fmt.Sprintf("\n\nConnections Polled:[__DELIM__]%d\n", numConns) + + displaySubs := engine.DisplaySubs + for _, conn := range stats.Connz.Conns { + if !*lookupDNS { + continue + } + + _, present := resolvedHosts[conn.IP] + if present { + continue + } + + addrs, err := net.LookupAddr(conn.IP) + + hostname := "" + if err == nil && len(addrs) > 0 && len(addrs[0]) > 0 { // Make a lookup for each one of the ips and memoize them for subsequent polls + hostname = addrs[0] + } else { // Otherwise just continue to use ip:port as resolved host can be an empty string even though there were no errors + hostname = fmt.Sprintf("%s:%d", conn.IP, conn.Port) + } + + resolvedHosts[conn.IP] = hostname + } + + header := make([]interface{}, 0) // Dynamically add columns + connHeader := "" + + header = append(header, "HOST") // HOST + connHeader += "%s[__DELIM__]" + + header = append(header, "CID") // CID + connHeader += "%s[__DELIM__]" + + header = append(header, "NAME") // NAME + connHeader += "%s[__DELIM__]" + + header = append(header, standardHeaders...) + connHeader += strings.Join(defaultHeaderAndRowColumnsForCsv, "[__DELIM__]") + if displaySubs { + connHeader += "%s" + } + + connHeader += "\n" // ...LAST ACTIVITY + + if displaySubs { + header = append(header, "SUBSCRIPTIONS") + } + + text += fmt.Sprintf(connHeader, header...) // Add to screen! + + connValues := "%s[__DELIM__]" // HOST: e.g. 192.168.1.1:78901 + connValues += "%d[__DELIM__]" // CID: e.g. 1234 + connValues += "%s[__DELIM__]" // NAME: e.g. hello + + connValues += strings.Join(defaultHeaderAndRowColumnsForCsv, "[__DELIM__]") + if displaySubs { + connValues += "%s" + } + connValues += "\n" + + for _, conn := range stats.Connz.Conns { + var h string + if *lookupDNS { + if rh, present := resolvedHosts[conn.IP]; present { + h = rh + } + } else { + h = fmt.Sprintf("%s:%d", conn.IP, conn.Port) + } + + connLineInfo := make([]interface{}, 0) + connLineInfo = append(connLineInfo, h) + connLineInfo = append(connLineInfo, conn.Cid) + connLineInfo = append(connLineInfo, conn.Name) + connLineInfo = append(connLineInfo, fmt.Sprintf("%d", conn.NumSubs)) + connLineInfo = append(connLineInfo, top.Psize(*displayRawBytes, int64(conn.Pending)), top.Psize(*displayRawBytes, conn.OutMsgs), top.Psize(*displayRawBytes, conn.InMsgs)) + connLineInfo = append(connLineInfo, top.Psize(*displayRawBytes, conn.OutBytes), top.Psize(*displayRawBytes, conn.InBytes)) + connLineInfo = append(connLineInfo, conn.Lang, conn.Version) + connLineInfo = append(connLineInfo, conn.Uptime, conn.LastActivity) + + if displaySubs { + subs := strings.Join(conn.Subs, "[__DELIM__]") + connLineInfo = append(connLineInfo, subs) + } + + text += fmt.Sprintf(connValues, connLineInfo...) // Add line to screen! + } + + text = strings.Replace(text, "[__DELIM__]", delimiter, -1) + + return text +} + type ViewMode int const ( @@ -392,7 +523,7 @@ func StartUI(engine *top.Engine) { } // Show empty values on first display - text := generateParagraph(engine, cleanStats, outputFormatText) + text := generateParagraph(engine, cleanStats, "") par := ui.NewPar(text) par.Height = ui.TermHeight() par.Width = ui.TermWidth() @@ -428,7 +559,7 @@ func StartUI(engine *top.Engine) { for { stats := <-engine.StatsCh - par.Text = generateParagraph(engine, stats, outputFormatText) // Update top view text + par.Text = generateParagraph(engine, stats, "") // Update top view text redraw <- DueToNewStats } diff --git a/readme.md b/readme.md index 2802d76..fc9604d 100644 --- a/readme.md +++ b/readme.md @@ -64,6 +64,10 @@ usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] Saves the very first nats-top snapshot to the given file and exits. If '-' is passed then the snapshot is printed to the standard output. +- `-l delimiter` + + Specifies the delimiter to use for the output file when the '-o' parameter is used. By default this option is unset which means that standard grid-like plain-text output will be used. + - `-sort by ` Field to use for sorting the connections. From 8275aea1c65547f2c4a4f24e0f74bfa469ae24f5 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Fri, 13 May 2022 17:58:43 +0200 Subject: [PATCH 26/33] feat (--version): add support for printing version via the --version flag --- nats-top.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nats-top.go b/nats-top.go index e6f753a..9252951 100644 --- a/nats-top.go +++ b/nats-top.go @@ -25,7 +25,7 @@ var ( sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") outputFile = flag.String("o", "", "Save the very first nats-top snapshot to the given file and exit. If '-' is passed then the snapshot is printed the standard output.") - showVersion = flag.Bool("v", false, "Show nats-top version.") + showVersion = false outputDelimiter = flag.String("l", "", "Specifies the delimiter to use for the output file when the '-o' parameter is used. By default this option is unset which means that standard grid-like plain-text output will be used.") displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") maxStatsRefreshes = flag.Int("r", -1, "Specifies the maximum number of times nats-top should refresh nats-stats before exiting.") @@ -40,7 +40,7 @@ var ( const usageHelp = ` usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-r max] [-o FILE] [-l DELIMITER] [-sort by] - [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] + [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] [-v|--version] ` @@ -49,14 +49,16 @@ func usage() { } func init() { + flag.BoolVar(&showVersion, "v", false, "Same as --version.") + flag.BoolVar(&showVersion, "version", false, "Show nats-top version.") + log.SetFlags(0) flag.Usage = usage flag.Parse() } func main() { - - if *showVersion { + if showVersion { log.Printf("nats-top v%s", version) os.Exit(0) } From 6321c4a25d5f44fd9d5c144e5e0df12db691d2c4 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Fri, 13 May 2022 18:52:38 +0200 Subject: [PATCH 27/33] feat (--display-subscriptions-column): new flag --- nats-top.go | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/nats-top.go b/nats-top.go index 9252951..a049be3 100644 --- a/nats-top.go +++ b/nats-top.go @@ -18,17 +18,18 @@ import ( const version = "0.5.2" var ( - host = flag.String("s", "127.0.0.1", "The nats server host.") - port = flag.Int("m", 8222, "The NATS server monitoring port.") - conns = flag.Int("n", 1024, "Maximum number of connections to poll.") - delay = flag.Int("d", 1, "Refresh interval in seconds.") - sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") - lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") - outputFile = flag.String("o", "", "Save the very first nats-top snapshot to the given file and exit. If '-' is passed then the snapshot is printed the standard output.") - showVersion = false - outputDelimiter = flag.String("l", "", "Specifies the delimiter to use for the output file when the '-o' parameter is used. By default this option is unset which means that standard grid-like plain-text output will be used.") - displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") - maxStatsRefreshes = flag.Int("r", -1, "Specifies the maximum number of times nats-top should refresh nats-stats before exiting.") + host = flag.String("s", "127.0.0.1", "The nats server host.") + port = flag.Int("m", 8222, "The NATS server monitoring port.") + conns = flag.Int("n", 1024, "Maximum number of connections to poll.") + delay = flag.Int("d", 1, "Refresh interval in seconds.") + sortBy = flag.String("sort", "cid", "Value for which to sort by the connections.") + lookupDNS = flag.Bool("lookup", false, "Enable client addresses DNS lookup.") + outputFile = flag.String("o", "", "Save the very first nats-top snapshot to the given file and exit. If '-' is passed then the snapshot is printed the standard output.") + showVersion = false + outputDelimiter = flag.String("l", "", "Specifies the delimiter to use for the output file when the '-o' parameter is used. By default this option is unset which means that standard grid-like plain-text output will be used.") + displayRawBytes = flag.Bool("b", false, "Display traffic in raw bytes.") + maxStatsRefreshes = flag.Int("r", -1, "Specifies the maximum number of times nats-top should refresh nats-stats before exiting.") + displaySubscriptionsColumn = flag.Bool("display-subscriptions-column", false, "Display subscriptions column upon launch.") // Secure options httpsPort = flag.Int("ms", 0, "The NATS server secure monitoring port.") @@ -40,7 +41,7 @@ var ( const usageHelp = ` usage: nats-top [-s server] [-m http_port] [-ms https_port] [-n num_connections] [-d delay_secs] [-r max] [-o FILE] [-l DELIMITER] [-sort by] - [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] [-v|--version] + [-cert FILE] [-key FILE] [-cacert FILE] [-k] [-b] [-v|--version] ` @@ -78,6 +79,10 @@ func main() { engine.SetupHTTP() } + if *displaySubscriptionsColumn { + engine.DisplaySubs = true + } + if engine.Host == "" { fmt.Fprintf(os.Stderr, "nats-top: invalid monitoring endpoint") usage() @@ -570,7 +575,7 @@ func StartUI(engine *top.Engine) { // Flags for capturing options waitingSortOption := false waitingLimitOption := false - displaySubscriptions := false + displaySubscriptions := engine.DisplaySubs optionBuf := "" refreshOptionHeader := func() { From 181a3921962a4ecd1e624d3df9863c8179f0d1d8 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Fri, 13 May 2022 18:53:10 +0200 Subject: [PATCH 28/33] feat (version): bump to 0.5.3 (old was: 0.5.2) --- nats-top.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nats-top.go b/nats-top.go index a049be3..00e81e2 100644 --- a/nats-top.go +++ b/nats-top.go @@ -15,7 +15,7 @@ import ( ui "gopkg.in/gizak/termui.v1" ) -const version = "0.5.2" +const version = "0.5.3" var ( host = flag.String("s", "127.0.0.1", "The nats server host.") @@ -79,10 +79,6 @@ func main() { engine.SetupHTTP() } - if *displaySubscriptionsColumn { - engine.DisplaySubs = true - } - if engine.Host == "" { fmt.Fprintf(os.Stderr, "nats-top: invalid monitoring endpoint") usage() @@ -107,6 +103,10 @@ func main() { } engine.SortOpt = sortOpt + if *displaySubscriptionsColumn { + engine.DisplaySubs = true + } + if *outputFile != "" { saveStatsSnapshotToFile(engine, outputFile, *outputDelimiter) return From 0faf7f874b0294889d1f24220f8999fc1ed5e3da Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Fri, 13 May 2022 19:24:03 +0200 Subject: [PATCH 29/33] fix (csv output): when emitting csv output via '-o' the subs will be printed separated by whitespace chars instead of ',' because the ',' is frequently reserved to separate entire csv-columns in the resulting output --- nats-top.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nats-top.go b/nats-top.go index 00e81e2..5e14f50 100644 --- a/nats-top.go +++ b/nats-top.go @@ -493,7 +493,7 @@ func generateParagraphCSV( connLineInfo = append(connLineInfo, conn.Uptime, conn.LastActivity) if displaySubs { - subs := strings.Join(conn.Subs, "[__DELIM__]") + subs := "[__DELIM__]" + strings.Join(conn.Subs, " ") // its safer to use a couple of whitespaces instead of commas to separate the subs because comma is reserved to separate entire columns! connLineInfo = append(connLineInfo, subs) } From 4152a5a6d3f8307efc447644f95562dd7e86e1bb Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Fri, 13 May 2022 19:24:35 +0200 Subject: [PATCH 30/33] clean (nats-top): trivial neutral cleanups in generateParagraphCSV() --- nats-top.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nats-top.go b/nats-top.go index 5e14f50..d8ce6e3 100644 --- a/nats-top.go +++ b/nats-top.go @@ -497,10 +497,10 @@ func generateParagraphCSV( connLineInfo = append(connLineInfo, subs) } - text += fmt.Sprintf(connValues, connLineInfo...) // Add line to screen! + text += fmt.Sprintf(connValues, connLineInfo...) } - text = strings.Replace(text, "[__DELIM__]", delimiter, -1) + text = strings.ReplaceAll(text, "[__DELIM__]", delimiter) return text } From d8919617847010ff5645e65646c34926cc9f13b0 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Fri, 13 May 2022 19:26:27 +0200 Subject: [PATCH 31/33] clean (nats-top.go): trivial simplifications in StartUI() --- nats-top.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/nats-top.go b/nats-top.go index d8ce6e3..7e4944c 100644 --- a/nats-top.go +++ b/nats-top.go @@ -575,7 +575,6 @@ func StartUI(engine *top.Engine) { // Flags for capturing options waitingSortOption := false waitingLimitOption := false - displaySubscriptions := engine.DisplaySubs optionBuf := "" refreshOptionHeader := func() { @@ -667,13 +666,7 @@ func StartUI(engine *top.Engine) { } if e.Type == ui.EventKey && e.Ch == 's' && !(waitingLimitOption || waitingSortOption) { - if displaySubscriptions { - displaySubscriptions = false - engine.DisplaySubs = false - } else { - displaySubscriptions = true - engine.DisplaySubs = true - } + engine.DisplaySubs = !engine.DisplaySubs } if e.Type == ui.EventKey && viewMode == HelpViewMode { From b9b08d039ac92398f0c983e50bf93ecd20c8f656 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Fri, 13 May 2022 19:28:12 +0200 Subject: [PATCH 32/33] clean (nats-top.go): more trivial simplifications in StartUI() --- nats-top.go | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/nats-top.go b/nats-top.go index 7e4944c..033ae5c 100644 --- a/nats-top.go +++ b/nats-top.go @@ -698,21 +698,11 @@ func StartUI(engine *top.Engine) { } if e.Type == ui.EventKey && (e.Ch == 'd') && !(waitingSortOption || waitingLimitOption) { - switch *lookupDNS { - case true: - *lookupDNS = false - case false: - *lookupDNS = true - } + *lookupDNS = !*lookupDNS } if e.Type == ui.EventKey && (e.Ch == 'b') && !(waitingSortOption || waitingLimitOption) { - switch *displayRawBytes { - case true: - *displayRawBytes = false - case false: - *displayRawBytes = true - } + *displayRawBytes = !*displayRawBytes } if e.Type == ui.EventResize { From 45411cc08c4c0edef8eabbbd18a57da692604c60 Mon Sep 17 00:00:00 2001 From: Dominick Sidiropoulos Date: Fri, 13 May 2022 21:25:10 +0200 Subject: [PATCH 33/33] clean (.gitignore) --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 891b313..dc47821 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,5 @@ _testmain.go /nats-top # VSCode - .vscode/** *.code-workspace