diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e4d08f6..b529cd0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,36 +7,45 @@ on: branches: [master] schedule: - cron: '0 14 */15 * *' + workflow_dispatch: + inputs: + force_run: + description: 'Force workflow run' + required: true + type: choice + options: [yes, no] + +permissions: + actions: read + contents: read + statuses: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: Go: name: Go runs-on: ubuntu-latest - env: - SRC_DIR: src/github.com/${{ github.repository }} - strategy: matrix: - go: [ '1.18.x', '1.19.x' ] + go: [ '1.21.x', '1.22.x' ] steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Set up Go - uses: actions/setup-go@v3 + uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - - name: Checkout - uses: actions/checkout@v3 - with: - path: ${{env.SRC_DIR}} - - name: Download dependencies - working-directory: ${{env.SRC_DIR}} run: make deps - name: Build binary - working-directory: ${{env.SRC_DIR}} run: make all Perfecto: @@ -47,10 +56,10 @@ jobs: steps: - name: Code checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Login to GitHub Container Registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -60,3 +69,17 @@ jobs: uses: essentialkaos/perfecto-action@v2 with: files: common/redis-latency-monitor.spec + + Typos: + name: Typos + runs-on: ubuntu-latest + + needs: Go + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Check spelling + continue-on-error: true + uses: crate-ci/typos@master diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bf5ecc9..bfc4df5 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -20,14 +20,14 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 2 - name: Initialize CodeQL - uses: github/codeql-action/init@v2 + uses: github/codeql-action/init@v3 with: languages: go - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 + uses: github/codeql-action/analyze@v3 diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..cc5caea --- /dev/null +++ b/.typos.toml @@ -0,0 +1,5 @@ +[files] +extend-exclude = ["go.sum"] + +[default.extend-identifiers] +O_WRONLY = "O_WRONLY" diff --git a/Makefile b/Makefile index 28aaeeb..8ce60a3 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ ################################################################################ -# This Makefile generated by GoMakeGen 1.5.1 using next command: +# This Makefile generated by GoMakeGen 2.2.0 using next command: # gomakegen --mod . # # More info: https://kaos.sh/gomakegen @@ -9,15 +9,24 @@ export GO111MODULE=on +ifdef VERBOSE ## Print verbose information (Flag) +VERBOSE_FLAG = -v +endif + +MAKEDIR = $(dir $(realpath $(firstword $(MAKEFILE_LIST)))) +GITREV ?= $(shell test -s $(MAKEDIR)/.git && git rev-parse --short HEAD) + +################################################################################ + .DEFAULT_GOAL := help -.PHONY = fmt vet all clean deps mod-init mod-update mod-vendor help +.PHONY = fmt vet all clean deps update init vendor mod-init mod-update mod-download mod-vendor help ################################################################################ all: redis-latency-monitor ## Build all binaries -redis-latency-monitor: ## Build redis-latency-monitor binary - go build redis-latency-monitor.go +redis-latency-monitor: + go build $(VERBOSE_FLAG) -ldflags="-X main.gitrev=$(GITREV)" redis-latency-monitor.go install: ## Install all binaries cp redis-latency-monitor /usr/bin/redis-latency-monitor @@ -25,32 +34,66 @@ install: ## Install all binaries uninstall: ## Uninstall all binaries rm -f /usr/bin/redis-latency-monitor -deps: mod-update ## Download dependencies +init: mod-init ## Initialize new module -mod-init: ## Initialize new module - go mod init - go mod tidy +deps: mod-download ## Download dependencies + +update: mod-update ## Update dependencies to the latest versions -mod-update: ## Download modules to local cache +vendor: mod-vendor ## Make vendored copy of dependencies + +mod-init: +ifdef MODULE_PATH ## Module path for initialization (String) + go mod init $(MODULE_PATH) +else + go mod init +endif + +ifdef COMPAT ## Compatible Go version (String) + go mod tidy $(VERBOSE_FLAG) -compat=$(COMPAT) +else + go mod tidy $(VERBOSE_FLAG) +endif + +mod-update: +ifdef UPDATE_ALL ## Update all dependencies (Flag) + go get -u $(VERBOSE_FLAG) all +else + go get -u $(VERBOSE_FLAG) ./... +endif + +ifdef COMPAT + go mod tidy $(VERBOSE_FLAG) -compat=$(COMPAT) +else + go mod tidy $(VERBOSE_FLAG) +endif + + test -d vendor && rm -rf vendor && go mod vendor $(VERBOSE_FLAG) || : + +mod-download: go mod download -mod-vendor: ## Make vendored copy of dependencies - go mod vendor +mod-vendor: + rm -rf vendor && go mod vendor $(VERBOSE_FLAG) fmt: ## Format source code with gofmt find . -name "*.go" -exec gofmt -s -w {} \; -vet: ## Runs go vet over sources +vet: ## Runs 'go vet' over sources go vet -composites=false -printfuncs=LPrintf,TLPrintf,TPrintf,log.Debug,log.Info,log.Warn,log.Error,log.Critical,log.Print ./... clean: ## Remove generated files rm -f redis-latency-monitor help: ## Show this info - @echo -e '\n\033[1mSupported targets:\033[0m\n' + @echo -e '\n\033[1mTargets:\033[0m\n' @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) \ | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[33m%-23s\033[0m %s\n", $$1, $$2}' + @echo -e '\n\033[1mVariables:\033[0m\n' + @grep -E '^ifdef [A-Z_]+ .*?## .*$$' $(abspath $(lastword $(MAKEFILE_LIST))) \ + | sed 's/ifdef //' \ + | awk 'BEGIN {FS = " .*?## "}; {printf " \033[32m%-14s\033[0m %s\n", $$1, $$2}' @echo -e '' - @echo -e '\033[90mGenerated by GoMakeGen 1.5.1\033[0m\n' + @echo -e '\033[90mGenerated by GoMakeGen 2.2.0\033[0m\n' ################################################################################ diff --git a/README.md b/README.md index d1e9f4a..13debdd 100644 --- a/README.md +++ b/README.md @@ -22,22 +22,16 @@ Tiny Redis client for latency measurement. Utility show `PING` command latency o #### From source -To build the `redis-latency-monitor` from scratch, make sure you have a working Go 1.17+ workspace (_[instructions](https://golang.org/doc/install)_), then: +To build the `redis-latency-monitor` from scratch, make sure you have a working Go 1.19+ workspace (_[instructions](https://go.dev/doc/install)_), then: ``` -go get github.com/essentialkaos/redis-latency-monitor -``` - -If you want to update `redis-latency-monitor` to latest stable release, do: - -``` -go get -u github.com/essentialkaos/redis-latency-monitor +go install github.com/essentialkaos/redis-latency-monitor@latest ``` #### From [ESSENTIAL KAOS Public Repository](https://yum.kaos.st) ```bash -sudo yum install -y https://yum.kaos.st/get/$(uname -r).rpm +sudo yum install -y https://yum.kaos.st/kaos-repo-latest.el$(grep 'CPE_NAME' /etc/os-release | tr -d '"' | cut -d':' -f5).noarch.rpm sudo yum install redis-latency-monitor ``` diff --git a/cli/cli.go b/cli/cli.go new file mode 100644 index 0000000..69240bb --- /dev/null +++ b/cli/cli.go @@ -0,0 +1,647 @@ +package cli + +// ////////////////////////////////////////////////////////////////////////////////// // +// // +// Copyright (c) 2024 ESSENTIAL KAOS // +// Apache License, Version 2.0 // +// // +// ////////////////////////////////////////////////////////////////////////////////// // + +import ( + "bufio" + "fmt" + "io" + "net" + "os" + "os/exec" + "runtime" + "strings" + "time" + + "github.com/essentialkaos/ek/v12/fmtc" + "github.com/essentialkaos/ek/v12/fmtutil" + "github.com/essentialkaos/ek/v12/fmtutil/table" + "github.com/essentialkaos/ek/v12/log" + "github.com/essentialkaos/ek/v12/mathutil" + "github.com/essentialkaos/ek/v12/options" + "github.com/essentialkaos/ek/v12/strutil" + "github.com/essentialkaos/ek/v12/support" + "github.com/essentialkaos/ek/v12/support/deps" + "github.com/essentialkaos/ek/v12/terminal/tty" + "github.com/essentialkaos/ek/v12/timeutil" + "github.com/essentialkaos/ek/v12/usage" + "github.com/essentialkaos/ek/v12/usage/completion/bash" + "github.com/essentialkaos/ek/v12/usage/completion/fish" + "github.com/essentialkaos/ek/v12/usage/completion/zsh" + "github.com/essentialkaos/ek/v12/usage/man" + "github.com/essentialkaos/ek/v12/usage/update" + + "github.com/essentialkaos/redis-latency-monitor/stats" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// App info +const ( + APP = "Redis Latency Monitor" + VER = "3.2.2" + DESC = "Tiny Redis client for latency measurement" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Main constants +const ( + LATENCY_SAMPLE_RATE int = 10 + CONNECT_SAMPLE_RATE = 100 +) + +// Options +const ( + OPT_HOST = "h:host" + OPT_PORT = "p:port" + OPT_AUTH = "a:password" + OPT_TIMEOUT = "t:timeout" + OPT_INTERVAL = "i:interval" + OPT_CONNECT = "c:connect" + OPT_TIMESTAMPS = "T:timestamps" + OPT_OUTPUT = "o:output" + OPT_ERROR_LOG = "e:error-log" + OPT_NO_COLOR = "nc:no-color" + OPT_HELP = "help" + OPT_VER = "v:version" + + OPT_VERB_VER = "vv:verbose-version" + OPT_COMPLETION = "completion" + OPT_GENERATE_MAN = "generate-man" +) + +// ////////////////////////////////////////////////////////////////////////////////// // + +// optMap is map with options +var optMap = options.Map{ + OPT_HOST: {Type: options.MIXED, Value: "127.0.0.1"}, + OPT_PORT: {Value: "6379"}, + OPT_CONNECT: {Type: options.BOOL}, + OPT_TIMEOUT: {Type: options.INT, Value: 3, Min: 1, Max: 300}, + OPT_AUTH: {}, + OPT_INTERVAL: {Type: options.INT, Value: 60, Min: 1, Max: 3600}, + OPT_TIMESTAMPS: {Type: options.BOOL}, + OPT_OUTPUT: {}, + OPT_ERROR_LOG: {}, + OPT_NO_COLOR: {Type: options.BOOL}, + OPT_HELP: {Type: options.BOOL}, + OPT_VER: {Type: options.MIXED}, + + OPT_VERB_VER: {Type: options.BOOL}, + OPT_COMPLETION: {}, + OPT_GENERATE_MAN: {Type: options.BOOL}, +} + +// colorTagApp contains color tag for app name +var colorTagApp string + +// colorTagVer contains color tag for app version +var colorTagVer string + +// pingCommand is PING command data +var pingCommand = []byte("PING\r\n") + +// conn is connection to Redis +var conn net.Conn + +// host is Redis host +var host string + +// timeout is connection timeout +var timeout time.Duration + +// outputWriter is buffered output writer +var outputWriter *bufio.Writer + +// errorLogged is error logging flag +var errorLogged bool + +// ////////////////////////////////////////////////////////////////////////////////// // + +// Run is main application function +func Run(gitRev string, gomod []byte) { + preConfigureUI() + + runtime.GOMAXPROCS(4) + + _, errs := options.Parse(optMap) + + if len(errs) != 0 { + for _, err := range errs { + printError(err.Error()) + } + + os.Exit(1) + } + + configureUI() + + switch { + case options.Has(OPT_COMPLETION): + os.Exit(printCompletion()) + case options.Has(OPT_GENERATE_MAN): + printMan() + os.Exit(0) + case options.GetB(OPT_VER): + genAbout(gitRev).Print(options.GetS(OPT_VER)) + os.Exit(0) + case options.GetB(OPT_VERB_VER): + support.Collect(APP, VER). + WithRevision(gitRev). + WithDeps(deps.Extract(gomod)). + WithApps(getRedisVersionInfo()). + Print() + os.Exit(0) + case options.GetB(OPT_HELP), options.GetS(OPT_HOST) == "true": + genUsage().Print() + os.Exit(0) + } + + if options.Has(OPT_ERROR_LOG) { + setupErrorLog() + } + + startMeasurementProcess() +} + +// preConfigureUI preconfigures UI based on information about user terminal +func preConfigureUI() { + if !tty.IsTTY() { + fmtc.DisableColors = true + } + + switch { + case fmtc.IsTrueColorSupported(): + colorTagApp, colorTagVer = "{*}{#DC382C}", "{#A32422}" + case fmtc.Is256ColorsSupported(): + colorTagApp, colorTagVer = "{*}{#160}", "{#124}" + default: + colorTagApp, colorTagVer = "{r*}", "{r}" + } +} + +// configureUI configures user interface +func configureUI() { + if options.GetB(OPT_NO_COLOR) { + fmtc.DisableColors = true + } +} + +// setupErrorLog setup error log +func setupErrorLog() { + err := log.Set(options.GetS(OPT_ERROR_LOG), 0644) + + if err != nil { + printErrorAndExit(err.Error()) + } +} + +// startMeasurementProcess start measurement process +func startMeasurementProcess() { + prettyOutput := !options.Has(OPT_OUTPUT) + interval := time.Duration(options.GetI(OPT_INTERVAL)) * time.Second + + host = options.GetS(OPT_HOST) + ":" + options.GetS(OPT_PORT) + timeout = time.Second * time.Duration(options.GetI(OPT_TIMEOUT)) + + if !options.GetB(OPT_CONNECT) { + connectToRedis(false) + } + + if options.Has(OPT_OUTPUT) { + createOutputWriter() + } + + measureLatency(interval, prettyOutput) +} + +// createOutputWriter create and open file for writing data +func createOutputWriter() { + fd, err := os.OpenFile(options.GetS(OPT_OUTPUT), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) + + if err != nil { + printErrorAndExit(err.Error()) + } + + outputWriter = bufio.NewWriter(fd) + + go flushOutput(250 * time.Millisecond) +} + +// connectToRedis connect to redis instance +func connectToRedis(reconnect bool) error { + var err error + + conn, err = net.DialTimeout("tcp", host, timeout) + + if err != nil { + if !reconnect { + printErrorAndExit("Can't connect to Redis on %s", host) + } else { + return err + } + } + + if options.GetS(OPT_AUTH) != "" { + _, err = conn.Write([]byte("AUTH " + options.GetS(OPT_AUTH) + "\r\n")) + + if err != nil { + if !reconnect { + printErrorAndExit("Can't send AUTH command") + } else { + return err + } + } + } + + return nil +} + +// measureLatency measure latency +func measureLatency(interval time.Duration, prettyOutput bool) { + var ( + measurements stats.Data + count, pointer int + t *table.Table + sampleRate int + errors int + buf *bufio.Reader + ) + + if prettyOutput { + t = createOutputTable() + } + + connect := options.GetB(OPT_CONNECT) + + if connect { + sampleRate = CONNECT_SAMPLE_RATE + } else { + sampleRate = LATENCY_SAMPLE_RATE + buf = bufio.NewReader(conn) + } + + measurements = createMeasurementsSlice(sampleRate) + + last := alignTime() + + for { + time.Sleep(time.Duration(sampleRate) * time.Millisecond) + + start := time.Now() + + if connect { + errors += makeConnection() + } else { + errors += execCommand(buf) + } + + dur := uint64(time.Since(start) / time.Microsecond) + measurements[pointer] = dur + + if time.Since(last) >= interval { + last = start + + printMeasurements(t, errors, measurements[:pointer], prettyOutput) + + if prettyOutput { + count++ + + if count == 10 { + t.Separator() + count = 0 + } + } + + errors = 0 + pointer = 0 + } else { + pointer++ + } + } +} + +// execCommand execute command and read output +func execCommand(buf *bufio.Reader) int { + if conn == nil { + if connectToRedis(true) != nil { + return 1 + } + } + + _, err := conn.Write(pingCommand) + + if err != nil { + if options.Has(OPT_ERROR_LOG) && !errorLogged { + log.Error(err.Error()) + errorLogged = true + } + + conn = nil + + return 1 + } + + _, err = buf.ReadString('\n') + + if err != nil && err != io.EOF { + if options.Has(OPT_ERROR_LOG) && !errorLogged { + log.Error(err.Error()) + errorLogged = true + } + + conn = nil + + return 1 + } + + errorLogged = false + + return 0 +} + +// makeConnection create and close connection to Redis +func makeConnection() int { + var err error + + conn, err = net.DialTimeout("tcp", host, timeout) + + if err != nil { + if options.Has(OPT_ERROR_LOG) && !errorLogged { + log.Error(err.Error()) + errorLogged = true + } + + return 1 + } + + conn.Close() + + errorLogged = false + + return 0 +} + +// printMeasurements calculate and print measurements +func printMeasurements(t *table.Table, errors int, measurements stats.Data, prettyOutput bool) { + measurements.Sort() + + min := stats.Min(measurements) + max := stats.Max(measurements) + men := stats.Mean(measurements) + sdv := stats.StandardDeviation(measurements) + p95 := stats.Percentile(measurements, 95.0) + p99 := stats.Percentile(measurements, 99.0) + + if prettyOutput { + t.Print( + timeutil.Format(time.Now(), "%H:%M:%S{s-}.%K{!}"), + fmtutil.PrettyNum(len(measurements)), + fmtutil.PrettyNum(errors), + formatNumber(min), formatNumber(max), + formatNumber(men), formatNumber(sdv), + formatNumber(p95), formatNumber(p99), + ) + } else { + if options.GetB(OPT_TIMESTAMPS) { + outputWriter.WriteString( + fmt.Sprintf( + "%d;%d;%d;%.03f;%.03f;%.03f;%.03f;%.03f;%.03f;\n", + time.Now().Unix(), len(measurements), errors, + usToMs(min), usToMs(max), usToMs(men), + usToMs(sdv), usToMs(p95), usToMs(p99), + ), + ) + } else { + outputWriter.WriteString( + fmt.Sprintf( + "%s;%d;%d;%.03f;%.03f;%.03f;%.03f;%.03f;%.03f;\n", + timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S.%K"), + len(measurements), errors, + usToMs(min), usToMs(max), usToMs(men), + usToMs(sdv), usToMs(p95), usToMs(p99), + ), + ) + } + + } +} + +// formatNumber format floating number +func formatNumber(value uint64) string { + if value == 0 { + return "{s-}------{!}" + } + + fv := float64(value) / 1000.0 + + switch { + case fv > 1000.0: + fv = mathutil.Round(fv, 0) + case fv > 10: + fv = mathutil.Round(fv, 1) + case fv > 1: + fv = mathutil.Round(fv, 2) + } + + switch { + case fv >= 100.0: + return "{r}" + fmtutil.PrettyNum(fv) + "{!}" + case fv >= 10.0: + return "{y}" + fmtutil.PrettyNum(fv) + "{!}" + default: + return strings.Replace(fmtutil.PrettyNum(fv), ".", "{s}.", -1) + "{!}" + } +} + +// usToMs convert us in uint64 to ms in float64 +func usToMs(us uint64) float64 { + return float64(us) / 1000.0 +} + +// createOutputTable create and configure output table struct +func createOutputTable() *table.Table { + t := table.NewTable( + "TIME", "SAMPLES", "ERRORS", "MIN", "MAX", + "MEAN", "STDDEV", "PERC 95", "PERC 99", + ) + + t.SetSizes(12, 8, 8, 8, 10, 8, 8, 8) + + t.SetAlignments( + table.ALIGN_RIGHT, table.ALIGN_RIGHT, table.ALIGN_RIGHT, + table.ALIGN_RIGHT, table.ALIGN_RIGHT, table.ALIGN_RIGHT, + table.ALIGN_RIGHT, table.ALIGN_RIGHT, + ) + + return t +} + +// alignTime block main thread until nearest interval start point +func alignTime() time.Time { + interval := options.GetI(OPT_INTERVAL) + + for { + now := time.Now() + + if interval >= 60 { + if now.Second() == 0 { + return now + } + } else { + if now.Second()%interval == 0 { + return now + } + } + + time.Sleep(10 * time.Millisecond) + } +} + +// createMeasurementsSlice create float64 slice for measurements +func createMeasurementsSlice(sampleRate int) []uint64 { + size := (options.GetI(OPT_INTERVAL) * 1000) / sampleRate + return make(stats.Data, size) +} + +// flushOutput is function for flushing output +func flushOutput(interval time.Duration) { + for range time.NewTicker(interval).C { + outputWriter.Flush() + } +} + +// printErrorAndExit print error message and exit from utility +func printErrorAndExit(f string, a ...interface{}) { + printError(f, a...) + shutdown(1) +} + +// printError prints error message to console +func printError(f string, a ...interface{}) { + fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) +} + +// shutdown close connection to Redis and exit from utility +func shutdown(code int) { + if conn != nil { + conn.Close() + } + + if outputWriter != nil { + outputWriter.Flush() + } + + os.Exit(code) +} + +// ////////////////////////////////////////////////////////////////////////////////// // + +// getRedisVersionInfo returns info about Redis version +func getRedisVersionInfo() support.App { + cmd := exec.Command("redis-server", "--version") + output, err := cmd.Output() + + if err != nil { + return support.App{"Redis", ""} + } + + ver := strutil.ReadField(string(output), 2, false, ' ') + ver = strings.TrimLeft(ver, "v=") + + return support.App{"Redis", ver} +} + +// printCompletion prints completion for given shell +func printCompletion() int { + info := genUsage() + + switch options.GetS(OPT_COMPLETION) { + case "bash": + fmt.Print(bash.Generate(info, "redis-latency-monitor")) + case "fish": + fmt.Print(fish.Generate(info, "redis-latency-monitor")) + case "zsh": + fmt.Print(zsh.Generate(info, optMap, "redis-latency-monitor")) + default: + return 1 + } + + return 0 +} + +// printMan prints man page +func printMan() { + fmt.Println( + man.Generate( + genUsage(), + genAbout(""), + ), + ) +} + +// genUsage generates usage info +func genUsage() *usage.Info { + info := usage.NewInfo() + + info.AppNameColorTag = colorTagApp + + info.AddSpoiler("Utility shows PING command latency or connection latency in milliseconds (one thousandth of a second).") + + info.AddOption(OPT_HOST, "Server hostname {s-}(127.0.0.1 by default){!}", "ip/host") + info.AddOption(OPT_PORT, "Server port {s-}(6379 by default){!}", "port") + info.AddOption(OPT_CONNECT, "Measure connection latency instead of command latency") + info.AddOption(OPT_AUTH, "Password to use when connecting to the server", "password") + info.AddOption(OPT_TIMEOUT, "Connection timeout in seconds {s-}(3 by default){!}", "1-300") + info.AddOption(OPT_INTERVAL, "Interval in seconds {s-}(60 by default){!}", "1-3600") + info.AddOption(OPT_TIMESTAMPS, "Use unix timestamps in output") + info.AddOption(OPT_OUTPUT, "Path to output CSV file", "file") + info.AddOption(OPT_ERROR_LOG, "Path to log with error messages", "file") + info.AddOption(OPT_NO_COLOR, "Disable colors in output") + info.AddOption(OPT_HELP, "Show this help message") + info.AddOption(OPT_VER, "Show version") + + info.AddExample( + "-h 192.168.0.123 -p 6821 -t 15", + "Start monitoring instance on 192.168.0.123:6821 with 15 second timeout", + ) + + info.AddExample( + "-c -i 15 -o latency.csv", + "Start connection latency monitoring with 15 second interval and save result to CSV file", + ) + + return info +} + +// genAbout generates info about version +func genAbout(gitRev string) *usage.About { + about := &usage.About{ + App: APP, + Version: VER, + Desc: DESC, + Year: 2006, + Owner: "ESSENTIAL KAOS", + License: "Apache License, Version 2.0 ", + + AppNameColorTag: colorTagApp, + VersionColorTag: colorTagVer, + DescSeparator: "{s}—{!}", + } + + if gitRev != "" { + about.Build = "git:" + gitRev + about.UpdateChecker = usage.UpdateChecker{ + "essentialkaos/redis-latency-monitor", + update.GitHubChecker, + } + } + + return about +} diff --git a/common/redis-latency-monitor.spec b/common/redis-latency-monitor.spec index 16211b7..c83edbd 100644 --- a/common/redis-latency-monitor.spec +++ b/common/redis-latency-monitor.spec @@ -1,16 +1,12 @@ ################################################################################ -# rpmbuilder:relative-pack true - -################################################################################ - %define debug_package %{nil} ################################################################################ Summary: Tiny Redis client for latency measurement Name: redis-latency-monitor -Version: 3.2.1 +Version: 3.2.2 Release: 0%{?dist} Group: Applications/System License: Apache License, Version 2.0 @@ -20,7 +16,7 @@ Source0: https://source.kaos.st/%{name}/%{name}-%{version}.tar.bz2 BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n) -BuildRequires: golang >= 1.17 +BuildRequires: golang >= 1.21 Provides: %{name} = %{version}-%{release} @@ -36,16 +32,21 @@ or connection latency in milliseconds (one thousandth of a second). %setup -q %build -export GOPATH=$(pwd) -pushd src/github.com/essentialkaos/%{name} - go build -mod vendor -o $GOPATH/%{name} %{name}.go +if [[ ! -d "%{name}/vendor" ]] ; then + echo "This package requires vendored dependencies" + exit 1 +fi + +pushd %{name} + go build %{name}.go + cp LICENSE .. popd %install rm -rf %{buildroot} install -dm 755 %{buildroot}%{_bindir} -install -pm 755 %{name} %{buildroot}%{_bindir}/ +install -pm 755 %{name}/%{name} %{buildroot}%{_bindir}/ %clean rm -rf %{buildroot} @@ -88,6 +89,14 @@ fi ################################################################################ %changelog +* Thu Mar 28 2024 Anton Novojilov - 3.2.2-0 +- Improved support information gathering +- Code refactoring +- Dependencies update + +* Wed Nov 30 2022 Anton Novojilov - 3.2.1-1 +- Fixed build using sources from source.kaos.st + * Wed Mar 30 2022 Anton Novojilov - 3.2.1-0 - Package ek updated to the latest stable version - Removed pkg.re usage diff --git a/go.mod b/go.mod index 021d787..a138448 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,10 @@ module github.com/essentialkaos/redis-latency-monitor -go 1.17 +go 1.18 -require github.com/essentialkaos/ek/v12 v12.56.0 +require github.com/essentialkaos/ek/v12 v12.113.1 + +require ( + github.com/essentialkaos/depsy v1.1.0 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/go.sum b/go.sum index a8109f7..efceef3 100644 --- a/go.sum +++ b/go.sum @@ -1,29 +1,10 @@ -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/essentialkaos/check v1.3.0 h1:ria+8o22RCLdt2D/1SHQsEH5Mmy5S+iWHaGHrrbPUc0= -github.com/essentialkaos/check v1.3.0/go.mod h1:PhxzfJWlf5L/skuyhzBLIvjMB5Xu9TIyDIsqpY5MvB8= -github.com/essentialkaos/ek/v12 v12.56.0 h1:Zh6KjIjraQd8/n1gJsL6HTJXozcH9xqmvouSn2g7Nu4= -github.com/essentialkaos/ek/v12 v12.56.0/go.mod h1:G8ghiSKh8ToJQCdB2bAhE3CnI6dn9nTJdWH3bQIVr1U= -github.com/essentialkaos/go-linenoise/v3 v3.4.0/go.mod h1:t1kNLY2bSMQCy1JXOefD2BDLs/TTPMtTv3DFNV5uDSI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/essentialkaos/check v1.4.0 h1:kWdFxu9odCxUqo1NNFNJmguGrDHgwi3A8daXX1nkuKk= +github.com/essentialkaos/depsy v1.1.0 h1:U6dp687UkQwXlZU17Hg2KMxbp3nfZAoZ8duaeUFYvJI= +github.com/essentialkaos/depsy v1.1.0/go.mod h1:kpiTAV17dyByVnrbNaMcZt2jRwvuXClUYOzpyJQwtG8= +github.com/essentialkaos/ek/v12 v12.113.1 h1:3opV9dwRpIQq1fqg5mkaSEt6ogECL4VLzrH/829qeYg= +github.com/essentialkaos/ek/v12 v12.113.1/go.mod h1:SslW97Se34YQKc08Ume2V/8h/HPTgLS1+Iok64cNF/U= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg= -github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= -golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI= -golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/redis-latency-monitor.go b/redis-latency-monitor.go index 473eb0c..41d60d0 100644 --- a/redis-latency-monitor.go +++ b/redis-latency-monitor.go @@ -2,556 +2,27 @@ package main // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2022 ESSENTIAL KAOS // +// Copyright (c) 2024 ESSENTIAL KAOS // // Apache License, Version 2.0 // // // // ////////////////////////////////////////////////////////////////////////////////// // import ( - "bufio" - "fmt" - "io" - "net" - "os" - "runtime" - "strings" - "time" + _ "embed" - "github.com/essentialkaos/ek/v12/fmtc" - "github.com/essentialkaos/ek/v12/fmtutil" - "github.com/essentialkaos/ek/v12/fmtutil/table" - "github.com/essentialkaos/ek/v12/log" - "github.com/essentialkaos/ek/v12/mathutil" - "github.com/essentialkaos/ek/v12/options" - "github.com/essentialkaos/ek/v12/timeutil" - "github.com/essentialkaos/ek/v12/usage" - "github.com/essentialkaos/ek/v12/usage/completion/bash" - "github.com/essentialkaos/ek/v12/usage/completion/fish" - "github.com/essentialkaos/ek/v12/usage/completion/zsh" - - "github.com/essentialkaos/redis-latency-monitor/stats" + CLI "github.com/essentialkaos/redis-latency-monitor/cli" ) // ////////////////////////////////////////////////////////////////////////////////// // -// App info -const ( - APP = "Redis Latency Monitor" - VER = "3.2.1" - DESC = "Tiny Redis client for latency measurement" -) +//go:embed go.mod +var gomod []byte -// Main constatnts -const ( - LATENCY_SAMPLE_RATE int = 10 - CONNECT_SAMPLE_RATE = 100 -) - -// Options -const ( - OPT_HOST = "h:host" - OPT_PORT = "p:port" - OPT_AUTH = "a:password" - OPT_TIMEOUT = "t:timeout" - OPT_INTERVAL = "i:interval" - OPT_CONNECT = "c:connect" - OPT_TIMESTAMPS = "T:timestamps" - OPT_OUTPUT = "o:output" - OPT_ERROR_LOG = "e:error-log" - OPT_NO_COLOR = "nc:no-color" - OPT_HELP = "help" - OPT_VER = "v:version" - - OPT_COMPLETION = "completion" -) +// gitrev is short hash of the latest git commit +var gitrev string // ////////////////////////////////////////////////////////////////////////////////// // -// optMap is map with options -var optMap = options.Map{ - OPT_HOST: {Type: options.MIXED, Value: "127.0.0.1"}, - OPT_PORT: {Value: "6379"}, - OPT_CONNECT: {Type: options.BOOL}, - OPT_TIMEOUT: {Type: options.INT, Value: 3, Min: 1, Max: 300}, - OPT_AUTH: {}, - OPT_INTERVAL: {Type: options.INT, Value: 60, Min: 1, Max: 3600}, - OPT_TIMESTAMPS: {Type: options.BOOL}, - OPT_OUTPUT: {}, - OPT_ERROR_LOG: {}, - OPT_NO_COLOR: {Type: options.BOOL}, - OPT_HELP: {Type: options.BOOL, Alias: "u:usage"}, - OPT_VER: {Type: options.BOOL, Alias: "ver"}, - - OPT_COMPLETION: {}, -} - -// pingCommand is PING command data -var pingCommand = []byte("PING\r\n") - -var ( - conn net.Conn - host string - timeout time.Duration - outputWriter *bufio.Writer - errorLogged bool -) - -// ////////////////////////////////////////////////////////////////////////////////// // - -// main is main function func main() { - runtime.GOMAXPROCS(4) - - _, errs := options.Parse(optMap) - - if len(errs) != 0 { - for _, err := range errs { - printError(err.Error()) - } - - os.Exit(1) - } - - if options.Has(OPT_COMPLETION) { - genCompletion() - } - - if options.GetB(OPT_NO_COLOR) { - fmtc.DisableColors = true - } - - if options.GetB(OPT_VER) { - showAbout() - return - } - - if options.GetB(OPT_HELP) || options.GetS(OPT_HOST) == "true" { - showUsage() - return - } - - if options.Has(OPT_ERROR_LOG) { - setupErrorLog() - } - - startMeasurementProcess() -} - -// setupErrorLog setup error log -func setupErrorLog() { - err := log.Set(options.GetS(OPT_ERROR_LOG), 0644) - - if err != nil { - printErrorAndExit(err.Error()) - } -} - -// startMeasurementProcess start measurement process -func startMeasurementProcess() { - prettyOutput := !options.Has(OPT_OUTPUT) - interval := time.Duration(options.GetI(OPT_INTERVAL)) * time.Second - - host = options.GetS(OPT_HOST) + ":" + options.GetS(OPT_PORT) - timeout = time.Second * time.Duration(options.GetI(OPT_TIMEOUT)) - - if !options.GetB(OPT_CONNECT) { - connectToRedis(false) - } - - if options.Has(OPT_OUTPUT) { - createOutputWriter() - } - - measureLatency(interval, prettyOutput) -} - -// createOutputWriter create and open file for writing data -func createOutputWriter() { - fd, err := os.OpenFile(options.GetS(OPT_OUTPUT), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644) - - if err != nil { - printErrorAndExit(err.Error()) - } - - outputWriter = bufio.NewWriter(fd) - - go flushOutput(250 * time.Millisecond) -} - -// connectToRedis connect to redis instance -func connectToRedis(reconnect bool) error { - var err error - - conn, err = net.DialTimeout("tcp", host, timeout) - - if err != nil { - if !reconnect { - printErrorAndExit("Can't connect to Redis on %s", host) - } else { - return err - } - } - - if options.GetS(OPT_AUTH) != "" { - _, err = conn.Write([]byte("AUTH " + options.GetS(OPT_AUTH) + "\r\n")) - - if err != nil { - if !reconnect { - printErrorAndExit("Can't send AUTH command") - } else { - return err - } - } - } - - return nil -} - -// measureLatency measure latency -func measureLatency(interval time.Duration, prettyOutput bool) { - var ( - measurements stats.Data - count, pointer int - t *table.Table - sampleRate int - errors int - buf *bufio.Reader - ) - - if prettyOutput { - t = createOutputTable() - } - - connect := options.GetB(OPT_CONNECT) - - if connect { - sampleRate = CONNECT_SAMPLE_RATE - } else { - sampleRate = LATENCY_SAMPLE_RATE - buf = bufio.NewReader(conn) - } - - measurements = createMeasurementsSlice(sampleRate) - - last := alignTime() - - for { - time.Sleep(time.Duration(sampleRate) * time.Millisecond) - - start := time.Now() - - if connect { - errors += makeConnection() - } else { - errors += execCommand(buf) - } - - dur := uint64(time.Since(start) / time.Microsecond) - measurements[pointer] = dur - - if time.Since(last) >= interval { - last = start - - printMeasurements(t, errors, measurements[:pointer], prettyOutput) - - if prettyOutput { - count++ - - if count == 10 { - t.Separator() - count = 0 - } - } - - errors = 0 - pointer = 0 - } else { - pointer++ - } - } -} - -// execCommand execute command and read output -func execCommand(buf *bufio.Reader) int { - if conn == nil { - if connectToRedis(true) != nil { - return 1 - } - } - - _, err := conn.Write(pingCommand) - - if err != nil { - if options.Has(OPT_ERROR_LOG) && !errorLogged { - log.Error(err.Error()) - errorLogged = true - } - - conn = nil - - return 1 - } - - _, err = buf.ReadString('\n') - - if err != nil && err != io.EOF { - if options.Has(OPT_ERROR_LOG) && !errorLogged { - log.Error(err.Error()) - errorLogged = true - } - - conn = nil - - return 1 - } - - errorLogged = false - - return 0 -} - -// makeConnection create and close connection to Redis -func makeConnection() int { - var err error - - conn, err = net.DialTimeout("tcp", host, timeout) - - if err != nil { - if options.Has(OPT_ERROR_LOG) && !errorLogged { - log.Error(err.Error()) - errorLogged = true - } - - return 1 - } - - conn.Close() - - errorLogged = false - - return 0 -} - -// printMeasurements calculate and print measurements -func printMeasurements(t *table.Table, errors int, measurements stats.Data, prettyOutput bool) { - measurements.Sort() - - min := stats.Min(measurements) - max := stats.Max(measurements) - men := stats.Mean(measurements) - sdv := stats.StandardDeviation(measurements) - p95 := stats.Percentile(measurements, 95.0) - p99 := stats.Percentile(measurements, 99.0) - - if prettyOutput { - t.Print( - timeutil.Format(time.Now(), "%H:%M:%S{s-}.%K{!}"), - fmtutil.PrettyNum(len(measurements)), - fmtutil.PrettyNum(errors), - formatNumber(min), formatNumber(max), - formatNumber(men), formatNumber(sdv), - formatNumber(p95), formatNumber(p99), - ) - } else { - if options.GetB(OPT_TIMESTAMPS) { - outputWriter.WriteString( - fmt.Sprintf( - "%d;%d;%d;%.03f;%.03f;%.03f;%.03f;%.03f;%.03f;\n", - time.Now().Unix(), len(measurements), errors, - usToMs(min), usToMs(max), usToMs(men), - usToMs(sdv), usToMs(p95), usToMs(p99), - ), - ) - } else { - outputWriter.WriteString( - fmt.Sprintf( - "%s;%d;%d;%.03f;%.03f;%.03f;%.03f;%.03f;%.03f;\n", - timeutil.Format(time.Now(), "%Y/%m/%d %H:%M:%S.%K"), - len(measurements), errors, - usToMs(min), usToMs(max), usToMs(men), - usToMs(sdv), usToMs(p95), usToMs(p99), - ), - ) - } - - } -} - -// formatNumber format floating number -func formatNumber(value uint64) string { - if value == 0 { - return "{s-}------{!}" - } - - fv := float64(value) / 1000.0 - - switch { - case fv > 1000.0: - fv = mathutil.Round(fv, 0) - case fv > 10: - fv = mathutil.Round(fv, 1) - case fv > 1: - fv = mathutil.Round(fv, 2) - } - - switch { - case fv >= 100.0: - return "{r}" + fmtutil.PrettyNum(fv) + "{!}" - case fv >= 10.0: - return "{y}" + fmtutil.PrettyNum(fv) + "{!}" - default: - return strings.Replace(fmtutil.PrettyNum(fv), ".", "{s}.", -1) + "{!}" - } -} - -// usToMs convert us in uint64 to ms in float64 -func usToMs(us uint64) float64 { - return float64(us) / 1000.0 -} - -// createOutputTable create and configure output table struct -func createOutputTable() *table.Table { - t := table.NewTable( - "TIME", "SAMPLES", "ERRORS", "MIN", "MAX", - "MEAN", "STDDEV", "PERC 95", "PERC 99", - ) - - t.SetSizes(12, 8, 8, 8, 10, 8, 8, 8) - - t.SetAlignments( - table.ALIGN_RIGHT, table.ALIGN_RIGHT, table.ALIGN_RIGHT, - table.ALIGN_RIGHT, table.ALIGN_RIGHT, table.ALIGN_RIGHT, - table.ALIGN_RIGHT, table.ALIGN_RIGHT, - ) - - return t -} - -// alignTime block main thread until nearest interval start point -func alignTime() time.Time { - interval := options.GetI(OPT_INTERVAL) - - for { - now := time.Now() - - if interval >= 60 { - if now.Second() == 0 { - return now - } - } else { - if now.Second()%interval == 0 { - return now - } - } - - time.Sleep(10 * time.Millisecond) - } -} - -// createMeasurementsSlice create float64 slice for measurements -func createMeasurementsSlice(sampleRate int) []uint64 { - size := (options.GetI(OPT_INTERVAL) * 1000) / sampleRate - return make(stats.Data, size) -} - -// flushOutput is function for flushing output -func flushOutput(interval time.Duration) { - for range time.NewTicker(interval).C { - outputWriter.Flush() - } -} - -// printErrorAndExit print error message and exit from utility -func printErrorAndExit(f string, a ...interface{}) { - printError(f, a...) - shutdown(1) -} - -// printError prints error message to console -func printError(f string, a ...interface{}) { - fmtc.Fprintf(os.Stderr, "{r}"+f+"{!}\n", a...) -} - -// shutdown close connection to Redis and exit from utility -func shutdown(code int) { - if conn != nil { - conn.Close() - } - - if outputWriter != nil { - outputWriter.Flush() - } - - os.Exit(code) -} - -// ////////////////////////////////////////////////////////////////////////////////// // - -// showUsage print usage info -func showUsage() { - genUsage().Render() -} - -// genUsage generates usage info -func genUsage() *usage.Info { - info := usage.NewInfo("") - - info.AddSpoiler("Utility shows PING command latency or connection latency in milliseconds (one thousandth of a second).") - - info.AddOption(OPT_HOST, "Server hostname {s-}(127.0.0.1 by default){!}", "ip/host") - info.AddOption(OPT_PORT, "Server port {s-}(6379 by default){!}", "port") - info.AddOption(OPT_CONNECT, "Measure connection latency instead of command latency") - info.AddOption(OPT_AUTH, "Password to use when connecting to the server", "password") - info.AddOption(OPT_TIMEOUT, "Connection timeout in seconds {s-}(3 by default){!}", "1-300") - info.AddOption(OPT_INTERVAL, "Interval in seconds {s-}(60 by default){!}", "1-3600") - info.AddOption(OPT_TIMESTAMPS, "Use unix timestamps in output") - info.AddOption(OPT_OUTPUT, "Path to output CSV file", "file") - info.AddOption(OPT_ERROR_LOG, "Path to log with error messages", "file") - info.AddOption(OPT_NO_COLOR, "Disable colors in output") - info.AddOption(OPT_HELP, "Show this help message") - info.AddOption(OPT_VER, "Show version") - - info.AddExample( - "-h 192.168.0.123 -p 6821 -t 15", - "Start monitoring instance on 192.168.0.123:6821 with 15 second timeout", - ) - - info.AddExample( - "-c -i 15 -o latency.csv", - "Start connection latency monitoring with 15 second interval and save result to CSV file", - ) - - return info -} - -// genCompletion generates completion for different shells -func genCompletion() { - info := genUsage() - - switch options.GetS(OPT_COMPLETION) { - case "bash": - fmt.Printf(bash.Generate(info, "redis-latency-monitor")) - case "fish": - fmt.Printf(fish.Generate(info, "redis-latency-monitor")) - case "zsh": - fmt.Printf(zsh.Generate(info, optMap, "redis-latency-monitor")) - default: - os.Exit(1) - } - - os.Exit(0) -} - -// showAbout print info about version -func showAbout() { - about := &usage.About{ - App: APP, - Version: VER, - Desc: DESC, - Year: 2006, - Owner: "ESSENTIAL KAOS", - License: "Apache License, Version 2.0 ", - } - - about.Render() + CLI.Run(gitrev, gomod) } diff --git a/stats/stats.go b/stats/stats.go index 6e25b2d..f4d091a 100644 --- a/stats/stats.go +++ b/stats/stats.go @@ -2,7 +2,7 @@ package stats // ////////////////////////////////////////////////////////////////////////////////// // // // -// Copyright (c) 2022 ESSENTIAL KAOS // +// Copyright (c) 2024 ESSENTIAL KAOS // // Apache License, Version 2.0 // // // // ////////////////////////////////////////////////////////////////////////////////// //