github.com/wfusion/gofusion@v1.1.14/common/infra/asynq/asynqmon/handler.go (about) 1 package asynqmon 2 3 import ( 4 "embed" 5 "errors" 6 "fmt" 7 "net/http" 8 "strings" 9 10 "github.com/gorilla/mux" 11 "github.com/redis/go-redis/v9" 12 13 "github.com/wfusion/gofusion/common/infra/asynq" 14 ) 15 16 // Options are used to configure HTTPHandler. 17 type Options struct { 18 // URL path the handler is responsible for. 19 // The path is used for the homepage of asynqmon, and every other page is rooted in this subtree. 20 // 21 // This field is optional. Default is "/". 22 RootPath string 23 24 // RedisConnOpt specifies the connection to a redis-server or redis-cluster. 25 // 26 // This field is required. 27 RedisConnOpt asynq.RedisConnOpt 28 29 // PayloadFormatter is used to convert payload bytes to string shown in the UI. 30 // 31 // This field is optional. 32 PayloadFormatter PayloadFormatter 33 34 // ResultFormatter is used to convert result bytes to string shown in the UI. 35 // 36 // This field is optional. 37 ResultFormatter ResultFormatter 38 39 // PrometheusAddress specifies the address of the Prometheus to connect to. 40 // 41 // This field is optional. If this field is set, asynqmon will query the Prometheus server 42 // to get the time series data about queue metrics and show them in the web UI. 43 PrometheusAddress string 44 45 // Set ReadOnly to true to restrict user to view-only mode. 46 ReadOnly bool 47 } 48 49 // HTTPHandler is a http.Handler for asynqmon application. 50 type HTTPHandler struct { 51 router *mux.Router 52 closers []func() error 53 rootPath string // the value should not have the trailing slash 54 } 55 56 func (h *HTTPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 57 h.router.ServeHTTP(w, r) 58 } 59 60 // New creates a HTTPHandler with the given options. 61 func New(opts Options) *HTTPHandler { 62 if opts.RedisConnOpt == nil { 63 panic("asynqmon.New: RedisConnOpt field is required") 64 } 65 rc, ok := opts.RedisConnOpt.MakeRedisClient().(redis.UniversalClient) 66 if !ok { 67 panic(fmt.Sprintf("asnyqmon.New: unsupported RedisConnOpt type %T", opts.RedisConnOpt)) 68 } 69 i := asynq.NewInspector(opts.RedisConnOpt) 70 71 // Make sure that RootPath starts with a slash if provided. 72 if opts.RootPath != "" && !strings.HasPrefix(opts.RootPath, "/") { 73 panic(errors.New("asynqmon.New: RootPath must start with a slash")) 74 } 75 // Remove tailing slash from RootPath. 76 opts.RootPath = strings.TrimSuffix(opts.RootPath, "/") 77 78 return &HTTPHandler{ 79 router: muxRouter(opts, rc, i), 80 closers: []func() error{rc.Close, i.Close}, 81 rootPath: opts.RootPath, 82 } 83 } 84 85 // Close closes connections to redis. 86 func (h *HTTPHandler) Close() error { 87 for _, f := range h.closers { 88 if err := f(); err != nil { 89 return err 90 } 91 } 92 return nil 93 } 94 95 // RootPath returns the root URL path used for asynqmon application. 96 // Returned path string does not have the trailing slash. 97 func (h *HTTPHandler) RootPath() string { 98 return h.rootPath 99 } 100 101 //go:embed ui/build/* 102 var staticContents embed.FS 103 104 // muxRouter 105 //nolint: revive // http api register issue 106 func muxRouter(opts Options, rc redis.UniversalClient, inspector *asynq.Inspector) *mux.Router { 107 router := mux.NewRouter().PathPrefix(opts.RootPath).Subrouter() 108 109 var payloadFmt PayloadFormatter = DefaultPayloadFormatter 110 if opts.PayloadFormatter != nil { 111 payloadFmt = opts.PayloadFormatter 112 } 113 114 var resultFmt ResultFormatter = DefaultResultFormatter 115 if opts.ResultFormatter != nil { 116 resultFmt = opts.ResultFormatter 117 } 118 119 api := router.PathPrefix("/api").Subrouter() 120 121 // Queue endpoints. 122 api.HandleFunc("/queues", newListQueuesHandlerFunc(inspector)).Methods("GET") 123 api.HandleFunc("/queues/{qname}", newGetQueueHandlerFunc(inspector)).Methods("GET") 124 api.HandleFunc("/queues/{qname}", newDeleteQueueHandlerFunc(inspector)).Methods("DELETE") 125 api.HandleFunc("/queues/{qname}:pause", newPauseQueueHandlerFunc(inspector)).Methods("POST") 126 api.HandleFunc("/queues/{qname}:resume", newResumeQueueHandlerFunc(inspector)).Methods("POST") 127 128 // Queue Historical Stats endpoint. 129 api.HandleFunc("/queue_stats", newListQueueStatsHandlerFunc(inspector)).Methods("GET") 130 131 // Task endpoints. 132 api.HandleFunc("/queues/{qname}/active_tasks", newListActiveTasksHandlerFunc(inspector, payloadFmt)).Methods("GET") 133 api.HandleFunc("/queues/{qname}/active_tasks/{task_id}:cancel", newCancelActiveTaskHandlerFunc(inspector)).Methods("POST") 134 api.HandleFunc("/queues/{qname}/active_tasks:cancel_all", newCancelAllActiveTasksHandlerFunc(inspector)).Methods("POST") 135 api.HandleFunc("/queues/{qname}/active_tasks:batch_cancel", newBatchCancelActiveTasksHandlerFunc(inspector)).Methods("POST") 136 137 api.HandleFunc("/queues/{qname}/pending_tasks", newListPendingTasksHandlerFunc(inspector, payloadFmt)).Methods("GET") 138 api.HandleFunc("/queues/{qname}/pending_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") 139 api.HandleFunc("/queues/{qname}/pending_tasks:delete_all", newDeleteAllPendingTasksHandlerFunc(inspector)).Methods("DELETE") 140 api.HandleFunc("/queues/{qname}/pending_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") 141 api.HandleFunc("/queues/{qname}/pending_tasks/{task_id}:archive", newArchiveTaskHandlerFunc(inspector)).Methods("POST") 142 api.HandleFunc("/queues/{qname}/pending_tasks:archive_all", newArchiveAllPendingTasksHandlerFunc(inspector)).Methods("POST") 143 api.HandleFunc("/queues/{qname}/pending_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST") 144 145 api.HandleFunc("/queues/{qname}/scheduled_tasks", newListScheduledTasksHandlerFunc(inspector, payloadFmt)).Methods("GET") 146 api.HandleFunc("/queues/{qname}/scheduled_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") 147 api.HandleFunc("/queues/{qname}/scheduled_tasks:delete_all", newDeleteAllScheduledTasksHandlerFunc(inspector)).Methods("DELETE") 148 api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") 149 api.HandleFunc("/queues/{qname}/scheduled_tasks/{task_id}:run", newRunTaskHandlerFunc(inspector)).Methods("POST") 150 api.HandleFunc("/queues/{qname}/scheduled_tasks:run_all", newRunAllScheduledTasksHandlerFunc(inspector)).Methods("POST") 151 api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST") 152 api.HandleFunc("/queues/{qname}/scheduled_tasks/{task_id}:archive", newArchiveTaskHandlerFunc(inspector)).Methods("POST") 153 api.HandleFunc("/queues/{qname}/scheduled_tasks:archive_all", newArchiveAllScheduledTasksHandlerFunc(inspector)).Methods("POST") 154 api.HandleFunc("/queues/{qname}/scheduled_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST") 155 156 api.HandleFunc("/queues/{qname}/retry_tasks", newListRetryTasksHandlerFunc(inspector, payloadFmt)).Methods("GET") 157 api.HandleFunc("/queues/{qname}/retry_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") 158 api.HandleFunc("/queues/{qname}/retry_tasks:delete_all", newDeleteAllRetryTasksHandlerFunc(inspector)).Methods("DELETE") 159 api.HandleFunc("/queues/{qname}/retry_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") 160 api.HandleFunc("/queues/{qname}/retry_tasks/{task_id}:run", newRunTaskHandlerFunc(inspector)).Methods("POST") 161 api.HandleFunc("/queues/{qname}/retry_tasks:run_all", newRunAllRetryTasksHandlerFunc(inspector)).Methods("POST") 162 api.HandleFunc("/queues/{qname}/retry_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST") 163 api.HandleFunc("/queues/{qname}/retry_tasks/{task_id}:archive", newArchiveTaskHandlerFunc(inspector)).Methods("POST") 164 api.HandleFunc("/queues/{qname}/retry_tasks:archive_all", newArchiveAllRetryTasksHandlerFunc(inspector)).Methods("POST") 165 api.HandleFunc("/queues/{qname}/retry_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST") 166 167 api.HandleFunc("/queues/{qname}/archived_tasks", newListArchivedTasksHandlerFunc(inspector, payloadFmt)).Methods("GET") 168 api.HandleFunc("/queues/{qname}/archived_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") 169 api.HandleFunc("/queues/{qname}/archived_tasks:delete_all", newDeleteAllArchivedTasksHandlerFunc(inspector)).Methods("DELETE") 170 api.HandleFunc("/queues/{qname}/archived_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") 171 api.HandleFunc("/queues/{qname}/archived_tasks/{task_id}:run", newRunTaskHandlerFunc(inspector)).Methods("POST") 172 api.HandleFunc("/queues/{qname}/archived_tasks:run_all", newRunAllArchivedTasksHandlerFunc(inspector)).Methods("POST") 173 api.HandleFunc("/queues/{qname}/archived_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST") 174 175 api.HandleFunc("/queues/{qname}/completed_tasks", newListCompletedTasksHandlerFunc(inspector, payloadFmt, resultFmt)).Methods("GET") 176 api.HandleFunc("/queues/{qname}/completed_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") 177 api.HandleFunc("/queues/{qname}/completed_tasks:delete_all", newDeleteAllCompletedTasksHandlerFunc(inspector)).Methods("DELETE") 178 api.HandleFunc("/queues/{qname}/completed_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") 179 180 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks", newListAggregatingTasksHandlerFunc(inspector, payloadFmt)).Methods("GET") 181 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks/{task_id}", newDeleteTaskHandlerFunc(inspector)).Methods("DELETE") 182 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:delete_all", newDeleteAllAggregatingTasksHandlerFunc(inspector)).Methods("DELETE") 183 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:batch_delete", newBatchDeleteTasksHandlerFunc(inspector)).Methods("POST") 184 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks/{task_id}:run", newRunTaskHandlerFunc(inspector)).Methods("POST") 185 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:run_all", newRunAllAggregatingTasksHandlerFunc(inspector)).Methods("POST") 186 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:batch_run", newBatchRunTasksHandlerFunc(inspector)).Methods("POST") 187 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks/{task_id}:archive", newArchiveTaskHandlerFunc(inspector)).Methods("POST") 188 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:archive_all", newArchiveAllAggregatingTasksHandlerFunc(inspector)).Methods("POST") 189 api.HandleFunc("/queues/{qname}/groups/{gname}/aggregating_tasks:batch_archive", newBatchArchiveTasksHandlerFunc(inspector)).Methods("POST") 190 191 api.HandleFunc("/queues/{qname}/tasks/{task_id}", newGetTaskHandlerFunc(inspector, payloadFmt, resultFmt)).Methods("GET") 192 193 // Groups endponts 194 api.HandleFunc("/queues/{qname}/groups", newListGroupsHandlerFunc(inspector)).Methods("GET") 195 196 // Servers endpoints. 197 api.HandleFunc("/servers", newListServersHandlerFunc(inspector, payloadFmt)).Methods("GET") 198 199 // Scheduler Entry endpoints. 200 api.HandleFunc("/scheduler_entries", newListSchedulerEntriesHandlerFunc(inspector, payloadFmt)).Methods("GET") 201 api.HandleFunc("/scheduler_entries/{entry_id}/enqueue_events", newListSchedulerEnqueueEventsHandlerFunc(inspector)).Methods("GET") 202 203 // Redis info endpoint. 204 switch c := rc.(type) { 205 case *redis.ClusterClient: 206 api.HandleFunc("/redis_info", newRedisClusterInfoHandlerFunc(c, inspector)).Methods("GET") 207 case *redis.Client: 208 api.HandleFunc("/redis_info", newRedisInfoHandlerFunc(c)).Methods("GET") 209 } 210 211 // Time series metrics endpoints. 212 api.HandleFunc("/metrics", newGetMetricsHandlerFunc(http.DefaultClient, opts.PrometheusAddress)).Methods("GET") 213 214 // Restrict APIs when running in read-only mode. 215 if opts.ReadOnly { 216 api.Use(restrictToReadOnly) 217 } 218 219 // Everything else, route to uiAssetsHandler. 220 router.NotFoundHandler = &uiAssetsHandler{ 221 rootPath: opts.RootPath, 222 contents: staticContents, 223 staticDirPath: "ui/build", 224 indexFileName: "index.html", 225 prometheusAddr: opts.PrometheusAddress, 226 readOnly: opts.ReadOnly, 227 } 228 229 return router 230 } 231 232 // restrictToReadOnly is a middleware function to restrict users to perform only GET requests. 233 func restrictToReadOnly(h http.Handler) http.Handler { 234 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 235 if r.Method != "GET" && r.Method != "" { 236 http.Error( 237 w, 238 fmt.Sprintf("API Server is running in read-only mode: %s request is not allowed", r.Method), 239 http.StatusMethodNotAllowed, 240 ) 241 return 242 } 243 h.ServeHTTP(w, r) 244 }) 245 }