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  }