mirror of
https://github.com/hibiken/asynqmon.git
synced 2025-01-18 18:55:54 +08:00
Add support for Prometheus integration
This commit is contained in:
parent
711ca8b5c8
commit
1b8d46a35e
40
README.md
40
README.md
@ -1,9 +1,12 @@
|
||||
<img src="https://user-images.githubusercontent.com/11155743/114745460-57760500-9d57-11eb-9a2c-43fa88171807.png" alt="Asynqmon logo" width="360px" />
|
||||
|
||||
# A modern web based tool for monitoring & administrating [Asynq](https://github.com/hibiken/asynq) queues, tasks and message broker
|
||||
# A modern web based tool for monitoring & administrating [Asynq](https://github.com/hibiken/asynq) queues and tasks
|
||||
|
||||
## Overview
|
||||
|
||||
Asynqmon is a web UI tool for monitoring & administrating [Asynq](https://github.com/hibiken/asynq) queues and tasks.
|
||||
It optionally integrates with Prometheus to display time-series data.
|
||||
|
||||
Asynqmon is both a library that you can include in your web application, as well as a binary that you can simply install and run.
|
||||
|
||||
## Version Compatibility
|
||||
@ -83,16 +86,28 @@ Here's the available flags:
|
||||
|
||||
_Note_: Use `--redis-url` to specify address, db-number, and password with one flag value; Alternatively, use `--redis-addr`, `--redis-db`, and `--redis-password` to specify each value.
|
||||
|
||||
| Flag | Env | Description | Default |
|
||||
| ------------------------------- | --------------------- | ------------------------------------------------------------------- | ---------------- |
|
||||
| `--port`(int) | `PORT` | port number to use for web ui server | 8080 |
|
||||
| `---redis-url`(string) | `REDIS_URL` | URL to redis server | "" |
|
||||
| `--redis-addr`(string) | `REDIS_ADDR` | address of redis server to connect to | "127.0.0.1:6379" |
|
||||
| `--redis-db`(int) | `REDIS_DB` | redis database number | 0 |
|
||||
| `--redis-password`(string) | `REDIS_PASSWORD` | password to use when connecting to redis server | "" |
|
||||
| `--redis-cluster-nodes`(string) | `REDIS_CLUSTER_NODES` | comma separated list of host:port addresses of cluster nodes | "" |
|
||||
| `--redis-tls`(string) | `REDIS_TLS` | server name for TLS validation used when connecting to redis server | "" |
|
||||
| `--redis-insecure-tls`(bool) | `REDIS_INSECURE_TLS` | disable TLS certificate host checks | false |
|
||||
| Flag | Env | Description | Default |
|
||||
| --------------------------------- | ------------------------- | ------------------------------------------------------------------- | ---------------- |
|
||||
| `--port`(int) | `PORT` | port number to use for web ui server | 8080 |
|
||||
| `---redis-url`(string) | `REDIS_URL` | URL to redis server | "" |
|
||||
| `--redis-addr`(string) | `REDIS_ADDR` | address of redis server to connect to | "127.0.0.1:6379" |
|
||||
| `--redis-db`(int) | `REDIS_DB` | redis database number | 0 |
|
||||
| `--redis-password`(string) | `REDIS_PASSWORD` | password to use when connecting to redis server | "" |
|
||||
| `--redis-cluster-nodes`(string) | `REDIS_CLUSTER_NODES` | comma separated list of host:port addresses of cluster nodes | "" |
|
||||
| `--redis-tls`(string) | `REDIS_TLS` | server name for TLS validation used when connecting to redis server | "" |
|
||||
| `--redis-insecure-tls`(bool) | `REDIS_INSECURE_TLS` | disable TLS certificate host checks | false |
|
||||
| `--enable-metrics-exporter`(bool) | `ENABLE_METRICS_EXPORTER` | enable prometheus metrics exporter to expose queue metrics | false |
|
||||
| `--prometheus-addr`(string) | `PROMETHEUS_ADDR` | address of prometheus server to query time series | "" |
|
||||
|
||||
### Integration with Prometheus
|
||||
|
||||
The binary supports two flags to enable integration with [Prometheus](https://prometheus.io/).
|
||||
|
||||
First, enable metrics exporter to expose queue metrics to Prometheus server by passing `--enable-metrics-exporter` flag.
|
||||
The metrics data is now available under `/metrics` for Prometheus to scape.
|
||||
|
||||
Once the metrics data is collected by a Prometheus server, you can pass the address of the Prometheus server to asynqmon to query the time-series data.
|
||||
The address can be specified via `--prometheus-addr`. This enables the metrics view on the Web UI.
|
||||
|
||||
### Examples
|
||||
|
||||
@ -100,6 +115,9 @@ _Note_: Use `--redis-url` to specify address, db-number, and password with one f
|
||||
# with a local binary; custom port and connect to redis server at localhost:6380
|
||||
./asynqmon --port=3000 --redis-addr=localhost:6380
|
||||
|
||||
# with prometheus integration enabled
|
||||
./asynqmon --enable-metrics-exporter --prometheus-addr=localhost:9090
|
||||
|
||||
# with Docker (connect to a Redis server running on the host machine)
|
||||
docker run --rm \
|
||||
--name asynqmon \
|
||||
|
@ -12,24 +12,28 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/rs/cors"
|
||||
|
||||
"github.com/hibiken/asynq"
|
||||
"github.com/hibiken/asynq/x/metrics"
|
||||
"github.com/hibiken/asynqmon"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
"github.com/rs/cors"
|
||||
)
|
||||
|
||||
// Command-line flags
|
||||
var (
|
||||
flagPort int
|
||||
flagRedisAddr string
|
||||
flagRedisDB int
|
||||
flagRedisPassword string
|
||||
flagRedisTLS string
|
||||
flagRedisURL string
|
||||
flagRedisInsecureTLS bool
|
||||
flagRedisClusterNodes string
|
||||
flagMaxPayloadLength int
|
||||
flagMaxResultLength int
|
||||
flagPort int
|
||||
flagRedisAddr string
|
||||
flagRedisDB int
|
||||
flagRedisPassword string
|
||||
flagRedisTLS string
|
||||
flagRedisURL string
|
||||
flagRedisInsecureTLS bool
|
||||
flagRedisClusterNodes string
|
||||
flagMaxPayloadLength int
|
||||
flagMaxResultLength int
|
||||
flagEnableMetricsExporter bool
|
||||
flagPrometheusServerAddr string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@ -37,12 +41,14 @@ func init() {
|
||||
flag.StringVar(&flagRedisAddr, "redis-addr", getEnvDefaultString("REDIS_ADDR", "127.0.0.1:6379"), "address of redis server to connect to")
|
||||
flag.IntVar(&flagRedisDB, "redis-db", getEnvOrDefaultInt("REDIS_DB", 0), "redis database number")
|
||||
flag.StringVar(&flagRedisPassword, "redis-password", getEnvDefaultString("REDIS_PASSWORD", ""), "password to use when connecting to redis server")
|
||||
flag.StringVar(&flagRedisTLS, "redis-tls", getEnvDefaultString("REDIS_TLS",""), "server name for TLS validation used when connecting to redis server")
|
||||
flag.StringVar(&flagRedisTLS, "redis-tls", getEnvDefaultString("REDIS_TLS", ""), "server name for TLS validation used when connecting to redis server")
|
||||
flag.StringVar(&flagRedisURL, "redis-url", getEnvDefaultString("REDIS_URL", ""), "URL to redis server")
|
||||
flag.BoolVar(&flagRedisInsecureTLS, "redis-insecure-tls", getEnvOrDefaultBool("REDIS_INSECURE_TLS", false), "disable TLS certificate host checks")
|
||||
flag.StringVar(&flagRedisClusterNodes, "redis-cluster-nodes", getEnvDefaultString("REDIS_CLUSTER_NODES", ""), "comma separated list of host:port addresses of cluster nodes")
|
||||
flag.IntVar(&flagMaxPayloadLength, "max-payload-length", getEnvOrDefaultInt("MAX_PAYLOAD_LENGTH", 200), "maximum number of utf8 characters printed in the payload cell in the Web UI")
|
||||
flag.IntVar(&flagMaxResultLength, "max-result-length", getEnvOrDefaultInt("MAX_RESULT_LENGTH", 200), "maximum number of utf8 characters printed in the result cell in the Web UI")
|
||||
flag.BoolVar(&flagEnableMetricsExporter, "enable-metrics-exporter", getEnvOrDefaultBool("ENABLE_METRICS_EXPORTER", false), "enable prometheus metrics exporter to expose queue metrics")
|
||||
flag.StringVar(&flagPrometheusServerAddr, "prometheus-addr", getEnvDefaultString("PROMETHEUS_ADDR", ""), "address of prometheus server to query time series")
|
||||
}
|
||||
|
||||
// TODO: Write test and refactor this code.
|
||||
@ -104,18 +110,35 @@ func main() {
|
||||
}
|
||||
|
||||
h := asynqmon.New(asynqmon.Options{
|
||||
RedisConnOpt: redisConnOpt,
|
||||
PayloadFormatter: asynqmon.PayloadFormatterFunc(formatPayload),
|
||||
ResultFormatter: asynqmon.ResultFormatterFunc(formatResult),
|
||||
RedisConnOpt: redisConnOpt,
|
||||
PayloadFormatter: asynqmon.PayloadFormatterFunc(formatPayload),
|
||||
ResultFormatter: asynqmon.ResultFormatterFunc(formatResult),
|
||||
PrometheusAddress: flagPrometheusServerAddr,
|
||||
})
|
||||
defer h.Close()
|
||||
|
||||
c := cors.New(cors.Options{
|
||||
AllowedMethods: []string{"GET", "POST", "DELETE"},
|
||||
})
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("/", c.Handler(h))
|
||||
if flagEnableMetricsExporter {
|
||||
// Using NewPedanticRegistry here to test the implementation of Collectors and Metrics.
|
||||
reg := prometheus.NewPedanticRegistry()
|
||||
|
||||
inspector := asynq.NewInspector(redisConnOpt)
|
||||
|
||||
reg.MustRegister(
|
||||
metrics.NewQueueMetricsCollector(inspector),
|
||||
// Add the standard process and go metrics to the registry
|
||||
prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{}),
|
||||
prometheus.NewGoCollector(),
|
||||
)
|
||||
mux.Handle("/metrics", promhttp.HandlerFor(reg, promhttp.HandlerOpts{}))
|
||||
}
|
||||
|
||||
srv := &http.Server{
|
||||
Handler: c.Handler(h),
|
||||
Handler: mux,
|
||||
Addr: fmt.Sprintf(":%d", flagPort),
|
||||
WriteTimeout: 10 * time.Second,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
@ -149,7 +172,7 @@ func truncate(s string, limit int) string {
|
||||
|
||||
func getEnvDefaultString(key, def string) string {
|
||||
v := os.Getenv(key)
|
||||
if (v == "") {
|
||||
if v == "" {
|
||||
return def
|
||||
}
|
||||
|
||||
@ -158,7 +181,7 @@ func getEnvDefaultString(key, def string) string {
|
||||
|
||||
func getEnvOrDefaultInt(key string, def int) int {
|
||||
v, err := strconv.Atoi(os.Getenv(key))
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
@ -166,8 +189,8 @@ func getEnvOrDefaultInt(key string, def int) int {
|
||||
|
||||
func getEnvOrDefaultBool(key string, def bool) bool {
|
||||
v, err := strconv.ParseBool(os.Getenv(key))
|
||||
if (err != nil) {
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
}
|
||||
|
12
go.mod
12
go.mod
@ -3,9 +3,15 @@ module github.com/hibiken/asynqmon
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/go-redis/redis/v8 v8.11.3
|
||||
github.com/go-redis/redis/v8 v8.11.4
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/hibiken/asynq v0.19.0
|
||||
github.com/hibiken/asynq v0.20.0
|
||||
github.com/hibiken/asynq/x v0.0.0-20211219150637-8dfabfccb3be // indirect
|
||||
github.com/prometheus/client_golang v1.11.0 // indirect
|
||||
github.com/rs/cors v1.7.0
|
||||
github.com/spf13/cast v1.4.1 // indirect
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 // indirect
|
||||
google.golang.org/protobuf v1.27.1 // indirect
|
||||
)
|
||||
|
||||
|
149
go.sum
149
go.sum
@ -1,35 +1,46 @@
|
||||
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE=
|
||||
github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M=
|
||||
github.com/go-redis/redis/v8 v8.11.3 h1:GCjoYp8c+yQTJfc0n69iwSiHjvuAdruxl7elnZCxgt8=
|
||||
github.com/go-redis/redis/v8 v8.11.3/go.mod h1:xNJ9xDG09FsIPwh3bWdk+0oDWHbtF9rPN0F/oD9XeKc=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg=
|
||||
github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
@ -38,6 +49,7 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
@ -46,25 +58,45 @@ github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMyw
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||
github.com/hibiken/asynq v0.18.6 h1:pBjtGh2QhDe1+/0yaSc56ANpdQ77BQgVfMIrj+NJrUM=
|
||||
github.com/hibiken/asynq v0.18.6/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
|
||||
github.com/hibiken/asynq v0.19.0 h1:AoJhoivymyFhF92ZAmVzxd7jr0RM264HdgkbjPc+x+M=
|
||||
github.com/hibiken/asynq v0.19.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
|
||||
github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
|
||||
github.com/hibiken/asynq v0.19.1 h1:Xo4arUse8zhjEYj8wUXgvUSGshMRsV1qgtrSGetgUtg=
|
||||
github.com/hibiken/asynq v0.19.1/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
|
||||
github.com/hibiken/asynq v0.20.0 h1:8k+safARLw9zwW91Tk/lbMl2OYTXUIvAibqJWGF6vMM=
|
||||
github.com/hibiken/asynq v0.20.0/go.mod h1:tyc63ojaW8SJ5SBm8mvI4DDONsguP5HE85EEl4Qr5Ig=
|
||||
github.com/hibiken/asynq/x v0.0.0-20211219150637-8dfabfccb3be h1:89J7WrDuoqFaKoQjZwqPczQXgXZ71liWYM+z9a8sILs=
|
||||
github.com/hibiken/asynq/x v0.0.0-20211219150637-8dfabfccb3be/go.mod h1:VmxwMfMKyb6gyv8xG0oOBMXIhquWKPx+zPtbVBd2Q1s=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
@ -76,87 +108,127 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||
github.com/onsi/gomega v1.15.0 h1:WjP/FQ/sk43MRmnEcT+MlDw2TFvkrXlprrPST/IudjU=
|
||||
github.com/onsi/gomega v1.15.0/go.mod h1:cIuvLEne0aoVhAgh/O6ac0Op8WWw9H6eYCriF+tEHG0=
|
||||
github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c=
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik=
|
||||
github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA=
|
||||
github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.2.1 h1:ruQGxdhGHe7FWOJPT0mKs5+pD2Xs1Bm/kdGlHO04FmM=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.uber.org/goleak v0.10.0 h1:G3eWbSNIskeRqtsN/1uI5B+eP73y3JUuBsv9AZjehb4=
|
||||
go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11 h1:GZokNIeuVkl3aZHJchRrr13WCsols02MLUcz1U9is6M=
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e h1:4nW4NLDYnU28ojHaHO8OVxFHk/aQ33U01a9cjED+pzE=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@ -164,15 +236,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
@ -186,20 +255,22 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
18
handler.go
18
handler.go
@ -34,6 +34,12 @@ type Options struct {
|
||||
//
|
||||
// This field is optional.
|
||||
ResultFormatter ResultFormatter
|
||||
|
||||
// PrometheusAddress specifies the address of the Prometheus to connect to.
|
||||
//
|
||||
// This field is optional. If this field is set, asynqmon will query the Prometheus server
|
||||
// to get the time series data about queue metrics and show them in the web UI.
|
||||
PrometheusAddress string
|
||||
}
|
||||
|
||||
// HTTPHandler is a http.Handler for asynqmon application.
|
||||
@ -181,12 +187,16 @@ func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspecto
|
||||
api.HandleFunc("/redis_info", newRedisInfoHandlerFunc(c)).Methods("GET")
|
||||
}
|
||||
|
||||
// Time series metrics endpoints.
|
||||
api.HandleFunc("/metrics", newGetMetricsHandlerFunc(http.DefaultClient, opts.PrometheusAddress)).Methods("GET")
|
||||
|
||||
// Everything else, route to uiAssetsHandler.
|
||||
router.NotFoundHandler = &uiAssetsHandler{
|
||||
rootPath: opts.RootPath,
|
||||
contents: staticContents,
|
||||
staticDirPath: "ui/build",
|
||||
indexFileName: "index.html",
|
||||
rootPath: opts.RootPath,
|
||||
contents: staticContents,
|
||||
staticDirPath: "ui/build",
|
||||
indexFileName: "index.html",
|
||||
prometheusAddr: opts.PrometheusAddress,
|
||||
}
|
||||
return router
|
||||
}
|
||||
|
230
metrics_handler.go
Normal file
230
metrics_handler.go
Normal file
@ -0,0 +1,230 @@
|
||||
package asynqmon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type getMetricsResponse struct {
|
||||
QueueSize *json.RawMessage `json:"queue_size"`
|
||||
QueueLatency *json.RawMessage `json:"queue_latency_seconds"`
|
||||
QueueMemUsgApprox *json.RawMessage `json:"queue_memory_usage_approx_bytes"`
|
||||
ProcessedPerSecond *json.RawMessage `json:"tasks_processed_per_second"`
|
||||
FailedPerSecond *json.RawMessage `json:"tasks_failed_per_second"`
|
||||
ErrorRate *json.RawMessage `json:"error_rate"`
|
||||
PendingTasksByQueue *json.RawMessage `json:"pending_tasks_by_queue"`
|
||||
RetryTasksByQueue *json.RawMessage `json:"retry_tasks_by_queue"`
|
||||
ArchivedTasksByQueue *json.RawMessage `json:"archived_tasks_by_queue"`
|
||||
}
|
||||
|
||||
type metricsFetchOptions struct {
|
||||
// Specifies the number of seconds to scan for metrics.
|
||||
duration time.Duration
|
||||
|
||||
// Specifies the end time when fetching metrics.
|
||||
endTime time.Time
|
||||
|
||||
// Optional filter to speicify a list of queues to get metrics for.
|
||||
// Empty list indicates no filter (i.e. get metrics for all queues).
|
||||
queues []string
|
||||
}
|
||||
|
||||
func newGetMetricsHandlerFunc(client *http.Client, prometheusAddr string) http.HandlerFunc {
|
||||
// res is the result of calling a JSON API endpoint.
|
||||
type res struct {
|
||||
query string
|
||||
msg *json.RawMessage
|
||||
err error
|
||||
}
|
||||
|
||||
// List of PromQLs.
|
||||
// Strings are used as template to optionally insert queue filter specified by QUEUE_FILTER.
|
||||
const (
|
||||
promQLQueueSize = "asynq_queue_size{QUEUE_FILTER}"
|
||||
promQLQueueLatency = "asynq_queue_latency_seconds{QUEUE_FILTER}"
|
||||
promQLMemUsage = "asynq_queue_memory_usage_approx_bytes{QUEUE_FILTER}"
|
||||
promQLProcessedTasks = "rate(asynq_tasks_processed_total{QUEUE_FILTER}[5m])"
|
||||
promQLFailedTasks = "rate(asynq_tasks_failed_total{QUEUE_FILTER}[5m])"
|
||||
promQLErrorRate = "rate(asynq_tasks_failed_total{QUEUE_FILTER}[5m]) / rate(asynq_tasks_processed_total{QUEUE_FILTER}[5m])"
|
||||
promQLPendingTasks = "asynq_tasks_enqueued_total{state=\"pending\",QUEUE_FILTER}"
|
||||
promQLRetryTasks = "asynq_tasks_enqueued_total{state=\"retry\",QUEUE_FILTER}"
|
||||
promQLArchivedTasks = "asynq_tasks_enqueued_total{state=\"archived\",QUEUE_FILTER}"
|
||||
)
|
||||
|
||||
// Optional query params:
|
||||
// `duration_sec`: specifies the number of seconds to scan
|
||||
// `end_time`: specifies the end_time in Unix time seconds
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
opts, err := extractMetricsFetchOptions(r)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("invalid query parameter: %v", err), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
// List of queries (i.e. promQL) to send to prometheus server.
|
||||
queries := []string{
|
||||
promQLQueueSize,
|
||||
promQLQueueLatency,
|
||||
promQLMemUsage,
|
||||
promQLProcessedTasks,
|
||||
promQLFailedTasks,
|
||||
promQLErrorRate,
|
||||
promQLPendingTasks,
|
||||
promQLRetryTasks,
|
||||
promQLArchivedTasks,
|
||||
}
|
||||
resp := getMetricsResponse{}
|
||||
// Make multiple API calls concurrently
|
||||
n := len(queries)
|
||||
ch := make(chan res, len(queries))
|
||||
for _, q := range queries {
|
||||
go func(q string) {
|
||||
url := buildPrometheusURL(prometheusAddr, q, opts)
|
||||
msg, err := fetchPrometheusMetrics(client, url)
|
||||
ch <- res{q, msg, err}
|
||||
}(q)
|
||||
}
|
||||
for r := range ch {
|
||||
n--
|
||||
if r.err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to fetch %q: %v", r.query, err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
switch r.query {
|
||||
case promQLQueueSize:
|
||||
resp.QueueSize = r.msg
|
||||
case promQLQueueLatency:
|
||||
resp.QueueLatency = r.msg
|
||||
case promQLMemUsage:
|
||||
resp.QueueMemUsgApprox = r.msg
|
||||
case promQLProcessedTasks:
|
||||
resp.ProcessedPerSecond = r.msg
|
||||
case promQLFailedTasks:
|
||||
resp.FailedPerSecond = r.msg
|
||||
case promQLErrorRate:
|
||||
resp.ErrorRate = r.msg
|
||||
case promQLPendingTasks:
|
||||
resp.PendingTasksByQueue = r.msg
|
||||
case promQLRetryTasks:
|
||||
resp.RetryTasksByQueue = r.msg
|
||||
case promQLArchivedTasks:
|
||||
resp.ArchivedTasksByQueue = r.msg
|
||||
}
|
||||
if n == 0 {
|
||||
break // fetched all metrics
|
||||
}
|
||||
}
|
||||
bytes, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to marshal response into JSON: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if _, err := w.Write(bytes); err != nil {
|
||||
http.Error(w, fmt.Sprintf("failed to write to response: %v", err), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const prometheusAPIPath = "/api/v1/query_range"
|
||||
|
||||
func extractMetricsFetchOptions(r *http.Request) (*metricsFetchOptions, error) {
|
||||
opts := &metricsFetchOptions{
|
||||
duration: 60 * time.Minute,
|
||||
endTime: time.Now(),
|
||||
}
|
||||
q := r.URL.Query()
|
||||
if d := q.Get("duration"); d != "" {
|
||||
val, err := strconv.Atoi(d)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value provided for duration: %q", d)
|
||||
}
|
||||
opts.duration = time.Duration(val) * time.Second
|
||||
}
|
||||
if t := q.Get("endtime"); t != "" {
|
||||
val, err := strconv.Atoi(t)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value provided for end_time: %q", t)
|
||||
}
|
||||
opts.endTime = time.Unix(int64(val), 0)
|
||||
}
|
||||
if qs := q.Get("queues"); qs != "" {
|
||||
opts.queues = strings.Split(qs, ",")
|
||||
}
|
||||
return opts, nil
|
||||
}
|
||||
|
||||
func buildPrometheusURL(baseAddr, promQL string, opts *metricsFetchOptions) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(strings.TrimSuffix(baseAddr, "/"))
|
||||
b.WriteString(prometheusAPIPath)
|
||||
v := url.Values{}
|
||||
v.Add("query", applyQueueFilter(promQL, opts.queues))
|
||||
v.Add("start", unixTimeString(opts.endTime.Add(-opts.duration)))
|
||||
v.Add("end", unixTimeString(opts.endTime))
|
||||
v.Add("step", strconv.Itoa(int(step(opts).Seconds())))
|
||||
b.WriteString("?")
|
||||
b.WriteString(v.Encode())
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func applyQueueFilter(promQL string, qnames []string) string {
|
||||
if len(qnames) == 0 {
|
||||
return strings.ReplaceAll(promQL, "QUEUE_FILTER", "")
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(`queue=~"`)
|
||||
for i, q := range qnames {
|
||||
if i != 0 {
|
||||
b.WriteString("|")
|
||||
}
|
||||
b.WriteString(q)
|
||||
}
|
||||
b.WriteByte('"')
|
||||
return strings.ReplaceAll(promQL, "QUEUE_FILTER", b.String())
|
||||
}
|
||||
|
||||
func fetchPrometheusMetrics(client *http.Client, url string) (*json.RawMessage, error) {
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
msg := json.RawMessage(bytes)
|
||||
return &msg, err
|
||||
}
|
||||
|
||||
// Returns step to use given the fetch options.
|
||||
// In general, the longer the duration, longer the each step.
|
||||
func step(opts *metricsFetchOptions) time.Duration {
|
||||
if opts.duration <= 6*time.Hour {
|
||||
// maximum number of data points to return: 6h / 10s = 2160
|
||||
return 10 * time.Second
|
||||
}
|
||||
if opts.duration <= 24*time.Hour {
|
||||
// maximum number of data points to return: 24h / 1m = 1440
|
||||
return 1 * time.Minute
|
||||
}
|
||||
if opts.duration <= 8*24*time.Hour {
|
||||
// maximum number of data points to return: (8*24)h / 3m = 3840
|
||||
return 3 * time.Minute
|
||||
}
|
||||
if opts.duration <= 30*24*time.Hour {
|
||||
// maximum number of data points to return: (30*24)h / 10m = 4320
|
||||
return 10 * time.Minute
|
||||
}
|
||||
return opts.duration / 3000
|
||||
}
|
||||
|
||||
func unixTimeString(t time.Time) string {
|
||||
return strconv.Itoa(int(t.Unix()))
|
||||
}
|
15
static.go
15
static.go
@ -15,10 +15,11 @@ import (
|
||||
// the path to the index file within that static directory are used to
|
||||
// serve the SPA.
|
||||
type uiAssetsHandler struct {
|
||||
rootPath string
|
||||
contents embed.FS
|
||||
staticDirPath string
|
||||
indexFileName string
|
||||
rootPath string
|
||||
contents embed.FS
|
||||
staticDirPath string
|
||||
indexFileName string
|
||||
prometheusAddr string
|
||||
}
|
||||
|
||||
// ServeHTTP inspects the URL path to locate a file within the static dir
|
||||
@ -59,9 +60,11 @@ func (h *uiAssetsHandler) renderIndexFile(w http.ResponseWriter) error {
|
||||
return err
|
||||
}
|
||||
data := struct {
|
||||
RootPath string
|
||||
RootPath string
|
||||
PrometheusAddr string
|
||||
}{
|
||||
RootPath: h.rootPath,
|
||||
RootPath: h.rootPath,
|
||||
PrometheusAddr: h.prometheusAddr,
|
||||
}
|
||||
return tmpl.Execute(w, data)
|
||||
}
|
||||
|
3
ui/build/static/js/2.8854b145.chunk.js
Normal file
3
ui/build/static/js/2.8854b145.chunk.js
Normal file
File diff suppressed because one or more lines are too long
253
ui/build/static/js/2.8854b145.chunk.js.LICENSE.txt
Normal file
253
ui/build/static/js/2.8854b145.chunk.js.LICENSE.txt
Normal file
@ -0,0 +1,253 @@
|
||||
/*
|
||||
object-assign
|
||||
(c) Sindre Sorhus
|
||||
@license MIT
|
||||
*/
|
||||
|
||||
/*!
|
||||
Copyright (c) 2017 Jed Watson.
|
||||
Licensed under the MIT License (MIT), see
|
||||
http://jedwatson.github.io/classnames
|
||||
*/
|
||||
|
||||
/*! Conditions:: INITIAL */
|
||||
|
||||
/*! Production:: $accept : expression $end */
|
||||
|
||||
/*! Production:: css_value : ANGLE */
|
||||
|
||||
/*! Production:: css_value : CHS */
|
||||
|
||||
/*! Production:: css_value : EMS */
|
||||
|
||||
/*! Production:: css_value : EXS */
|
||||
|
||||
/*! Production:: css_value : FREQ */
|
||||
|
||||
/*! Production:: css_value : LENGTH */
|
||||
|
||||
/*! Production:: css_value : PERCENTAGE */
|
||||
|
||||
/*! Production:: css_value : REMS */
|
||||
|
||||
/*! Production:: css_value : RES */
|
||||
|
||||
/*! Production:: css_value : SUB css_value */
|
||||
|
||||
/*! Production:: css_value : TIME */
|
||||
|
||||
/*! Production:: css_value : VHS */
|
||||
|
||||
/*! Production:: css_value : VMAXS */
|
||||
|
||||
/*! Production:: css_value : VMINS */
|
||||
|
||||
/*! Production:: css_value : VWS */
|
||||
|
||||
/*! Production:: css_variable : CSS_VAR LPAREN CSS_CPROP COMMA math_expression RPAREN */
|
||||
|
||||
/*! Production:: css_variable : CSS_VAR LPAREN CSS_CPROP RPAREN */
|
||||
|
||||
/*! Production:: expression : math_expression EOF */
|
||||
|
||||
/*! Production:: math_expression : LPAREN math_expression RPAREN */
|
||||
|
||||
/*! Production:: math_expression : NESTED_CALC LPAREN math_expression RPAREN */
|
||||
|
||||
/*! Production:: math_expression : SUB PREFIX SUB NESTED_CALC LPAREN math_expression RPAREN */
|
||||
|
||||
/*! Production:: math_expression : css_value */
|
||||
|
||||
/*! Production:: math_expression : css_variable */
|
||||
|
||||
/*! Production:: math_expression : math_expression ADD math_expression */
|
||||
|
||||
/*! Production:: math_expression : math_expression DIV math_expression */
|
||||
|
||||
/*! Production:: math_expression : math_expression MUL math_expression */
|
||||
|
||||
/*! Production:: math_expression : math_expression SUB math_expression */
|
||||
|
||||
/*! Production:: math_expression : value */
|
||||
|
||||
/*! Production:: value : NUMBER */
|
||||
|
||||
/*! Production:: value : SUB NUMBER */
|
||||
|
||||
/*! Rule:: $ */
|
||||
|
||||
/*! Rule:: (--[0-9a-z-A-Z-]*) */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)% */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)Hz\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ch\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)cm\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)deg\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dpcm\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dpi\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)dppx\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)em\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ex\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)grad\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)in\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)kHz\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)mm\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)ms\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)pc\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)pt\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)px\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)rad\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)rem\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)s\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)turn\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vh\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vmax\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vmin\b */
|
||||
|
||||
/*! Rule:: ([0-9]+(\.[0-9]*)?|\.[0-9]+)vw\b */
|
||||
|
||||
/*! Rule:: ([a-z]+) */
|
||||
|
||||
/*! Rule:: (calc) */
|
||||
|
||||
/*! Rule:: (var) */
|
||||
|
||||
/*! Rule:: , */
|
||||
|
||||
/*! Rule:: - */
|
||||
|
||||
/*! Rule:: \( */
|
||||
|
||||
/*! Rule:: \) */
|
||||
|
||||
/*! Rule:: \* */
|
||||
|
||||
/*! Rule:: \+ */
|
||||
|
||||
/*! Rule:: \/ */
|
||||
|
||||
/*! Rule:: \s+ */
|
||||
|
||||
/*! decimal.js-light v2.5.1 https://github.com/MikeMcl/decimal.js-light/LICENCE */
|
||||
|
||||
/**
|
||||
* A better abstraction over CSS.
|
||||
*
|
||||
* @copyright Oleg Isonen (Slobodskoi) / Isonen 2014-present
|
||||
* @website https://github.com/cssinjs/jss
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/** @license React v0.19.1
|
||||
* scheduler.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.10.2
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.13.1
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.14.0
|
||||
* react-dom.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.14.0
|
||||
* react-jsx-runtime.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v16.14.0
|
||||
* react.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/** @license React v17.0.1
|
||||
* react-is.production.min.js
|
||||
*
|
||||
* Copyright (c) Facebook, Inc. and its affiliates.
|
||||
*
|
||||
* This source code is licensed under the MIT license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
/**!
|
||||
* @fileOverview Kickass library to create and place poppers near their reference elements.
|
||||
* @version 1.16.1-lts
|
||||
* @license
|
||||
* Copyright (c) 2016 Federico Zivolo and contributors
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
* SOFTWARE.
|
||||
*/
|
1
ui/build/static/js/2.8854b145.chunk.js.map
Normal file
1
ui/build/static/js/2.8854b145.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
2
ui/build/static/js/main.aac2a828.chunk.js
Normal file
2
ui/build/static/js/main.aac2a828.chunk.js
Normal file
File diff suppressed because one or more lines are too long
1
ui/build/static/js/main.aac2a828.chunk.js.map
Normal file
1
ui/build/static/js/main.aac2a828.chunk.js.map
Normal file
File diff suppressed because one or more lines are too long
@ -51,6 +51,7 @@
|
||||
/>
|
||||
<script>
|
||||
window.ROOT_PATH = "%PUBLIC_URL%";
|
||||
window.PROMETHEUS_SERVER_ADDRESS = "/[[.PrometheusAddr]]";
|
||||
</script>
|
||||
<title>Asynq - Monitoring</title>
|
||||
</head>
|
||||
|
@ -22,6 +22,7 @@ import LayersIcon from "@material-ui/icons/Layers";
|
||||
import SettingsIcon from "@material-ui/icons/Settings";
|
||||
import ScheduleIcon from "@material-ui/icons/Schedule";
|
||||
import FeedbackIcon from "@material-ui/icons/Feedback";
|
||||
import TimelineIcon from "@material-ui/icons/Timeline";
|
||||
import DoubleArrowIcon from "@material-ui/icons/DoubleArrow";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import { AppState } from "./store";
|
||||
@ -37,6 +38,7 @@ import TaskDetailsView from "./views/TaskDetailsView";
|
||||
import SettingsView from "./views/SettingsView";
|
||||
import ServersView from "./views/ServersView";
|
||||
import RedisInfoView from "./views/RedisInfoView";
|
||||
import MetricsView from "./views/MetricsView";
|
||||
import PageNotFoundView from "./views/PageNotFoundView";
|
||||
|
||||
const drawerWidth = 220;
|
||||
@ -244,6 +246,13 @@ function App(props: ConnectedProps<typeof connector>) {
|
||||
primary="Redis"
|
||||
icon={<LayersIcon />}
|
||||
/>
|
||||
{window.PROMETHEUS_SERVER_ADDRESS && (
|
||||
<ListItemLink
|
||||
to={paths.METRICS}
|
||||
primary="Metrics"
|
||||
icon={<TimelineIcon />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</List>
|
||||
<List>
|
||||
@ -291,6 +300,9 @@ function App(props: ConnectedProps<typeof connector>) {
|
||||
<Route exact path={paths.HOME}>
|
||||
<DashboardView />
|
||||
</Route>
|
||||
<Route exact path={paths.METRICS}>
|
||||
<MetricsView />
|
||||
</Route>
|
||||
<Route path="*">
|
||||
<PageNotFoundView />
|
||||
</Route>
|
||||
|
48
ui/src/actions/metricsActions.ts
Normal file
48
ui/src/actions/metricsActions.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import { Dispatch } from "redux";
|
||||
import { getMetrics, MetricsResponse } from "../api";
|
||||
import { toErrorString, toErrorStringWithHttpStatus } from "../utils";
|
||||
|
||||
// List of metrics related action types.
|
||||
export const GET_METRICS_BEGIN = "GET_METRICS_BEGIN";
|
||||
export const GET_METRICS_SUCCESS = "GET_METRICS_SUCCESS";
|
||||
export const GET_METRICS_ERROR = "GET_METRICS_ERROR";
|
||||
|
||||
interface GetMetricsBeginAction {
|
||||
type: typeof GET_METRICS_BEGIN;
|
||||
}
|
||||
|
||||
interface GetMetricsSuccessAction {
|
||||
type: typeof GET_METRICS_SUCCESS;
|
||||
payload: MetricsResponse;
|
||||
}
|
||||
|
||||
interface GetMetricsErrorAction {
|
||||
type: typeof GET_METRICS_ERROR;
|
||||
error: string;
|
||||
}
|
||||
|
||||
// Union of all metrics related actions.
|
||||
export type MetricsActionTypes =
|
||||
| GetMetricsBeginAction
|
||||
| GetMetricsSuccessAction
|
||||
| GetMetricsErrorAction;
|
||||
|
||||
export function getMetricsAsync(
|
||||
endTime: number,
|
||||
duration: number,
|
||||
queues: string[]
|
||||
) {
|
||||
return async (dispatch: Dispatch<MetricsActionTypes>) => {
|
||||
dispatch({ type: GET_METRICS_BEGIN });
|
||||
try {
|
||||
const response = await getMetrics(endTime, duration, queues);
|
||||
dispatch({ type: GET_METRICS_SUCCESS, payload: response });
|
||||
} catch (error) {
|
||||
console.error(`getMetricsAsync: ${toErrorStringWithHttpStatus(error)}`);
|
||||
dispatch({
|
||||
type: GET_METRICS_ERROR,
|
||||
error: toErrorString(error),
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
@ -76,6 +76,45 @@ export interface QueueLocation {
|
||||
nodes: string[]; // node addresses
|
||||
}
|
||||
|
||||
export interface MetricsResponse {
|
||||
queue_size: PrometheusMetricsResponse;
|
||||
queue_latency_seconds: PrometheusMetricsResponse;
|
||||
queue_memory_usage_approx_bytes: PrometheusMetricsResponse;
|
||||
tasks_processed_per_second: PrometheusMetricsResponse;
|
||||
tasks_failed_per_second: PrometheusMetricsResponse;
|
||||
error_rate: PrometheusMetricsResponse;
|
||||
pending_tasks_by_queue: PrometheusMetricsResponse;
|
||||
retry_tasks_by_queue: PrometheusMetricsResponse;
|
||||
archived_tasks_by_queue: PrometheusMetricsResponse;
|
||||
}
|
||||
|
||||
export interface PrometheusMetricsResponse {
|
||||
status: "success" | "error";
|
||||
data?: MetricsResult; // present if status === "success"
|
||||
error?: string; // present if status === "error"
|
||||
errorType?: string; // present if status === "error"
|
||||
}
|
||||
|
||||
export interface MetricsResult {
|
||||
resultType: string;
|
||||
result: Metrics[];
|
||||
}
|
||||
|
||||
export interface Metrics {
|
||||
metric: MetricsInfo;
|
||||
values: [number, string][]; // [unixtime, value]
|
||||
}
|
||||
|
||||
export interface MetricsInfo {
|
||||
__name__: string;
|
||||
instance: string;
|
||||
job: string;
|
||||
|
||||
// labels (may or may not be present depending on metrics)
|
||||
queue?: string;
|
||||
state?: string;
|
||||
}
|
||||
|
||||
// Return value from redis INFO command.
|
||||
// See https://redis.io/commands/info#return-value.
|
||||
export interface RedisInfo {
|
||||
@ -854,3 +893,28 @@ export async function getRedisInfo(): Promise<RedisInfoResponse> {
|
||||
});
|
||||
return resp.data;
|
||||
}
|
||||
|
||||
interface MetricsEndpointParams {
|
||||
endtime: number;
|
||||
duration: number;
|
||||
queues?: string; // comma-separated list of queues
|
||||
}
|
||||
|
||||
export async function getMetrics(
|
||||
endTime: number,
|
||||
duration: number,
|
||||
queues: string[]
|
||||
): Promise<MetricsResponse> {
|
||||
let params: MetricsEndpointParams = {
|
||||
endtime: endTime,
|
||||
duration: duration,
|
||||
};
|
||||
if (queues && queues.length > 0) {
|
||||
params.queues = queues.join(",");
|
||||
}
|
||||
const resp = await axios({
|
||||
method: "get",
|
||||
url: `${BASE_URL}/metrics?${queryString.stringify(params)}`,
|
||||
});
|
||||
return resp.data;
|
||||
}
|
||||
|
@ -30,8 +30,12 @@ export default function DailyStatsChart(props: Props) {
|
||||
<ResponsiveContainer>
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="date" minTickGap={10} />
|
||||
<YAxis />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
minTickGap={10}
|
||||
stroke={theme.palette.text.secondary}
|
||||
/>
|
||||
<YAxis stroke={theme.palette.text.secondary} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Line
|
||||
|
736
ui/src/components/MetricsFetchControls.tsx
Normal file
736
ui/src/components/MetricsFetchControls.tsx
Normal file
@ -0,0 +1,736 @@
|
||||
import React from "react";
|
||||
import { connect, ConnectedProps } from "react-redux";
|
||||
import { makeStyles, Theme } from "@material-ui/core/styles";
|
||||
import Button, { ButtonProps } from "@material-ui/core/Button";
|
||||
import ButtonGroup from "@material-ui/core/ButtonGroup";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import Popover from "@material-ui/core/Popover";
|
||||
import Radio from "@material-ui/core/Radio";
|
||||
import RadioGroup from "@material-ui/core/RadioGroup";
|
||||
import Checkbox from "@material-ui/core/Checkbox";
|
||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||
import FormControl from "@material-ui/core/FormControl";
|
||||
import FormGroup from "@material-ui/core/FormGroup";
|
||||
import FormLabel from "@material-ui/core/FormLabel";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import ArrowLeftIcon from "@material-ui/icons/ArrowLeft";
|
||||
import ArrowRightIcon from "@material-ui/icons/ArrowRight";
|
||||
import FilterListIcon from "@material-ui/icons/FilterList";
|
||||
import dayjs from "dayjs";
|
||||
import { currentUnixtime, parseDuration } from "../utils";
|
||||
import { AppState } from "../store";
|
||||
import { isDarkTheme } from "../theme";
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return { pollInterval: state.settings.pollInterval };
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps);
|
||||
type ReduxProps = ConnectedProps<typeof connector>;
|
||||
|
||||
interface Props extends ReduxProps {
|
||||
// Specifies the endtime in Unix time seconds.
|
||||
endTimeSec: number;
|
||||
onEndTimeChange: (t: number, isEndTimeFixed: boolean) => void;
|
||||
|
||||
// Specifies the duration in seconds.
|
||||
durationSec: number;
|
||||
onDurationChange: (d: number, isEndTimeFixed: boolean) => void;
|
||||
|
||||
// All available queues.
|
||||
queues: string[];
|
||||
// Selected queues.
|
||||
selectedQueues: string[];
|
||||
addQueue: (qname: string) => void;
|
||||
removeQueue: (qname: string) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
endTimeOption: EndTimeOption;
|
||||
durationOption: DurationOption;
|
||||
customEndTime: string; // text shown in input field
|
||||
customDuration: string; // text shown in input field
|
||||
customEndTimeError: string;
|
||||
customDurationError: string;
|
||||
}
|
||||
|
||||
type EndTimeOption = "real_time" | "freeze_at_now" | "custom";
|
||||
type DurationOption = "1h" | "6h" | "1d" | "8d" | "30d" | "custom";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
endTimeCaption: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
shiftButtons: {
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
buttonGroupRoot: {
|
||||
height: 29,
|
||||
position: "relative",
|
||||
top: 1,
|
||||
},
|
||||
endTimeShiftControls: {
|
||||
padding: theme.spacing(1),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderBottomColor: theme.palette.divider,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomStyle: "solid",
|
||||
},
|
||||
leftShiftButtons: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
rightShiftButtons: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginLeft: theme.spacing(2),
|
||||
},
|
||||
controlsContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
controlSelectorBox: {
|
||||
display: "flex",
|
||||
minWidth: 490,
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
controlEndTimeSelector: {
|
||||
width: "50%",
|
||||
},
|
||||
controlDurationSelector: {
|
||||
width: "50%",
|
||||
},
|
||||
radioButtonRoot: {
|
||||
paddingTop: theme.spacing(0.5),
|
||||
paddingBottom: theme.spacing(0.5),
|
||||
paddingLeft: theme.spacing(1),
|
||||
paddingRight: theme.spacing(1),
|
||||
},
|
||||
formControlLabel: {
|
||||
fontSize: 14,
|
||||
},
|
||||
buttonLabel: {
|
||||
textTransform: "none",
|
||||
fontSize: 12,
|
||||
},
|
||||
formControlRoot: {
|
||||
width: "100%",
|
||||
margin: 0,
|
||||
},
|
||||
formLabel: {
|
||||
fontSize: 14,
|
||||
fontWeight: 500,
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
customInputField: {
|
||||
marginTop: theme.spacing(1),
|
||||
},
|
||||
filterButton: {
|
||||
marginLeft: theme.spacing(1),
|
||||
},
|
||||
queueFilters: {
|
||||
padding: theme.spacing(2),
|
||||
maxHeight: 400,
|
||||
},
|
||||
checkbox: {
|
||||
padding: 6,
|
||||
},
|
||||
}));
|
||||
|
||||
// minute, hour, day in seconds
|
||||
const minute = 60;
|
||||
const hour = 60 * minute;
|
||||
const day = 24 * hour;
|
||||
|
||||
function getInitialState(endTimeSec: number, durationSec: number): State {
|
||||
let endTimeOption: EndTimeOption = "real_time";
|
||||
let customEndTime = "";
|
||||
let durationOption: DurationOption = "1h";
|
||||
let customDuration = "";
|
||||
|
||||
const now = currentUnixtime();
|
||||
// Account for 1s difference, may just happen to elapse 1s
|
||||
// between the parent component's render and this component's render.
|
||||
if (now <= endTimeSec && endTimeSec <= now + 1) {
|
||||
endTimeOption = "real_time";
|
||||
} else {
|
||||
endTimeOption = "custom";
|
||||
customEndTime = new Date(endTimeSec * 1000).toISOString();
|
||||
}
|
||||
|
||||
switch (durationSec) {
|
||||
case 1 * hour:
|
||||
durationOption = "1h";
|
||||
break;
|
||||
case 6 * hour:
|
||||
durationOption = "6h";
|
||||
break;
|
||||
case 1 * day:
|
||||
durationOption = "1d";
|
||||
break;
|
||||
case 8 * day:
|
||||
durationOption = "8d";
|
||||
break;
|
||||
case 30 * day:
|
||||
durationOption = "30d";
|
||||
break;
|
||||
default:
|
||||
durationOption = "custom";
|
||||
customDuration = durationSec + "s";
|
||||
}
|
||||
|
||||
return {
|
||||
endTimeOption,
|
||||
customEndTime,
|
||||
customEndTimeError: "",
|
||||
durationOption,
|
||||
customDuration,
|
||||
customDurationError: "",
|
||||
};
|
||||
}
|
||||
|
||||
function MetricsFetchControls(props: Props) {
|
||||
const classes = useStyles();
|
||||
|
||||
const [state, setState] = React.useState<State>(
|
||||
getInitialState(props.endTimeSec, props.durationSec)
|
||||
);
|
||||
const [timePopoverAnchorElem, setTimePopoverAnchorElem] =
|
||||
React.useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const [queuePopoverAnchorElem, setQueuePopoverAnchorElem] =
|
||||
React.useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const handleEndTimeOptionChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const selectedOpt = (event.target as HTMLInputElement)
|
||||
.value as EndTimeOption;
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
endTimeOption: selectedOpt,
|
||||
customEndTime: "",
|
||||
customEndTimeError: "",
|
||||
}));
|
||||
switch (selectedOpt) {
|
||||
case "real_time":
|
||||
props.onEndTimeChange(currentUnixtime(), /*isEndTimeFixed=*/ false);
|
||||
break;
|
||||
case "freeze_at_now":
|
||||
props.onEndTimeChange(currentUnixtime(), /*isEndTimeFixed=*/ true);
|
||||
break;
|
||||
case "custom":
|
||||
// No-op
|
||||
}
|
||||
};
|
||||
|
||||
const handleDurationOptionChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
const selectedOpt = (event.target as HTMLInputElement)
|
||||
.value as DurationOption;
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
durationOption: selectedOpt,
|
||||
customDuration: "",
|
||||
customDurationError: "",
|
||||
}));
|
||||
const isEndTimeFixed = state.endTimeOption !== "real_time";
|
||||
switch (selectedOpt) {
|
||||
case "1h":
|
||||
props.onDurationChange(1 * hour, isEndTimeFixed);
|
||||
break;
|
||||
case "6h":
|
||||
props.onDurationChange(6 * hour, isEndTimeFixed);
|
||||
break;
|
||||
case "1d":
|
||||
props.onDurationChange(1 * day, isEndTimeFixed);
|
||||
break;
|
||||
case "8d":
|
||||
props.onDurationChange(8 * day, isEndTimeFixed);
|
||||
break;
|
||||
case "30d":
|
||||
props.onDurationChange(30 * day, isEndTimeFixed);
|
||||
break;
|
||||
case "custom":
|
||||
// No-op
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomDurationChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
event.persist(); // https://reactjs.org/docs/legacy-event-pooling.html
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
customDuration: event.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCustomEndTimeChange = (
|
||||
event: React.ChangeEvent<HTMLInputElement>
|
||||
) => {
|
||||
event.persist(); // https://reactjs.org/docs/legacy-event-pooling.html
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
customEndTime: event.target.value,
|
||||
}));
|
||||
};
|
||||
|
||||
const handleCustomDurationKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (event.key === "Enter") {
|
||||
try {
|
||||
const d = parseDuration(state.customDuration);
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
durationOption: "custom",
|
||||
customDurationError: "",
|
||||
}));
|
||||
props.onDurationChange(d, state.endTimeOption !== "real_time");
|
||||
} catch (error) {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
customDurationError: "Duration invalid",
|
||||
}));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomEndTimeKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLInputElement>
|
||||
) => {
|
||||
if (event.key === "Enter") {
|
||||
const timeUsecOrNaN = Date.parse(state.customEndTime);
|
||||
if (isNaN(timeUsecOrNaN)) {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
customEndTimeError: "End time invalid",
|
||||
}));
|
||||
return;
|
||||
}
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
endTimeOption: "custom",
|
||||
customEndTimeError: "",
|
||||
}));
|
||||
props.onEndTimeChange(
|
||||
Math.floor(timeUsecOrNaN / 1000),
|
||||
/* isEndTimeFixed= */ true
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenTimePopover = (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
setTimePopoverAnchorElem(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseTimePopover = () => {
|
||||
setTimePopoverAnchorElem(null);
|
||||
};
|
||||
|
||||
const handleOpenQueuePopover = (
|
||||
event: React.MouseEvent<HTMLButtonElement>
|
||||
) => {
|
||||
setQueuePopoverAnchorElem(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseQueuePopover = () => {
|
||||
setQueuePopoverAnchorElem(null);
|
||||
};
|
||||
|
||||
const isTimePopoverOpen = Boolean(timePopoverAnchorElem);
|
||||
const isQueuePopoverOpen = Boolean(queuePopoverAnchorElem);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (state.endTimeOption === "real_time") {
|
||||
const id = setInterval(() => {
|
||||
props.onEndTimeChange(currentUnixtime(), /*isEndTimeFixed=*/ false);
|
||||
}, props.pollInterval * 1000);
|
||||
return () => clearInterval(id);
|
||||
}
|
||||
});
|
||||
|
||||
const shiftBy = (deltaSec: number) => {
|
||||
return () => {
|
||||
const now = currentUnixtime();
|
||||
const endTime = props.endTimeSec + deltaSec;
|
||||
if (now <= endTime) {
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
customEndTime: "",
|
||||
endTimeOption: "real_time",
|
||||
}));
|
||||
props.onEndTimeChange(now, /*isEndTimeFixed=*/ false);
|
||||
return;
|
||||
}
|
||||
setState((prevState) => ({
|
||||
...prevState,
|
||||
endTimeOption: "custom",
|
||||
customEndTime: new Date(endTime * 1000).toISOString(),
|
||||
}));
|
||||
props.onEndTimeChange(endTime, /*isEndTimeFixed=*/ true);
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textPrimary"
|
||||
className={classes.endTimeCaption}
|
||||
>
|
||||
{formatTime(props.endTimeSec)}
|
||||
</Typography>
|
||||
<div>
|
||||
<Button
|
||||
aria-describedby={isTimePopoverOpen ? "time-popover" : undefined}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
onClick={handleOpenTimePopover}
|
||||
size="small"
|
||||
classes={{
|
||||
label: classes.buttonLabel,
|
||||
}}
|
||||
>
|
||||
{state.endTimeOption === "real_time" ? "Realtime" : "Historical"}:{" "}
|
||||
{state.durationOption === "custom"
|
||||
? state.customDuration
|
||||
: state.durationOption}
|
||||
</Button>
|
||||
<Popover
|
||||
id={isTimePopoverOpen ? "time-popover" : undefined}
|
||||
open={isTimePopoverOpen}
|
||||
anchorEl={timePopoverAnchorElem}
|
||||
onClose={handleCloseTimePopover}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
<div className={classes.endTimeShiftControls}>
|
||||
<div className={classes.leftShiftButtons}>
|
||||
<ShiftButton
|
||||
direction="left"
|
||||
text="2h"
|
||||
onClick={shiftBy(-2 * hour)}
|
||||
dense={true}
|
||||
/>
|
||||
<ShiftButton
|
||||
direction="left"
|
||||
text="1h"
|
||||
onClick={shiftBy(-1 * hour)}
|
||||
dense={true}
|
||||
/>
|
||||
<ShiftButton
|
||||
direction="left"
|
||||
text="30m"
|
||||
onClick={shiftBy(-30 * minute)}
|
||||
dense={true}
|
||||
/>
|
||||
<ShiftButton
|
||||
direction="left"
|
||||
text="15m"
|
||||
onClick={shiftBy(-15 * minute)}
|
||||
dense={true}
|
||||
/>
|
||||
<ShiftButton
|
||||
direction="left"
|
||||
text="5m"
|
||||
onClick={shiftBy(-5 * minute)}
|
||||
dense={true}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.rightShiftButtons}>
|
||||
<ShiftButton
|
||||
direction="right"
|
||||
text="5m"
|
||||
onClick={shiftBy(5 * minute)}
|
||||
dense={true}
|
||||
/>
|
||||
<ShiftButton
|
||||
direction="right"
|
||||
text="15m"
|
||||
onClick={shiftBy(15 * minute)}
|
||||
dense={true}
|
||||
/>
|
||||
<ShiftButton
|
||||
direction="right"
|
||||
text="30m"
|
||||
onClick={shiftBy(30 * minute)}
|
||||
dense={true}
|
||||
/>
|
||||
<ShiftButton
|
||||
direction="right"
|
||||
text="1h"
|
||||
onClick={shiftBy(1 * hour)}
|
||||
dense={true}
|
||||
/>
|
||||
<ShiftButton
|
||||
direction="right"
|
||||
text="2h"
|
||||
onClick={shiftBy(2 * hour)}
|
||||
dense={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.controlSelectorBox}>
|
||||
<div className={classes.controlEndTimeSelector}>
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
margin="dense"
|
||||
classes={{ root: classes.formControlRoot }}
|
||||
>
|
||||
<FormLabel className={classes.formLabel} component="legend">
|
||||
End Time
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="end_time"
|
||||
name="end_time"
|
||||
value={state.endTimeOption}
|
||||
onChange={handleEndTimeOptionChange}
|
||||
>
|
||||
<RadioInput value="real_time" label="Real Time" />
|
||||
<RadioInput value="freeze_at_now" label="Freeze at now" />
|
||||
<RadioInput value="custom" label="Custom End Time" />
|
||||
</RadioGroup>
|
||||
<div className={classes.customInputField}>
|
||||
<TextField
|
||||
id="custom-endtime"
|
||||
label="yyyy-mm-dd hh:mm:ssz"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onChange={handleCustomEndTimeChange}
|
||||
value={state.customEndTime}
|
||||
onKeyDown={handleCustomEndTimeKeyDown}
|
||||
error={state.customEndTimeError !== ""}
|
||||
helperText={state.customEndTimeError}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
<div className={classes.controlDurationSelector}>
|
||||
<FormControl
|
||||
component="fieldset"
|
||||
margin="dense"
|
||||
classes={{ root: classes.formControlRoot }}
|
||||
>
|
||||
<FormLabel className={classes.formLabel} component="legend">
|
||||
Duration
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
aria-label="duration"
|
||||
name="duration"
|
||||
value={state.durationOption}
|
||||
onChange={handleDurationOptionChange}
|
||||
>
|
||||
<RadioInput value="1h" label="1h" />
|
||||
<RadioInput value="6h" label="6h" />
|
||||
<RadioInput value="1d" label="1 day" />
|
||||
<RadioInput value="8d" label="8 days" />
|
||||
<RadioInput value="30d" label="30 days" />
|
||||
<RadioInput value="custom" label="Custom Duration" />
|
||||
</RadioGroup>
|
||||
<div className={classes.customInputField}>
|
||||
<TextField
|
||||
id="custom-duration"
|
||||
label="duration"
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onChange={handleCustomDurationChange}
|
||||
value={state.customDuration}
|
||||
onKeyDown={handleCustomDurationKeyDown}
|
||||
error={state.customDurationError !== ""}
|
||||
helperText={state.customDurationError}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
<div className={classes.shiftButtons}>
|
||||
<ButtonGroup
|
||||
classes={{ root: classes.buttonGroupRoot }}
|
||||
size="small"
|
||||
color="primary"
|
||||
aria-label="shift buttons"
|
||||
>
|
||||
<ShiftButton
|
||||
direction="left"
|
||||
text={
|
||||
state.durationOption === "custom" ? "1h" : state.durationOption
|
||||
}
|
||||
color="primary"
|
||||
onClick={
|
||||
state.durationOption === "custom"
|
||||
? shiftBy(-1 * hour)
|
||||
: shiftBy(-props.durationSec)
|
||||
}
|
||||
/>
|
||||
<ShiftButton
|
||||
direction="right"
|
||||
text={
|
||||
state.durationOption === "custom" ? "1h" : state.durationOption
|
||||
}
|
||||
color="primary"
|
||||
onClick={
|
||||
state.durationOption === "custom"
|
||||
? shiftBy(1 * hour)
|
||||
: shiftBy(props.durationSec)
|
||||
}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<div className={classes.filterButton}>
|
||||
<IconButton
|
||||
aria-label="filter"
|
||||
size="small"
|
||||
onClick={handleOpenQueuePopover}
|
||||
>
|
||||
<FilterListIcon />
|
||||
</IconButton>
|
||||
<Popover
|
||||
id={isQueuePopoverOpen ? "queue-popover" : undefined}
|
||||
open={isQueuePopoverOpen}
|
||||
anchorEl={queuePopoverAnchorElem}
|
||||
onClose={handleCloseQueuePopover}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
<FormControl className={classes.queueFilters}>
|
||||
<FormLabel className={classes.formLabel} component="legend">
|
||||
Queues
|
||||
</FormLabel>
|
||||
<FormGroup>
|
||||
{props.queues.map((qname) => (
|
||||
<FormControlLabel
|
||||
key={qname}
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
checked={props.selectedQueues.includes(qname)}
|
||||
onChange={() => {
|
||||
if (props.selectedQueues.includes(qname)) {
|
||||
props.removeQueue(qname);
|
||||
} else {
|
||||
props.addQueue(qname);
|
||||
}
|
||||
}}
|
||||
name={qname}
|
||||
className={classes.checkbox}
|
||||
/>
|
||||
}
|
||||
label={qname}
|
||||
classes={{ label: classes.formControlLabel }}
|
||||
/>
|
||||
))}
|
||||
</FormGroup>
|
||||
</FormControl>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/****************** Helper functions/components *******************/
|
||||
|
||||
function formatTime(unixtime: number): string {
|
||||
const tz = new Date(unixtime * 1000)
|
||||
.toLocaleTimeString("en-us", { timeZoneName: "short" })
|
||||
.split(" ")[2];
|
||||
return dayjs.unix(unixtime).format("ddd, DD MMM YYYY HH:mm:ss ") + tz;
|
||||
}
|
||||
|
||||
interface RadioInputProps {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function RadioInput(props: RadioInputProps) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<FormControlLabel
|
||||
classes={{ label: classes.formControlLabel }}
|
||||
value={props.value}
|
||||
control={
|
||||
<Radio size="small" classes={{ root: classes.radioButtonRoot }} />
|
||||
}
|
||||
label={props.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ShiftButtonProps extends ButtonProps {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
direction: "left" | "right";
|
||||
dense?: boolean;
|
||||
}
|
||||
|
||||
const useShiftButtonStyles = makeStyles((theme: Theme) => ({
|
||||
root: {
|
||||
minWidth: 40,
|
||||
fontWeight: (props: ShiftButtonProps) => (props.dense ? 400 : 500),
|
||||
},
|
||||
label: { fontSize: 12, textTransform: "none" },
|
||||
iconRoot: {
|
||||
marginRight: (props: ShiftButtonProps) =>
|
||||
props.direction === "left" ? (props.dense ? -8 : -4) : 0,
|
||||
marginLeft: (props: ShiftButtonProps) =>
|
||||
props.direction === "right" ? (props.dense ? -8 : -4) : 0,
|
||||
color: (props: ShiftButtonProps) =>
|
||||
props.color
|
||||
? props.color
|
||||
: theme.palette.grey[isDarkTheme(theme) ? 200 : 700],
|
||||
},
|
||||
}));
|
||||
|
||||
function ShiftButton(props: ShiftButtonProps) {
|
||||
const classes = useShiftButtonStyles(props);
|
||||
return (
|
||||
<Button
|
||||
{...props}
|
||||
classes={{
|
||||
root: classes.root,
|
||||
label: classes.label,
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
{props.direction === "left" && (
|
||||
<ArrowLeftIcon classes={{ root: classes.iconRoot }} />
|
||||
)}
|
||||
{props.text}
|
||||
{props.direction === "right" && (
|
||||
<ArrowRightIcon classes={{ root: classes.iconRoot }} />
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
ShiftButton.defaultProps = {
|
||||
dense: false,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps)(MetricsFetchControls);
|
@ -27,8 +27,8 @@ function ProcessedTasksChart(props: Props) {
|
||||
<ResponsiveContainer>
|
||||
<BarChart data={props.data} maxBarSize={120}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="queue" />
|
||||
<YAxis />
|
||||
<XAxis dataKey="queue" stroke={theme.palette.text.secondary} />
|
||||
<YAxis stroke={theme.palette.text.secondary} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar
|
||||
|
108
ui/src/components/QueueMetricsChart.tsx
Normal file
108
ui/src/components/QueueMetricsChart.tsx
Normal file
@ -0,0 +1,108 @@
|
||||
import { useTheme } from "@material-ui/core/styles";
|
||||
import React from "react";
|
||||
import {
|
||||
LineChart,
|
||||
Line,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Metrics } from "../api";
|
||||
|
||||
interface Props {
|
||||
data: Metrics[];
|
||||
|
||||
// both startTime and endTime are in unix time (seconds)
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
|
||||
// (optional): Tick formatter function for YAxis
|
||||
yAxisTickFormatter?: (val: number) => string;
|
||||
}
|
||||
|
||||
// interface that rechart understands.
|
||||
interface ChartData {
|
||||
timestamp: number;
|
||||
[qname: string]: number;
|
||||
}
|
||||
|
||||
function toChartData(metrics: Metrics[]): ChartData[] {
|
||||
if (metrics.length === 0) {
|
||||
return [];
|
||||
}
|
||||
let byTimestamp: { [key: number]: ChartData } = {};
|
||||
for (let x of metrics) {
|
||||
for (let [ts, val] of x.values) {
|
||||
if (!byTimestamp[ts]) {
|
||||
byTimestamp[ts] = { timestamp: ts };
|
||||
}
|
||||
const qname = x.metric.queue;
|
||||
if (qname) {
|
||||
byTimestamp[ts][qname] = parseFloat(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
return Object.values(byTimestamp);
|
||||
}
|
||||
|
||||
const lineColors = [
|
||||
"#2085ec",
|
||||
"#72b4eb",
|
||||
"#0a417a",
|
||||
"#8464a0",
|
||||
"#cea9bc",
|
||||
"#323232",
|
||||
];
|
||||
|
||||
function QueueMetricsChart(props: Props) {
|
||||
const theme = useTheme();
|
||||
|
||||
const data = toChartData(props.data);
|
||||
const keys = props.data.map((x) => x.metric.queue);
|
||||
return (
|
||||
<ResponsiveContainer height={260}>
|
||||
<LineChart data={data}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis
|
||||
minTickGap={10}
|
||||
dataKey="timestamp"
|
||||
domain={[props.startTime, props.endTime]}
|
||||
tickFormatter={(timestamp: number) =>
|
||||
new Date(timestamp * 1000).toLocaleTimeString()
|
||||
}
|
||||
type="number"
|
||||
scale="time"
|
||||
stroke={theme.palette.text.secondary}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={props.yAxisTickFormatter}
|
||||
stroke={theme.palette.text.secondary}
|
||||
/>
|
||||
<Tooltip
|
||||
labelFormatter={(timestamp: number) => {
|
||||
return new Date(timestamp * 1000).toLocaleTimeString();
|
||||
}}
|
||||
/>
|
||||
<Legend />
|
||||
{keys.map((key, idx) => (
|
||||
<Line
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey={key}
|
||||
stroke={lineColors[idx % lineColors.length]}
|
||||
dot={false}
|
||||
/>
|
||||
))}
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
);
|
||||
}
|
||||
|
||||
QueueMetricsChart.defaultProps = {
|
||||
yAxisTickFormatter: (val: number) => val.toString(),
|
||||
};
|
||||
|
||||
export default QueueMetricsChart;
|
@ -10,6 +10,7 @@ import {
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import { useTheme } from "@material-ui/core/styles";
|
||||
import { queueDetailsPath } from "../paths";
|
||||
|
||||
interface Props {
|
||||
@ -27,6 +28,7 @@ interface TaskBreakdown {
|
||||
}
|
||||
|
||||
function QueueSizeChart(props: Props) {
|
||||
const theme = useTheme();
|
||||
const handleClick = (params: { activeLabel?: string } | null) => {
|
||||
const allQueues = props.data.map((b) => b.queue);
|
||||
if (
|
||||
@ -47,8 +49,8 @@ function QueueSizeChart(props: Props) {
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="queue" />
|
||||
<YAxis />
|
||||
<XAxis dataKey="queue" stroke={theme.palette.text.secondary} />
|
||||
<YAxis stroke={theme.palette.text.secondary} />
|
||||
<Tooltip />
|
||||
<Legend />
|
||||
<Bar dataKey="active" stackId="a" fill="#1967d2" />
|
||||
|
6
ui/src/global.d.ts
vendored
6
ui/src/global.d.ts
vendored
@ -2,4 +2,8 @@ interface Window {
|
||||
// Root URL path for asynqmon app.
|
||||
// ROOT_PATH should not have the tailing slash.
|
||||
ROOT_PATH: string;
|
||||
}
|
||||
|
||||
// Prometheus server address to query time series data.
|
||||
// This field is set to empty string by default. Use this field only if it's set.
|
||||
PROMETHEUS_SERVER_ADDRESS: string;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
// usePolling repeatedly calls doFn with a fix time delay specified
|
||||
// by interval (in millisecond).
|
||||
@ -9,3 +10,9 @@ export function usePolling(doFn: () => void, interval: number) {
|
||||
return () => clearInterval(id);
|
||||
}, [interval, doFn]);
|
||||
}
|
||||
|
||||
// useQuery gets the URL search params from the current URL.
|
||||
export function useQuery(): URLSearchParams {
|
||||
const { search } = useLocation();
|
||||
return useMemo(() => new URLSearchParams(search), [search]);
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ export const paths = {
|
||||
QUEUE_DETAILS: `${window.ROOT_PATH}/queues/:qname`,
|
||||
REDIS: `${window.ROOT_PATH}/redis`,
|
||||
TASK_DETAILS: `${window.ROOT_PATH}/queues/:qname/tasks/:taskId`,
|
||||
METRICS: `${window.ROOT_PATH}/metrics`,
|
||||
};
|
||||
|
||||
/**************************************************************
|
||||
@ -35,4 +36,4 @@ export interface QueueDetailsRouteParams {
|
||||
export interface TaskDetailsRouteParams {
|
||||
qname: string;
|
||||
taskId: string;
|
||||
}
|
||||
}
|
||||
|
49
ui/src/reducers/metricsReducer.ts
Normal file
49
ui/src/reducers/metricsReducer.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import {
|
||||
GET_METRICS_BEGIN,
|
||||
GET_METRICS_ERROR,
|
||||
GET_METRICS_SUCCESS,
|
||||
MetricsActionTypes,
|
||||
} from "../actions/metricsActions";
|
||||
import { MetricsResponse } from "../api";
|
||||
|
||||
interface MetricsState {
|
||||
loading: boolean;
|
||||
error: string;
|
||||
data: MetricsResponse | null;
|
||||
}
|
||||
|
||||
const initialState: MetricsState = {
|
||||
loading: false,
|
||||
error: "",
|
||||
data: null,
|
||||
};
|
||||
|
||||
export default function metricsReducer(
|
||||
state = initialState,
|
||||
action: MetricsActionTypes
|
||||
): MetricsState {
|
||||
switch (action.type) {
|
||||
case GET_METRICS_BEGIN:
|
||||
return {
|
||||
...state,
|
||||
loading: true,
|
||||
};
|
||||
|
||||
case GET_METRICS_ERROR:
|
||||
return {
|
||||
...state,
|
||||
loading: false,
|
||||
error: action.error,
|
||||
};
|
||||
|
||||
case GET_METRICS_SUCCESS:
|
||||
return {
|
||||
loading: false,
|
||||
error: "",
|
||||
data: action.payload,
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import schedulerEntriesReducer from "./reducers/schedulerEntriesReducer";
|
||||
import snackbarReducer from "./reducers/snackbarReducer";
|
||||
import queueStatsReducer from "./reducers/queueStatsReducer";
|
||||
import redisInfoReducer from "./reducers/redisInfoReducer";
|
||||
import metricsReducer from "./reducers/metricsReducer";
|
||||
import { loadState } from "./localStorage";
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
@ -18,6 +19,7 @@ const rootReducer = combineReducers({
|
||||
snackbar: snackbarReducer,
|
||||
queueStats: queueStatsReducer,
|
||||
redis: redisInfoReducer,
|
||||
metrics: metricsReducer,
|
||||
});
|
||||
|
||||
const preloadedState = loadState();
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createMuiTheme, Theme } from "@material-ui/core/styles";
|
||||
import { createTheme, Theme } from "@material-ui/core/styles";
|
||||
import { ThemePreference } from "./reducers/settingsReducer";
|
||||
import useMediaQuery from "@material-ui/core/useMediaQuery";
|
||||
|
||||
@ -9,7 +9,7 @@ export function useTheme(themePreference: ThemePreference): Theme {
|
||||
} else if (themePreference === ThemePreference.Never) {
|
||||
prefersDarkMode = false;
|
||||
}
|
||||
return createMuiTheme({
|
||||
return createTheme({
|
||||
// Got color palette from https://htmlcolors.com/palette/31/stripe
|
||||
palette: {
|
||||
primary: {
|
||||
|
@ -74,8 +74,8 @@ export function timeAgo(timestamp: string): string {
|
||||
}
|
||||
|
||||
export function timeAgoUnix(unixtime: number): string {
|
||||
if (unixtime === 0) {
|
||||
return ""
|
||||
if (unixtime === 0) {
|
||||
return "";
|
||||
}
|
||||
const duration = durationBetween(Date.now(), unixtime * 1000);
|
||||
return stringifyDuration(duration) + " ago";
|
||||
@ -103,14 +103,12 @@ export function percentage(numerator: number, denominator: number): string {
|
||||
return `${perc} %`;
|
||||
}
|
||||
|
||||
|
||||
export function isJsonPayload(p: string) {
|
||||
try {
|
||||
JSON.parse(p);
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -118,6 +116,31 @@ export function prettifyPayload(p: string) {
|
||||
if (isJsonPayload(p)) {
|
||||
return JSON.stringify(JSON.parse(p), null, 2);
|
||||
}
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
// Returns the number of seconds elapsed since January 1, 1970 00:00:00 UTC.
|
||||
export function currentUnixtime(): number {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
const durationRegex = /([0-9]*(\.[0-9]*)?)[s|m|h]/;
|
||||
// Parses the given string and returns the number of seconds if the input is valid.
|
||||
// Otherwise, it throws an error
|
||||
// Supported time units are "s", "m", "h"
|
||||
export function parseDuration(s: string): number {
|
||||
if (!durationRegex.test(s)) {
|
||||
throw new Error("invalid duration");
|
||||
}
|
||||
const val = parseFloat(s.slice(0, -1));
|
||||
switch (s.slice(-1)) {
|
||||
case "s":
|
||||
return val;
|
||||
case "m":
|
||||
return val * 60;
|
||||
case "h":
|
||||
return val * 60 * 60;
|
||||
default:
|
||||
throw new Error("invalid duration unit");
|
||||
}
|
||||
}
|
||||
|
329
ui/src/views/MetricsView.tsx
Normal file
329
ui/src/views/MetricsView.tsx
Normal file
@ -0,0 +1,329 @@
|
||||
import React from "react";
|
||||
import { connect, ConnectedProps } from "react-redux";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import queryString from "query-string";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Container from "@material-ui/core/Container";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
import WarningIcon from "@material-ui/icons/Warning";
|
||||
import InfoIcon from "@material-ui/icons/Info";
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { getMetricsAsync } from "../actions/metricsActions";
|
||||
import { listQueuesAsync } from "../actions/queuesActions";
|
||||
import { AppState } from "../store";
|
||||
import QueueMetricsChart from "../components/QueueMetricsChart";
|
||||
import Tooltip from "../components/Tooltip";
|
||||
import { currentUnixtime } from "../utils";
|
||||
import MetricsFetchControls from "../components/MetricsFetchControls";
|
||||
import { useQuery } from "../hooks";
|
||||
import { PrometheusMetricsResponse } from "../api";
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
container: {
|
||||
marginTop: 30,
|
||||
paddingTop: theme.spacing(4),
|
||||
paddingBottom: theme.spacing(4),
|
||||
},
|
||||
controlsContainer: {
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
position: "fixed",
|
||||
background: theme.palette.background.paper,
|
||||
zIndex: theme.zIndex.appBar,
|
||||
right: 0,
|
||||
top: 64, // app-bar height
|
||||
width: "100%",
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
chartInfo: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
infoIcon: {
|
||||
marginLeft: theme.spacing(1),
|
||||
color: theme.palette.grey[500],
|
||||
cursor: "pointer",
|
||||
},
|
||||
errorMessage: {
|
||||
marginLeft: "auto",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
},
|
||||
warningIcon: {
|
||||
color: "#ff6700",
|
||||
marginRight: 6,
|
||||
},
|
||||
}));
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
loading: state.metrics.loading,
|
||||
error: state.metrics.error,
|
||||
data: state.metrics.data,
|
||||
pollInterval: state.settings.pollInterval,
|
||||
queues: state.queues.data.map((q) => q.name),
|
||||
};
|
||||
}
|
||||
|
||||
const connector = connect(mapStateToProps, {
|
||||
getMetricsAsync,
|
||||
listQueuesAsync,
|
||||
});
|
||||
type Props = ConnectedProps<typeof connector>;
|
||||
|
||||
const ENDTIME_URL_PARAM_KEY = "end";
|
||||
const DURATION_URL_PARAM_KEY = "duration";
|
||||
|
||||
function MetricsView(props: Props) {
|
||||
const classes = useStyles();
|
||||
const history = useHistory();
|
||||
const query = useQuery();
|
||||
|
||||
const endTimeStr = query.get(ENDTIME_URL_PARAM_KEY);
|
||||
const endTime = endTimeStr ? parseFloat(endTimeStr) : currentUnixtime(); // default to now
|
||||
|
||||
const durationStr = query.get(DURATION_URL_PARAM_KEY);
|
||||
const duration = durationStr ? parseFloat(durationStr) : 60 * 60; // default to 1h
|
||||
|
||||
const { pollInterval, getMetricsAsync, listQueuesAsync, data } = props;
|
||||
|
||||
const [endTimeSec, setEndTimeSec] = React.useState(endTime);
|
||||
const [durationSec, setDurationSec] = React.useState(duration);
|
||||
const [selectedQueues, setSelectedQueues] = React.useState<string[]>([]);
|
||||
|
||||
const handleEndTimeChange = (endTime: number, isEndTimeFixed: boolean) => {
|
||||
const urlQuery = isEndTimeFixed
|
||||
? {
|
||||
[ENDTIME_URL_PARAM_KEY]: endTime,
|
||||
[DURATION_URL_PARAM_KEY]: durationSec,
|
||||
}
|
||||
: {
|
||||
[DURATION_URL_PARAM_KEY]: durationSec,
|
||||
};
|
||||
history.push({
|
||||
...history.location,
|
||||
search: queryString.stringify(urlQuery),
|
||||
});
|
||||
setEndTimeSec(endTime);
|
||||
};
|
||||
|
||||
const handleDurationChange = (duration: number, isEndTimeFixed: boolean) => {
|
||||
const urlQuery = isEndTimeFixed
|
||||
? {
|
||||
[ENDTIME_URL_PARAM_KEY]: endTimeSec,
|
||||
[DURATION_URL_PARAM_KEY]: duration,
|
||||
}
|
||||
: {
|
||||
[DURATION_URL_PARAM_KEY]: duration,
|
||||
};
|
||||
history.push({
|
||||
...history.location,
|
||||
search: queryString.stringify(urlQuery),
|
||||
});
|
||||
setDurationSec(duration);
|
||||
};
|
||||
|
||||
const handleAddQueue = (qname: string) => {
|
||||
if (selectedQueues.includes(qname)) {
|
||||
return;
|
||||
}
|
||||
setSelectedQueues(selectedQueues.concat(qname));
|
||||
};
|
||||
|
||||
const handleRemoveQueue = (qname: string) => {
|
||||
if (selectedQueues.length === 1) {
|
||||
return; // ensure that selected queues doesn't go down to zero once user selected
|
||||
}
|
||||
if (selectedQueues.length === 0) {
|
||||
// when user first select filter (remove once of the queues),
|
||||
// we need to lazily initialize the selectedQueues with the rest (all queues but the selected one).
|
||||
setSelectedQueues(props.queues.filter((q) => q !== qname));
|
||||
return;
|
||||
}
|
||||
setSelectedQueues(selectedQueues.filter((q) => q !== qname));
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
listQueuesAsync();
|
||||
}, [listQueuesAsync]);
|
||||
|
||||
React.useEffect(() => {
|
||||
getMetricsAsync(endTimeSec, durationSec, selectedQueues);
|
||||
}, [pollInterval, getMetricsAsync, durationSec, endTimeSec, selectedQueues]);
|
||||
|
||||
return (
|
||||
<Container maxWidth="lg" className={classes.container}>
|
||||
<div className={classes.controlsContainer}>
|
||||
<MetricsFetchControls
|
||||
endTimeSec={endTimeSec}
|
||||
onEndTimeChange={handleEndTimeChange}
|
||||
durationSec={durationSec}
|
||||
onDurationChange={handleDurationChange}
|
||||
queues={props.queues}
|
||||
selectedQueues={
|
||||
// If none are selected (e.g. initial state), no filters should apply.
|
||||
selectedQueues.length === 0 ? props.queues : selectedQueues
|
||||
}
|
||||
addQueue={handleAddQueue}
|
||||
removeQueue={handleRemoveQueue}
|
||||
/>
|
||||
</div>
|
||||
<Grid container spacing={3}>
|
||||
{data?.tasks_processed_per_second && (
|
||||
<Grid item xs={12}>
|
||||
<ChartRow
|
||||
title="Tasks Processed"
|
||||
description="Number of tasks processed (both succeeded and failed) per second."
|
||||
metrics={data.tasks_processed_per_second}
|
||||
endTime={endTimeSec}
|
||||
startTime={endTimeSec - durationSec}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{data?.tasks_failed_per_second && (
|
||||
<Grid item xs={12}>
|
||||
<ChartRow
|
||||
title="Tasks Failed"
|
||||
description="Number of tasks failed per second."
|
||||
metrics={data.tasks_failed_per_second}
|
||||
endTime={endTimeSec}
|
||||
startTime={endTimeSec - durationSec}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{data?.error_rate && (
|
||||
<Grid item xs={12}>
|
||||
<ChartRow
|
||||
title="Error Rate"
|
||||
description="Rate of task failures"
|
||||
metrics={data.error_rate}
|
||||
endTime={endTimeSec}
|
||||
startTime={endTimeSec - durationSec}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{data?.queue_size && (
|
||||
<Grid item xs={12}>
|
||||
<ChartRow
|
||||
title="Queue Size"
|
||||
description="Total number of tasks in a given queue."
|
||||
metrics={data.queue_size}
|
||||
endTime={endTimeSec}
|
||||
startTime={endTimeSec - durationSec}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{data?.queue_latency_seconds && (
|
||||
<Grid item xs={12}>
|
||||
<ChartRow
|
||||
title="Queue Latency"
|
||||
description="Latency of queue, measured by the oldest pending task in the queue."
|
||||
metrics={data.queue_latency_seconds}
|
||||
endTime={endTimeSec}
|
||||
startTime={endTimeSec - durationSec}
|
||||
yAxisTickFormatter={(val: number) => val + "s"}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{data?.queue_size && (
|
||||
<Grid item xs={12}>
|
||||
<ChartRow
|
||||
title="Queue Memory Usage (approx)"
|
||||
description="Memory usage by queue. Approximate value by sampling a few tasks in a queue."
|
||||
metrics={data.queue_memory_usage_approx_bytes}
|
||||
endTime={endTimeSec}
|
||||
startTime={endTimeSec - durationSec}
|
||||
yAxisTickFormatter={(val: number) => {
|
||||
try {
|
||||
return prettyBytes(val);
|
||||
} catch (error) {
|
||||
return val + "B";
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{data?.pending_tasks_by_queue && (
|
||||
<Grid item xs={12}>
|
||||
<ChartRow
|
||||
title="Pending Tasks"
|
||||
description="Number of pending tasks in a given queue."
|
||||
metrics={data.pending_tasks_by_queue}
|
||||
endTime={endTimeSec}
|
||||
startTime={endTimeSec - durationSec}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{data?.retry_tasks_by_queue && (
|
||||
<Grid item xs={12}>
|
||||
<ChartRow
|
||||
title="Retry Tasks"
|
||||
description="Number of retry tasks in a given queue."
|
||||
metrics={data.retry_tasks_by_queue}
|
||||
endTime={endTimeSec}
|
||||
startTime={endTimeSec - durationSec}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
{data?.archived_tasks_by_queue && (
|
||||
<Grid item xs={12}>
|
||||
<ChartRow
|
||||
title="Archived Tasks"
|
||||
description="Number of archived tasks in a given queue."
|
||||
metrics={data.archived_tasks_by_queue}
|
||||
endTime={endTimeSec}
|
||||
startTime={endTimeSec - durationSec}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default connector(MetricsView);
|
||||
|
||||
/******** Helper components ********/
|
||||
|
||||
interface ChartRowProps {
|
||||
title: string;
|
||||
description: string;
|
||||
metrics: PrometheusMetricsResponse;
|
||||
endTime: number;
|
||||
startTime: number;
|
||||
yAxisTickFormatter?: (val: number) => string;
|
||||
}
|
||||
|
||||
function ChartRow(props: ChartRowProps) {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<>
|
||||
<div className={classes.chartInfo}>
|
||||
<Typography color="textPrimary">{props.title}</Typography>
|
||||
<Tooltip title={<div>{props.description}</div>}>
|
||||
<InfoIcon fontSize="small" className={classes.infoIcon} />
|
||||
</Tooltip>
|
||||
{props.metrics.status === "error" && (
|
||||
<div className={classes.errorMessage}>
|
||||
<WarningIcon fontSize="small" className={classes.warningIcon} />
|
||||
<Typography color="textSecondary">
|
||||
Failed to get metrics data: {props.metrics.error}
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<QueueMetricsChart
|
||||
data={
|
||||
props.metrics.status === "error"
|
||||
? []
|
||||
: props.metrics.data?.result || []
|
||||
}
|
||||
endTime={props.endTime}
|
||||
startTime={props.startTime}
|
||||
yAxisTickFormatter={props.yAxisTickFormatter}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -6,10 +6,11 @@ import Grid from "@material-ui/core/Grid";
|
||||
import TasksTable from "../components/TasksTable";
|
||||
import QueueInfoBanner from "../components/QueueInfoBanner";
|
||||
import QueueBreadCrumb from "../components/QueueBreadcrumb";
|
||||
import { useParams, useLocation } from "react-router-dom";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { listQueuesAsync } from "../actions/queuesActions";
|
||||
import { AppState } from "../store";
|
||||
import { QueueDetailsRouteParams } from "../paths";
|
||||
import { useQuery } from "../hooks";
|
||||
|
||||
function mapStateToProps(state: AppState) {
|
||||
return {
|
||||
@ -34,10 +35,6 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
function useQuery(): URLSearchParams {
|
||||
return new URLSearchParams(useLocation().search);
|
||||
}
|
||||
|
||||
const validStatus = [
|
||||
"active",
|
||||
"pending",
|
||||
|
Loading…
Reference in New Issue
Block a user