diff --git a/nats-top.go b/nats-top.go index e22d444..49f3301 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. 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.") @@ -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,31 @@ func main() { defer ui.Close() go engine.MonitorStats() + StartUI(engine) } +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) + } + + 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/readme.md b/readme.md index b503ad1..2802d76 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] [-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] [-sort by] [-cert FILE] [-key FILE ][-cacert FILE] [-k] [-b] ``` @@ -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. diff --git a/util/toputils.go b/util/toputils.go index 7683e9e..2ec74db 100644 --- a/util/toputils.go +++ b/util/toputils.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "crypto/x509" "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -90,8 +91,35 @@ 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 + delay := time.Duration(engine.Delay) * time.Second + isFirstTime := true + lastPollTime := time.Now() + + for { + select { + case <-engine.ShutdownCh: + return nil + case <-time.After(delay): + stats, newLastPollTime := engine.fetchStats(isFirstTime, lastPollTime) + if stats != nil && errors.Is(stats.Error, errDud) { + isFirstTime = false + lastPollTime = newLastPollTime + } + + engine.StatsCh <- stats + } + } +} + +func (engine *Engine) FetchStatsSnapshot() *Stats { + stats, _ := engine.fetchStats(true, time.Now()) + + return stats +} + +var errDud = fmt.Errorf("") +func (engine *Engine) fetchStats(isFirstTime bool, lastPollTime time.Time) (*Stats, time.Time) { var inMsgsDelta int64 var outMsgsDelta int64 var inBytesDelta int64 @@ -107,89 +135,74 @@ func (engine *Engine) MonitorStats() error { var inBytesRate float64 var outBytesRate float64 - first := true - pollTime = time.Now() - - delay := time.Duration(engine.Delay) * time.Second + stats := &Stats{ + Varz: &server.Varz{}, + Connz: &server.Connz{}, + Rates: &Rates{}, + Error: errDud, + } - for { - 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 + return stats, time.Time{} } - 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 - } - } + 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 - } - } + // Get /connz + { + result, err := engine.Request("/connz") + if err != nil { + stats.Error = err + return stats, time.Time{} + } - // 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() - } + if connz, ok := result.(*server.Connz); ok { + stats.Connz = connz + } + } - stats.Rates = &Rates{ - InMsgsRate: inMsgsRate, - OutMsgsRate: outMsgsRate, - InBytesRate: inBytesRate, - OutBytesRate: outBytesRate, - } + // 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) + + // 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() + } - engine.StatsCh <- stats - } + stats.Rates = &Rates{ + InMsgsRate: inMsgsRate, + OutMsgsRate: outMsgsRate, + InBytesRate: inBytesRate, + OutBytesRate: outBytesRate, } + + return stats, now } // SetupHTTPS sets up the http client and uri to use for polling.