mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-10-10 09:56:30 +08:00
feat: add measurement value formatter (#2773)
This commit is contained in:
parent
826cbe0803
commit
745626f516
3
Makefile
3
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/...
|
||||
go test ./pkg/query-service/converter/...
|
||||
go test ./pkg/query-service/formatter/...
|
||||
|
2
go.mod
2
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
|
||||
|
3
go.sum
3
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=
|
||||
|
45
pkg/query-service/formatter/bool.go
Normal file
45
pkg/query-service/formatter/bool.go
Normal file
@ -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)
|
||||
|
||||
}
|
50
pkg/query-service/formatter/data.go
Normal file
50
pkg/query-service/formatter/data.go
Normal file
@ -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)
|
||||
}
|
70
pkg/query-service/formatter/data_rate.go
Normal file
70
pkg/query-service/formatter/data_rate.go
Normal file
@ -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)
|
||||
}
|
23
pkg/query-service/formatter/data_test.go
Normal file
23
pkg/query-service/formatter/data_test.go
Normal file
@ -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"))
|
||||
}
|
34
pkg/query-service/formatter/formatter.go
Normal file
34
pkg/query-service/formatter/formatter.go
Normal file
@ -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
|
||||
}
|
||||
}
|
13
pkg/query-service/formatter/none.go
Normal file
13
pkg/query-service/formatter/none.go
Normal file
@ -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)
|
||||
}
|
28
pkg/query-service/formatter/percent.go
Normal file
28
pkg/query-service/formatter/percent.go
Normal file
@ -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)
|
||||
}
|
137
pkg/query-service/formatter/scale.go
Normal file
137
pkg/query-service/formatter/scale.go
Normal file
@ -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
|
||||
}
|
||||
}
|
44
pkg/query-service/formatter/throughput.go
Normal file
44
pkg/query-service/formatter/throughput.go
Normal file
@ -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)
|
||||
}
|
15
pkg/query-service/formatter/throughput_test.go
Normal file
15
pkg/query-service/formatter/throughput_test.go
Normal file
@ -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"))
|
||||
}
|
171
pkg/query-service/formatter/time.go
Normal file
171
pkg/query-service/formatter/time.go
Normal file
@ -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")
|
||||
}
|
||||
}
|
29
pkg/query-service/formatter/time_test.go
Normal file
29
pkg/query-service/formatter/time_test.go
Normal file
@ -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"))
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user