2021-09-18 20:25:59 +08:00
package main
import (
"crypto/tls"
"flag"
"fmt"
"log"
"net/http"
2021-12-14 08:35:10 +08:00
"os"
"strconv"
2021-09-18 20:25:59 +08:00
"strings"
"time"
2021-10-01 02:26:01 +08:00
"github.com/go-redis/redis/v8"
2021-09-18 20:25:59 +08:00
"github.com/hibiken/asynq"
2021-12-19 23:30:16 +08:00
"github.com/hibiken/asynq/x/metrics"
2021-09-18 22:10:42 +08:00
"github.com/hibiken/asynqmon"
2021-12-19 23:30:16 +08:00
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/cors"
2021-09-18 20:25:59 +08:00
)
// Command-line flags
var (
2021-12-19 23:30:16 +08:00
flagPort int
flagRedisAddr string
flagRedisDB int
flagRedisPassword string
flagRedisTLS string
flagRedisURL string
flagRedisInsecureTLS bool
flagRedisClusterNodes string
flagMaxPayloadLength int
flagMaxResultLength int
flagEnableMetricsExporter bool
flagPrometheusServerAddr string
2022-02-27 04:19:15 +08:00
flagReadOnly bool
2021-09-18 20:25:59 +08:00
)
func init ( ) {
2021-12-14 08:35:10 +08:00
flag . IntVar ( & flagPort , "port" , getEnvOrDefaultInt ( "PORT" , 8080 ) , "port number to use for web ui server" )
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" )
2021-12-19 23:30:16 +08:00
flag . StringVar ( & flagRedisTLS , "redis-tls" , getEnvDefaultString ( "REDIS_TLS" , "" ) , "server name for TLS validation used when connecting to redis server" )
2021-12-14 08:35:10 +08:00
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" )
2021-12-19 23:30:16 +08:00
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" )
2022-02-27 04:19:15 +08:00
flag . BoolVar ( & flagReadOnly , "read-only" , getEnvOrDefaultBool ( "READ_ONLY" , false ) , "restrict to read-only mode" )
2021-09-18 20:25:59 +08:00
}
2021-10-10 21:33:38 +08:00
// TODO: Write test and refactor this code.
// IDEA: https://eli.thegreenplace.net/2020/testing-flag-parsing-in-go-programs/
func getRedisOptionsFromFlags ( ) ( asynq . RedisConnOpt , error ) {
2021-09-18 20:25:59 +08:00
var opts redis . UniversalOptions
if flagRedisClusterNodes != "" {
opts . Addrs = strings . Split ( flagRedisClusterNodes , "," )
opts . Password = flagRedisPassword
} else {
if flagRedisURL != "" {
res , err := redis . ParseURL ( flagRedisURL )
if err != nil {
return nil , err
}
opts . Addrs = append ( opts . Addrs , res . Addr )
opts . DB = res . DB
opts . Password = res . Password
} else {
opts . Addrs = [ ] string { flagRedisAddr }
opts . DB = flagRedisDB
opts . Password = flagRedisPassword
}
}
if flagRedisTLS != "" {
opts . TLSConfig = & tls . Config { ServerName : flagRedisTLS }
}
if flagRedisInsecureTLS {
if opts . TLSConfig == nil {
opts . TLSConfig = & tls . Config { }
}
opts . TLSConfig . InsecureSkipVerify = true
}
2021-10-10 21:33:38 +08:00
if flagRedisClusterNodes != "" {
return asynq . RedisClusterClientOpt {
Addrs : opts . Addrs ,
Password : opts . Password ,
TLSConfig : opts . TLSConfig ,
} , nil
}
return asynq . RedisClientOpt {
Addr : opts . Addrs [ 0 ] ,
DB : opts . DB ,
Password : opts . Password ,
TLSConfig : opts . TLSConfig ,
} , nil
2021-09-18 20:25:59 +08:00
}
func main ( ) {
flag . Parse ( )
2021-10-10 21:33:38 +08:00
redisConnOpt , err := getRedisOptionsFromFlags ( )
2021-09-18 20:25:59 +08:00
if err != nil {
log . Fatal ( err )
}
2021-10-04 23:18:00 +08:00
h := asynqmon . New ( asynqmon . Options {
2021-12-19 23:30:16 +08:00
RedisConnOpt : redisConnOpt ,
PayloadFormatter : asynqmon . PayloadFormatterFunc ( formatPayload ) ,
ResultFormatter : asynqmon . ResultFormatterFunc ( formatResult ) ,
PrometheusAddress : flagPrometheusServerAddr ,
2022-02-27 04:19:15 +08:00
ReadOnly : flagReadOnly ,
2021-09-18 20:25:59 +08:00
} )
2021-10-04 23:18:00 +08:00
defer h . Close ( )
2021-09-18 20:25:59 +08:00
c := cors . New ( cors . Options {
AllowedMethods : [ ] string { "GET" , "POST" , "DELETE" } ,
} )
2021-12-19 23:30:16 +08:00
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 { } ) )
}
2021-09-18 20:25:59 +08:00
srv := & http . Server {
2021-12-19 23:30:16 +08:00
Handler : mux ,
2021-09-18 20:25:59 +08:00
Addr : fmt . Sprintf ( ":%d" , flagPort ) ,
WriteTimeout : 10 * time . Second ,
ReadTimeout : 10 * time . Second ,
}
fmt . Printf ( "Asynq Monitoring WebUI server is listening on port %d\n" , flagPort )
log . Fatal ( srv . ListenAndServe ( ) )
}
2021-10-21 22:11:46 +08:00
func formatPayload ( taskType string , payload [ ] byte ) string {
payloadStr := asynqmon . DefaultPayloadFormatter . FormatPayload ( taskType , payload )
return truncate ( payloadStr , flagMaxPayloadLength )
}
2021-11-07 06:23:10 +08:00
func formatResult ( taskType string , result [ ] byte ) string {
resultStr := asynqmon . DefaultResultFormatter . FormatResult ( taskType , result )
return truncate ( resultStr , flagMaxResultLength )
}
2021-10-21 22:11:46 +08:00
// truncates string s to limit length (in utf8).
func truncate ( s string , limit int ) string {
i := 0
for pos := range s {
if i == limit {
return s [ : pos ] + "…"
}
i ++
}
return s
}
2021-12-14 08:35:10 +08:00
func getEnvDefaultString ( key , def string ) string {
v := os . Getenv ( key )
2021-12-19 23:30:16 +08:00
if v == "" {
2021-12-14 08:35:10 +08:00
return def
}
return v
}
func getEnvOrDefaultInt ( key string , def int ) int {
v , err := strconv . Atoi ( os . Getenv ( key ) )
2021-12-19 23:30:16 +08:00
if err != nil {
2021-12-14 08:35:10 +08:00
return def
}
return v
}
func getEnvOrDefaultBool ( key string , def bool ) bool {
v , err := strconv . ParseBool ( os . Getenv ( key ) )
2021-12-19 23:30:16 +08:00
if err != nil {
2021-12-14 08:35:10 +08:00
return def
}
return v
2021-12-19 23:30:16 +08:00
}