diff --git a/Makefile b/Makefile index ff970f1776..c08b0c5f85 100644 --- a/Makefile +++ b/Makefile @@ -140,4 +140,5 @@ test: go test ./pkg/query-service/app/metrics/... go test ./pkg/query-service/cache/... go test ./pkg/query-service/app/... - go test ./pkg/query-service/converter/... \ No newline at end of file + go test ./pkg/query-service/converter/... + go test ./pkg/query-service/formatter/... diff --git a/go.mod b/go.mod index e18f39b14f..ef31d473ed 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/ClickHouse/clickhouse-go/v2 v2.5.1 github.com/SigNoz/govaluate v0.0.0-20220522085550-d19c08c206cb github.com/coreos/go-oidc/v3 v3.4.0 + github.com/dustin/go-humanize v1.0.0 github.com/go-kit/log v0.2.1 github.com/go-redis/redis/v8 v8.11.5 github.com/go-redis/redismock/v8 v8.11.5 @@ -30,6 +31,7 @@ require ( github.com/rs/cors v1.8.2 github.com/russellhaering/gosaml2 v0.8.0 github.com/russellhaering/goxmldsig v1.2.0 + github.com/samber/lo v1.38.1 github.com/sethvargo/go-password v0.2.0 github.com/smartystreets/goconvey v1.6.4 github.com/soheilhy/cmux v0.1.5 diff --git a/go.sum b/go.sum index 9ae992d914..d43d43782b 100644 --- a/go.sum +++ b/go.sum @@ -163,6 +163,7 @@ github.com/docker/docker v20.10.22+incompatible h1:6jX4yB+NtcbldT90k7vBSaWJDB3i+ github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11 h1:IPrmumsT9t5BS7XcPhgsCTlkWbYg80SEXUzDpReaU6Y= github.com/docker/go-connections v0.4.1-0.20210727194412-58542c764a11/go.mod h1:a6bNUGTbQBsY6VRHTr4h/rkOXjl244DyRD0tx3fgq4Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= +github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= @@ -605,6 +606,8 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM= +github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.10 h1:wsfMs0iv+MJiViM37qh5VEKISi3/ZUq2nNKNdqmumAs= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys= diff --git a/pkg/query-service/formatter/bool.go b/pkg/query-service/formatter/bool.go new file mode 100644 index 0000000000..7080a2bb04 --- /dev/null +++ b/pkg/query-service/formatter/bool.go @@ -0,0 +1,45 @@ +package formatter + +import "fmt" + +type boolFormatter struct{} + +func NewBoolFormatter() Formatter { + return &boolFormatter{} +} + +func toBool(value float64) string { + if value == 0 { + return "false" + } + return "true" +} + +func toBoolYesNo(value float64) string { + if value == 0 { + return "no" + } + return "yes" +} + +func toBoolOnOff(value float64) string { + if value == 0 { + return "off" + } + return "on" +} + +func (f *boolFormatter) Format(value float64, unit string) string { + + switch unit { + case "bool": + return toBool(value) + case "bool_yes_no": + return toBoolYesNo(value) + case "bool_on_off": + return toBoolOnOff(value) + } + // When unit is not matched, return the value as it is. + return fmt.Sprintf("%v", value) + +} diff --git a/pkg/query-service/formatter/data.go b/pkg/query-service/formatter/data.go new file mode 100644 index 0000000000..ecf7e6e64d --- /dev/null +++ b/pkg/query-service/formatter/data.go @@ -0,0 +1,50 @@ +package formatter + +import ( + "fmt" + + "github.com/dustin/go-humanize" + "go.signoz.io/signoz/pkg/query-service/converter" +) + +type dataFormatter struct { +} + +func NewDataFormatter() Formatter { + return &dataFormatter{} +} + +func (f *dataFormatter) Format(value float64, unit string) string { + switch unit { + case "bytes": + return humanize.IBytes(uint64(value)) + case "decbytes": + return humanize.Bytes(uint64(value)) + case "bits": + return humanize.IBytes(uint64(value * converter.Bit)) + case "decbits": + return humanize.Bytes(uint64(value * converter.Bit)) + case "kbytes": + return humanize.IBytes(uint64(value * converter.Kibibit)) + case "deckbytes": + return humanize.IBytes(uint64(value * converter.Kilobit)) + case "mbytes": + return humanize.IBytes(uint64(value * converter.Mebibit)) + case "decmbytes": + return humanize.Bytes(uint64(value * converter.Megabit)) + case "gbytes": + return humanize.IBytes(uint64(value * converter.Gibibit)) + case "decgbytes": + return humanize.Bytes(uint64(value * converter.Gigabit)) + case "tbytes": + return humanize.IBytes(uint64(value * converter.Tebibit)) + case "dectbytes": + return humanize.Bytes(uint64(value * converter.Terabit)) + case "pbytes": + return humanize.IBytes(uint64(value * converter.Pebibit)) + case "decpbytes": + return humanize.Bytes(uint64(value * converter.Petabit)) + } + // When unit is not matched, return the value as it is. + return fmt.Sprintf("%v", value) +} diff --git a/pkg/query-service/formatter/data_rate.go b/pkg/query-service/formatter/data_rate.go new file mode 100644 index 0000000000..ef1053fc93 --- /dev/null +++ b/pkg/query-service/formatter/data_rate.go @@ -0,0 +1,70 @@ +package formatter + +import ( + "fmt" + + "github.com/dustin/go-humanize" + "go.signoz.io/signoz/pkg/query-service/converter" +) + +type dataRateFormatter struct { +} + +func NewDataRateFormatter() Formatter { + return &dataRateFormatter{} +} + +func (f *dataRateFormatter) Format(value float64, unit string) string { + switch unit { + case "binBps": + return humanize.IBytes(uint64(value)) + "/s" + case "Bps": + return humanize.Bytes(uint64(value)) + "/s" + case "binbps": + return humanize.IBytes(uint64(value*converter.BitPerSecond)) + "/s" + case "bps": + return humanize.Bytes(uint64(value*converter.BitPerSecond)) + "/s" + case "KiBs": + return humanize.IBytes(uint64(value*converter.KibibitPerSecond)) + "/s" + case "Kibits": + return humanize.IBytes(uint64(value*converter.KibibytePerSecond)) + "/s" + case "KBs": + return humanize.IBytes(uint64(value*converter.KilobitPerSecond)) + "/s" + case "Kbits": + return humanize.IBytes(uint64(value*converter.KilobytePerSecond)) + "/s" + case "MiBs": + return humanize.IBytes(uint64(value*converter.MebibitPerSecond)) + "/s" + case "Mibits": + return humanize.IBytes(uint64(value*converter.MebibytePerSecond)) + "/s" + case "MBs": + return humanize.IBytes(uint64(value*converter.MegabitPerSecond)) + "/s" + case "Mbits": + return humanize.IBytes(uint64(value*converter.MegabytePerSecond)) + "/s" + case "GiBs": + return humanize.IBytes(uint64(value*converter.GibibitPerSecond)) + "/s" + case "Gibits": + return humanize.IBytes(uint64(value*converter.GibibytePerSecond)) + "/s" + case "GBs": + return humanize.IBytes(uint64(value*converter.GigabitPerSecond)) + "/s" + case "Gbits": + return humanize.IBytes(uint64(value*converter.GigabytePerSecond)) + "/s" + case "TiBs": + return humanize.IBytes(uint64(value*converter.TebibitPerSecond)) + "/s" + case "Tibits": + return humanize.IBytes(uint64(value*converter.TebibytePerSecond)) + "/s" + case "TBs": + return humanize.IBytes(uint64(value*converter.TerabitPerSecond)) + "/s" + case "Tbits": + return humanize.IBytes(uint64(value*converter.TerabytePerSecond)) + "/s" + case "PiBs": + return humanize.IBytes(uint64(value*converter.PebibitPerSecond)) + "/s" + case "Pibits": + return humanize.IBytes(uint64(value*converter.PebibytePerSecond)) + "/s" + case "PBs": + return humanize.IBytes(uint64(value*converter.PetabitPerSecond)) + "/s" + case "Pbits": + return humanize.IBytes(uint64(value*converter.PetabytePerSecond)) + "/s" + } + // When unit is not matched, return the value as it is. + return fmt.Sprintf("%v", value) +} diff --git a/pkg/query-service/formatter/data_test.go b/pkg/query-service/formatter/data_test.go new file mode 100644 index 0000000000..b5da03cec5 --- /dev/null +++ b/pkg/query-service/formatter/data_test.go @@ -0,0 +1,23 @@ +package formatter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestData(t *testing.T) { + dataFormatter := NewDataFormatter() + + assert.Equal(t, "1 B", dataFormatter.Format(1, "bytes")) + assert.Equal(t, "1.0 KiB", dataFormatter.Format(1024, "bytes")) + assert.Equal(t, "2.3 GiB", dataFormatter.Format(2.3*1024, "mbytes")) + assert.Equal(t, "1.0 MiB", dataFormatter.Format(1024*1024, "bytes")) + assert.Equal(t, "69 TiB", dataFormatter.Format(69*1024*1024, "mbytes")) + assert.Equal(t, "102 KiB", dataFormatter.Format(102*1024, "bytes")) + assert.Equal(t, "240 MiB", dataFormatter.Format(240*1024, "kbytes")) + assert.Equal(t, "1.0 GiB", dataFormatter.Format(1024*1024, "kbytes")) + assert.Equal(t, "23 GiB", dataFormatter.Format(23*1024*1024, "kbytes")) + assert.Equal(t, "32 TiB", dataFormatter.Format(32*1024*1024*1024, "kbytes")) + assert.Equal(t, "24 MiB", dataFormatter.Format(24, "mbytes")) +} diff --git a/pkg/query-service/formatter/formatter.go b/pkg/query-service/formatter/formatter.go new file mode 100644 index 0000000000..5c700b53c4 --- /dev/null +++ b/pkg/query-service/formatter/formatter.go @@ -0,0 +1,34 @@ +package formatter + +type Formatter interface { + Format(value float64, unit string) string +} + +var ( + DurationFormatter = NewDurationFormatter() + BoolFormatter = NewBoolFormatter() + PercentFormatter = NewPercentFormatter() + NoneFormatter = NewNoneFormatter() + DataFormatter = NewDataFormatter() + DataRateFormatter = NewDataRateFormatter() + ThroughputFormatter = NewThroughputFormatter() +) + +func FromUnit(u string) Formatter { + switch u { + case "ns", "us", "ms", "s", "m", "h", "d": + return DurationFormatter + case "bytes", "decbytes", "bits", "decbits", "kbytes", "decKbytes", "mbytes", "decMbytes", "gbytes", "decGbytes", "tbytes", "decTbytes", "pbytes", "decPbytes": + return DataFormatter + case "binBps", "Bps", "binbps", "bps", "KiBs", "Kibits", "KBs", "Kbits", "MiBs", "Mibits", "MBs", "Mbits", "GiBs", "Gibits", "GBs", "Gbits", "TiBs", "Tibits", "TBs", "Tbits", "PiBs", "Pibits", "PBs", "Pbits": + return DataRateFormatter + case "percent", "percentunit": + return PercentFormatter + case "bool", "bool_yes_no", "bool_true_false", "bool_1_0": + return BoolFormatter + case "cps", "ops", "reqps", "rps", "wps", "iops", "cpm", "opm", "rpm", "wpm": + return ThroughputFormatter + default: + return NoneFormatter + } +} diff --git a/pkg/query-service/formatter/none.go b/pkg/query-service/formatter/none.go new file mode 100644 index 0000000000..8f2fbab6df --- /dev/null +++ b/pkg/query-service/formatter/none.go @@ -0,0 +1,13 @@ +package formatter + +import "fmt" + +type noneFormatter struct{} + +func NewNoneFormatter() Formatter { + return &noneFormatter{} +} + +func (f *noneFormatter) Format(value float64, unit string) string { + return fmt.Sprintf("%v", value) +} diff --git a/pkg/query-service/formatter/percent.go b/pkg/query-service/formatter/percent.go new file mode 100644 index 0000000000..699ff4894b --- /dev/null +++ b/pkg/query-service/formatter/percent.go @@ -0,0 +1,28 @@ +package formatter + +import "fmt" + +type percentFormatter struct{} + +func NewPercentFormatter() Formatter { + return &percentFormatter{} +} + +func toPercent(value float64, decimals DecimalCount) string { + return toFixed(value, decimals) + "%" +} + +func toPercentUnit(value float64, decimals DecimalCount) string { + return toFixed(value*100, decimals) + "%" +} + +func (f *percentFormatter) Format(value float64, unit string) string { + switch unit { + case "percent": + return toPercent(value, nil) + case "percentunit": + return toPercentUnit(value, nil) + } + // When unit is not matched, return the value as it is. + return fmt.Sprintf("%v", value) +} diff --git a/pkg/query-service/formatter/scale.go b/pkg/query-service/formatter/scale.go new file mode 100644 index 0000000000..8b29597765 --- /dev/null +++ b/pkg/query-service/formatter/scale.go @@ -0,0 +1,137 @@ +package formatter + +import ( + "fmt" + "math" + "strconv" + "strings" + + "github.com/samber/lo" +) + +type IntervalsInSecondsType map[Interval]int + +type Interval string + +const ( + Year Interval = "year" + Month Interval = "month" + Week Interval = "week" + Day Interval = "day" + Hour Interval = "hour" + Minute Interval = "minute" + Second Interval = "second" + Millisecond Interval = "millisecond" +) + +var Units = []Interval{ + Year, + Month, + Week, + Day, + Hour, + Minute, + Second, + Millisecond, +} + +var IntervalsInSeconds = IntervalsInSecondsType{ + Year: 31536000, + Month: 2592000, + Week: 604800, + Day: 86400, + Hour: 3600, + Minute: 60, + Second: 1, + Millisecond: 1, +} + +type DecimalCount *int + +func toFixed(value float64, decimals DecimalCount) string { + if value == 0 { + return strconv.FormatFloat(value, 'f', getDecimalsForValue(value), 64) + } + + if math.IsInf(value, 0) { + return strconv.FormatFloat(value, 'f', -1, 64) + } + + if decimals == nil { + count := getDecimalsForValue(value) + decimals = &count + } + + factor := math.Pow(10, math.Max(0, float64(*decimals))) + formatted := strconv.FormatFloat(math.Round(value*factor)/factor, 'f', -1, 64) + + if formatted == "NaN" { + return "" + } + + if formatted == "-0" { + formatted = "0" + } + + if value == 0 || strings.Contains(formatted, "e") { + return formatted + } + + decimalPos := strings.Index(formatted, ".") + precision := 0 + if decimalPos != -1 { + precision = len(formatted) - decimalPos - 1 + } + + if precision < *decimals { + return formatted + strings.Repeat("0", *decimals-precision) + } + + return formatted +} + +func toFixedScaled(value float64, decimals DecimalCount, scaleFormat string) string { + return toFixed(value, decimals) + scaleFormat +} + +func getDecimalsForValue(value float64) int { + absValue := math.Abs(value) + log10 := math.Floor(math.Log10(absValue)) + dec := int(-log10 + 1) + magn := math.Pow10(-dec) + norm := absValue / magn + + if norm > 2.25 { + dec++ + } + + if math.Mod(value, 1) == 0 { + dec = 0 + } + + return int(math.Max(float64(dec), 0)) +} + +type ValueFormatter func(value float64, decimals *int) string + +func logb(base, x float64) float64 { + return math.Log10(x) / math.Log10(base) +} + +func scaledUnits(factor float64, extArray []string, offset int) ValueFormatter { + return func(value float64, decimals *int) string { + if value == 0 || math.IsNaN(value) || math.IsInf(value, 0) { + return fmt.Sprintf("%f", value) + } + + siIndex := int(math.Floor(logb(factor, math.Abs(value)))) + if value < 0 { + siIndex = -siIndex + } + siIndex = lo.Clamp(siIndex+offset, 0, len(extArray)-1) + + suffix := extArray[siIndex] + + return toFixed(value/math.Pow(factor, float64(siIndex-offset)), decimals) + suffix + } +} diff --git a/pkg/query-service/formatter/throughput.go b/pkg/query-service/formatter/throughput.go new file mode 100644 index 0000000000..72f8c1a74a --- /dev/null +++ b/pkg/query-service/formatter/throughput.go @@ -0,0 +1,44 @@ +package formatter + +import "fmt" + +type throughputFormatter struct { +} + +func NewThroughputFormatter() Formatter { + return &throughputFormatter{} +} + +func simpleCountUnit(value float64, decimals *int, symbol string) string { + units := []string{"", "K", "M", "B", "T"} + scaler := scaledUnits(1000, units, 0) + + return scaler(value, decimals) + " " + symbol +} + +func (f *throughputFormatter) Format(value float64, unit string) string { + switch unit { + case "cps": + return simpleCountUnit(value, nil, "c/s") + case "ops": + return simpleCountUnit(value, nil, "op/s") + case "reqps": + return simpleCountUnit(value, nil, "req/s") + case "rps": + return simpleCountUnit(value, nil, "r/s") + case "wps": + return simpleCountUnit(value, nil, "w/s") + case "iops": + return simpleCountUnit(value, nil, "iops") + case "cpm": + return simpleCountUnit(value, nil, "c/m") + case "opm": + return simpleCountUnit(value, nil, "op/m") + case "rpm": + return simpleCountUnit(value, nil, "r/m") + case "wpm": + return simpleCountUnit(value, nil, "w/m") + } + // When unit is not matched, return the value as it is. + return fmt.Sprintf("%v", value) +} diff --git a/pkg/query-service/formatter/throughput_test.go b/pkg/query-service/formatter/throughput_test.go new file mode 100644 index 0000000000..acb7c37adf --- /dev/null +++ b/pkg/query-service/formatter/throughput_test.go @@ -0,0 +1,15 @@ +package formatter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestThroughput(t *testing.T) { + throughputFormatter := NewThroughputFormatter() + + assert.Equal(t, "10 req/s", throughputFormatter.Format(10, "reqps")) + assert.Equal(t, "1K req/s", throughputFormatter.Format(1000, "reqps")) + assert.Equal(t, "1M req/s", throughputFormatter.Format(1000000, "reqps")) +} diff --git a/pkg/query-service/formatter/time.go b/pkg/query-service/formatter/time.go new file mode 100644 index 0000000000..3d9b04d1b7 --- /dev/null +++ b/pkg/query-service/formatter/time.go @@ -0,0 +1,171 @@ +package formatter + +import ( + "fmt" + "math" +) + +type durationFormatter struct { +} + +func NewDurationFormatter() Formatter { + return &durationFormatter{} +} + +func (f *durationFormatter) Format(value float64, unit string) string { + switch unit { + case "ns": + return toNanoSeconds(value) + case "µs": + return toMicroSeconds(value) + case "ms": + return toMilliSeconds(value) + case "s": + return toSeconds(value) + case "m": + return toMinutes(value) + case "h": + return toHours(value) + case "d": + return toDays(value) + case "w": + return toWeeks(value) + } + // When unit is not matched, return the value as it is. + return fmt.Sprintf("%v", value) +} + +// toNanoSeconds returns a easy to read string representation of the given value in nanoseconds +func toNanoSeconds(value float64) string { + absValue := math.Abs(value) + + if absValue < 1000 { + return toFixed(value, nil) + " ns" + } else if absValue < 1000000 { // 2000 ns is better represented as 2 µs + return toFixedScaled(value/1000, nil, " µs") + } else if absValue < 1000000000 { // 2000000 ns is better represented as 2 ms + return toFixedScaled(value/1000000, nil, " ms") + } else if absValue < 60000000000 { + return toFixedScaled(value/1000000000, nil, " s") + } else if absValue < 3600000000000 { + return toFixedScaled(value/60000000000, nil, " min") + } else if absValue < 86400000000000 { + return toFixedScaled(value/3600000000000, nil, " hour") + } else { + return toFixedScaled(value/86400000000000, nil, " day") + } +} + +// toMicroSeconds returns a easy to read string representation of the given value in microseconds +func toMicroSeconds(value float64) string { + absValue := math.Abs(value) + if absValue < 1000 { + return toFixed(value, nil) + " µs" + } else if absValue < 1000000 { // 2000 µs is better represented as 2 ms + return toFixedScaled(value/1000, nil, " ms") + } else { + return toFixedScaled(value/1000000, nil, " s") + } +} + +// toMilliSeconds returns a easy to read string representation of the given value in milliseconds +func toMilliSeconds(value float64) string { + + absValue := math.Abs(value) + + if absValue < 1000 { + return toFixed(value, nil) + " ms" + } else if absValue < 60000 { + return toFixedScaled(value/1000, nil, " s") + } else if absValue < 3600000 { + return toFixedScaled(value/60000, nil, " min") + } else if absValue < 86400000 { // 172800000 ms is better represented as 2 day + return toFixedScaled(value/3600000, nil, " hour") + } else if absValue < 31536000000 { + return toFixedScaled(value/86400000, nil, " day") + } + + return toFixedScaled(value/31536000000, nil, " year") +} + +// toSeconds returns a easy to read string representation of the given value in seconds +func toSeconds(value float64) string { + absValue := math.Abs(value) + + if absValue < 0.000001 { + return toFixedScaled(value*1e9, nil, " ns") + } else if absValue < 0.001 { + return toFixedScaled(value*1e6, nil, " µs") + } else if absValue < 1 { + return toFixedScaled(value*1e3, nil, " ms") + } else if absValue < 60 { + return toFixed(value, nil) + " s" + } else if absValue < 3600 { + return toFixedScaled(value/60, nil, " min") + } else if absValue < 86400 { // 56000 s is better represented as 15.56 hour + return toFixedScaled(value/3600, nil, " hour") + } else if absValue < 604800 { + return toFixedScaled(value/86400, nil, " day") + } else if absValue < 31536000 { + return toFixedScaled(value/604800, nil, " week") + } + + return toFixedScaled(value/3.15569e7, nil, " year") +} + +// toMinutes returns a easy to read string representation of the given value in minutes +func toMinutes(value float64) string { + absValue := math.Abs(value) + + if absValue < 60 { + return toFixed(value, nil) + " min" + } else if absValue < 1440 { + return toFixedScaled(value/60, nil, " hour") + } else if absValue < 10080 { + return toFixedScaled(value/1440, nil, " day") + } else if absValue < 604800 { + return toFixedScaled(value/10080, nil, " week") + } else { + return toFixedScaled(value/5.25948e5, nil, " year") + } +} + +// toHours returns a easy to read string representation of the given value in hours +func toHours(value float64) string { + + absValue := math.Abs(value) + + if absValue < 24 { + return toFixed(value, nil) + " hour" + } else if absValue < 168 { + return toFixedScaled(value/24, nil, " day") + } else if absValue < 8760 { + return toFixedScaled(value/168, nil, " week") + } else { + return toFixedScaled(value/8760, nil, " year") + } +} + +// toDays returns a easy to read string representation of the given value in days +func toDays(value float64) string { + absValue := math.Abs(value) + + if absValue < 7 { + return toFixed(value, nil) + " day" + } else if absValue < 365 { + return toFixedScaled(value/7, nil, " week") + } else { + return toFixedScaled(value/365, nil, " year") + } +} + +// toWeeks returns a easy to read string representation of the given value in weeks +func toWeeks(value float64) string { + absValue := math.Abs(value) + + if absValue < 52 { + return toFixed(value, nil) + " week" + } else { + return toFixedScaled(value/52, nil, " year") + } +} diff --git a/pkg/query-service/formatter/time_test.go b/pkg/query-service/formatter/time_test.go new file mode 100644 index 0000000000..daa270e1ff --- /dev/null +++ b/pkg/query-service/formatter/time_test.go @@ -0,0 +1,29 @@ +package formatter + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestDuration(t *testing.T) { + durationFormatter := NewDurationFormatter() + + assert.Equal(t, "1 s", durationFormatter.Format(1, "s")) + assert.Equal(t, "5 µs", durationFormatter.Format(5000, "ns")) + assert.Equal(t, "1 min", durationFormatter.Format(60, "s")) + assert.Equal(t, "18.2 min", durationFormatter.Format(1092000000000, "ns")) + assert.Equal(t, "20 min", durationFormatter.Format(1200, "s")) + assert.Equal(t, "3.40 µs", durationFormatter.Format(3400, "ns")) + assert.Equal(t, "1 µs", durationFormatter.Format(1000, "ns")) + assert.Equal(t, "1 s", durationFormatter.Format(1000, "ms")) + assert.Equal(t, "2 day", durationFormatter.Format(172800, "s")) + assert.Equal(t, "4 week", durationFormatter.Format(2419200, "s")) + assert.Equal(t, "1.01 year", durationFormatter.Format(31736420, "s")) + assert.Equal(t, "38.5 year", durationFormatter.Format(2000, "w")) + assert.Equal(t, "1 ns", durationFormatter.Format(1, "ns")) + assert.Equal(t, "69 ms", durationFormatter.Format(69000000, "ns")) + assert.Equal(t, "1.82 min", durationFormatter.Format(109200000000, "ns")) + assert.Equal(t, "1.27 day", durationFormatter.Format(109800000000000, "ns")) + assert.Equal(t, "2 day", durationFormatter.Format(172800000, "ms")) +}