github.com/astaxie/beego@v1.12.3/admin.go (about)

     1  // Copyright 2014 beego Author. All Rights Reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package beego
    16  
    17  import (
    18  	"bytes"
    19  	"encoding/json"
    20  	"fmt"
    21  	"net/http"
    22  	"os"
    23  	"reflect"
    24  	"strconv"
    25  	"text/template"
    26  	"time"
    27  
    28  	"github.com/prometheus/client_golang/prometheus/promhttp"
    29  
    30  	"github.com/astaxie/beego/grace"
    31  	"github.com/astaxie/beego/logs"
    32  	"github.com/astaxie/beego/toolbox"
    33  	"github.com/astaxie/beego/utils"
    34  )
    35  
    36  // BeeAdminApp is the default adminApp used by admin module.
    37  var beeAdminApp *adminApp
    38  
    39  // FilterMonitorFunc is default monitor filter when admin module is enable.
    40  // if this func returns, admin module records qps for this request by condition of this function logic.
    41  // usage:
    42  // 	func MyFilterMonitor(method, requestPath string, t time.Duration, pattern string, statusCode int) bool {
    43  //	 	if method == "POST" {
    44  //			return false
    45  //	 	}
    46  //	 	if t.Nanoseconds() < 100 {
    47  //			return false
    48  //	 	}
    49  //	 	if strings.HasPrefix(requestPath, "/astaxie") {
    50  //			return false
    51  //	 	}
    52  //	 	return true
    53  // 	}
    54  // 	beego.FilterMonitorFunc = MyFilterMonitor.
    55  var FilterMonitorFunc func(string, string, time.Duration, string, int) bool
    56  
    57  func init() {
    58  	beeAdminApp = &adminApp{
    59  		routers: make(map[string]http.HandlerFunc),
    60  	}
    61  	// keep in mind that all data should be html escaped to avoid XSS attack
    62  	beeAdminApp.Route("/", adminIndex)
    63  	beeAdminApp.Route("/qps", qpsIndex)
    64  	beeAdminApp.Route("/prof", profIndex)
    65  	beeAdminApp.Route("/healthcheck", healthcheck)
    66  	beeAdminApp.Route("/task", taskStatus)
    67  	beeAdminApp.Route("/listconf", listConf)
    68  	beeAdminApp.Route("/metrics", promhttp.Handler().ServeHTTP)
    69  	FilterMonitorFunc = func(string, string, time.Duration, string, int) bool { return true }
    70  }
    71  
    72  // AdminIndex is the default http.Handler for admin module.
    73  // it matches url pattern "/".
    74  func adminIndex(rw http.ResponseWriter, _ *http.Request) {
    75  	writeTemplate(rw, map[interface{}]interface{}{}, indexTpl, defaultScriptsTpl)
    76  }
    77  
    78  // QpsIndex is the http.Handler for writing qps statistics map result info in http.ResponseWriter.
    79  // it's registered with url pattern "/qps" in admin module.
    80  func qpsIndex(rw http.ResponseWriter, _ *http.Request) {
    81  	data := make(map[interface{}]interface{})
    82  	data["Content"] = toolbox.StatisticsMap.GetMap()
    83  
    84  	// do html escape before display path, avoid xss
    85  	if content, ok := (data["Content"]).(M); ok {
    86  		if resultLists, ok := (content["Data"]).([][]string); ok {
    87  			for i := range resultLists {
    88  				if len(resultLists[i]) > 0 {
    89  					resultLists[i][0] = template.HTMLEscapeString(resultLists[i][0])
    90  				}
    91  			}
    92  		}
    93  	}
    94  
    95  	writeTemplate(rw, data, qpsTpl, defaultScriptsTpl)
    96  }
    97  
    98  // ListConf is the http.Handler of displaying all beego configuration values as key/value pair.
    99  // it's registered with url pattern "/listconf" in admin module.
   100  func listConf(rw http.ResponseWriter, r *http.Request) {
   101  	r.ParseForm()
   102  	command := r.Form.Get("command")
   103  	if command == "" {
   104  		rw.Write([]byte("command not support"))
   105  		return
   106  	}
   107  
   108  	data := make(map[interface{}]interface{})
   109  	switch command {
   110  	case "conf":
   111  		m := make(M)
   112  		list("BConfig", BConfig, m)
   113  		m["AppConfigPath"] = template.HTMLEscapeString(appConfigPath)
   114  		m["AppConfigProvider"] = template.HTMLEscapeString(appConfigProvider)
   115  		tmpl := template.Must(template.New("dashboard").Parse(dashboardTpl))
   116  		tmpl = template.Must(tmpl.Parse(configTpl))
   117  		tmpl = template.Must(tmpl.Parse(defaultScriptsTpl))
   118  
   119  		data["Content"] = m
   120  
   121  		tmpl.Execute(rw, data)
   122  
   123  	case "router":
   124  		content := PrintTree()
   125  		content["Fields"] = []string{
   126  			"Router Pattern",
   127  			"Methods",
   128  			"Controller",
   129  		}
   130  		data["Content"] = content
   131  		data["Title"] = "Routers"
   132  		writeTemplate(rw, data, routerAndFilterTpl, defaultScriptsTpl)
   133  	case "filter":
   134  		var (
   135  			content = M{
   136  				"Fields": []string{
   137  					"Router Pattern",
   138  					"Filter Function",
   139  				},
   140  			}
   141  			filterTypes    = []string{}
   142  			filterTypeData = make(M)
   143  		)
   144  
   145  		if BeeApp.Handlers.enableFilter {
   146  			var filterType string
   147  			for k, fr := range map[int]string{
   148  				BeforeStatic: "Before Static",
   149  				BeforeRouter: "Before Router",
   150  				BeforeExec:   "Before Exec",
   151  				AfterExec:    "After Exec",
   152  				FinishRouter: "Finish Router"} {
   153  				if bf := BeeApp.Handlers.filters[k]; len(bf) > 0 {
   154  					filterType = fr
   155  					filterTypes = append(filterTypes, filterType)
   156  					resultList := new([][]string)
   157  					for _, f := range bf {
   158  						var result = []string{
   159  							// void xss
   160  							template.HTMLEscapeString(f.pattern),
   161  							template.HTMLEscapeString(utils.GetFuncName(f.filterFunc)),
   162  						}
   163  						*resultList = append(*resultList, result)
   164  					}
   165  					filterTypeData[filterType] = resultList
   166  				}
   167  			}
   168  		}
   169  
   170  		content["Data"] = filterTypeData
   171  		content["Methods"] = filterTypes
   172  
   173  		data["Content"] = content
   174  		data["Title"] = "Filters"
   175  		writeTemplate(rw, data, routerAndFilterTpl, defaultScriptsTpl)
   176  	default:
   177  		rw.Write([]byte("command not support"))
   178  	}
   179  }
   180  
   181  func list(root string, p interface{}, m M) {
   182  	pt := reflect.TypeOf(p)
   183  	pv := reflect.ValueOf(p)
   184  	if pt.Kind() == reflect.Ptr {
   185  		pt = pt.Elem()
   186  		pv = pv.Elem()
   187  	}
   188  	for i := 0; i < pv.NumField(); i++ {
   189  		var key string
   190  		if root == "" {
   191  			key = pt.Field(i).Name
   192  		} else {
   193  			key = root + "." + pt.Field(i).Name
   194  		}
   195  		if pv.Field(i).Kind() == reflect.Struct {
   196  			list(key, pv.Field(i).Interface(), m)
   197  		} else {
   198  			m[key] = pv.Field(i).Interface()
   199  		}
   200  	}
   201  }
   202  
   203  // PrintTree prints all registered routers.
   204  func PrintTree() M {
   205  	var (
   206  		content     = M{}
   207  		methods     = []string{}
   208  		methodsData = make(M)
   209  	)
   210  	for method, t := range BeeApp.Handlers.routers {
   211  
   212  		resultList := new([][]string)
   213  
   214  		printTree(resultList, t)
   215  
   216  		methods = append(methods, template.HTMLEscapeString(method))
   217  		methodsData[template.HTMLEscapeString(method)] = resultList
   218  	}
   219  
   220  	content["Data"] = methodsData
   221  	content["Methods"] = methods
   222  	return content
   223  }
   224  
   225  func printTree(resultList *[][]string, t *Tree) {
   226  	for _, tr := range t.fixrouters {
   227  		printTree(resultList, tr)
   228  	}
   229  	if t.wildcard != nil {
   230  		printTree(resultList, t.wildcard)
   231  	}
   232  	for _, l := range t.leaves {
   233  		if v, ok := l.runObject.(*ControllerInfo); ok {
   234  			if v.routerType == routerTypeBeego {
   235  				var result = []string{
   236  					template.HTMLEscapeString(v.pattern),
   237  					template.HTMLEscapeString(fmt.Sprintf("%s", v.methods)),
   238  					template.HTMLEscapeString(v.controllerType.String()),
   239  				}
   240  				*resultList = append(*resultList, result)
   241  			} else if v.routerType == routerTypeRESTFul {
   242  				var result = []string{
   243  					template.HTMLEscapeString(v.pattern),
   244  					template.HTMLEscapeString(fmt.Sprintf("%s", v.methods)),
   245  					"",
   246  				}
   247  				*resultList = append(*resultList, result)
   248  			} else if v.routerType == routerTypeHandler {
   249  				var result = []string{
   250  					template.HTMLEscapeString(v.pattern),
   251  					"",
   252  					"",
   253  				}
   254  				*resultList = append(*resultList, result)
   255  			}
   256  		}
   257  	}
   258  }
   259  
   260  // ProfIndex is a http.Handler for showing profile command.
   261  // it's in url pattern "/prof" in admin module.
   262  func profIndex(rw http.ResponseWriter, r *http.Request) {
   263  	r.ParseForm()
   264  	command := r.Form.Get("command")
   265  	if command == "" {
   266  		return
   267  	}
   268  
   269  	var (
   270  		format = r.Form.Get("format")
   271  		data   = make(map[interface{}]interface{})
   272  		result bytes.Buffer
   273  	)
   274  	toolbox.ProcessInput(command, &result)
   275  	data["Content"] = template.HTMLEscapeString(result.String())
   276  
   277  	if format == "json" && command == "gc summary" {
   278  		dataJSON, err := json.Marshal(data)
   279  		if err != nil {
   280  			http.Error(rw, err.Error(), http.StatusInternalServerError)
   281  			return
   282  		}
   283  		writeJSON(rw, dataJSON)
   284  		return
   285  	}
   286  
   287  	data["Title"] = template.HTMLEscapeString(command)
   288  	defaultTpl := defaultScriptsTpl
   289  	if command == "gc summary" {
   290  		defaultTpl = gcAjaxTpl
   291  	}
   292  	writeTemplate(rw, data, profillingTpl, defaultTpl)
   293  }
   294  
   295  // Healthcheck is a http.Handler calling health checking and showing the result.
   296  // it's in "/healthcheck" pattern in admin module.
   297  func healthcheck(rw http.ResponseWriter, r *http.Request) {
   298  	var (
   299  		result     []string
   300  		data       = make(map[interface{}]interface{})
   301  		resultList = new([][]string)
   302  		content    = M{
   303  			"Fields": []string{"Name", "Message", "Status"},
   304  		}
   305  	)
   306  
   307  	for name, h := range toolbox.AdminCheckList {
   308  		if err := h.Check(); err != nil {
   309  			result = []string{
   310  				"error",
   311  				template.HTMLEscapeString(name),
   312  				template.HTMLEscapeString(err.Error()),
   313  			}
   314  		} else {
   315  			result = []string{
   316  				"success",
   317  				template.HTMLEscapeString(name),
   318  				"OK",
   319  			}
   320  		}
   321  		*resultList = append(*resultList, result)
   322  	}
   323  
   324  	queryParams := r.URL.Query()
   325  	jsonFlag := queryParams.Get("json")
   326  	shouldReturnJSON, _ := strconv.ParseBool(jsonFlag)
   327  
   328  	if shouldReturnJSON {
   329  		response := buildHealthCheckResponseList(resultList)
   330  		jsonResponse, err := json.Marshal(response)
   331  
   332  		if err != nil {
   333  			http.Error(rw, err.Error(), http.StatusInternalServerError)
   334  		} else {
   335  			writeJSON(rw, jsonResponse)
   336  		}
   337  		return
   338  	}
   339  
   340  	content["Data"] = resultList
   341  	data["Content"] = content
   342  	data["Title"] = "Health Check"
   343  
   344  	writeTemplate(rw, data, healthCheckTpl, defaultScriptsTpl)
   345  }
   346  
   347  func buildHealthCheckResponseList(healthCheckResults *[][]string) []map[string]interface{} {
   348  	response := make([]map[string]interface{}, len(*healthCheckResults))
   349  
   350  	for i, healthCheckResult := range *healthCheckResults {
   351  		currentResultMap := make(map[string]interface{})
   352  
   353  		currentResultMap["name"] = healthCheckResult[0]
   354  		currentResultMap["message"] = healthCheckResult[1]
   355  		currentResultMap["status"] = healthCheckResult[2]
   356  
   357  		response[i] = currentResultMap
   358  	}
   359  
   360  	return response
   361  
   362  }
   363  
   364  func writeJSON(rw http.ResponseWriter, jsonData []byte) {
   365  	rw.Header().Set("Content-Type", "application/json")
   366  	rw.Write(jsonData)
   367  }
   368  
   369  // TaskStatus is a http.Handler with running task status (task name, status and the last execution).
   370  // it's in "/task" pattern in admin module.
   371  func taskStatus(rw http.ResponseWriter, req *http.Request) {
   372  	data := make(map[interface{}]interface{})
   373  
   374  	// Run Task
   375  	req.ParseForm()
   376  	taskname := req.Form.Get("taskname")
   377  	if taskname != "" {
   378  		if t, ok := toolbox.AdminTaskList[taskname]; ok {
   379  			if err := t.Run(); err != nil {
   380  				data["Message"] = []string{"error", template.HTMLEscapeString(fmt.Sprintf("%s", err))}
   381  			}
   382  			data["Message"] = []string{"success", template.HTMLEscapeString(fmt.Sprintf("%s run success,Now the Status is <br>%s", taskname, t.GetStatus()))}
   383  		} else {
   384  			data["Message"] = []string{"warning", template.HTMLEscapeString(fmt.Sprintf("there's no task which named: %s", taskname))}
   385  		}
   386  	}
   387  
   388  	// List Tasks
   389  	content := make(M)
   390  	resultList := new([][]string)
   391  	var fields = []string{
   392  		"Task Name",
   393  		"Task Spec",
   394  		"Task Status",
   395  		"Last Time",
   396  		"",
   397  	}
   398  	for tname, tk := range toolbox.AdminTaskList {
   399  		result := []string{
   400  			template.HTMLEscapeString(tname),
   401  			template.HTMLEscapeString(tk.GetSpec()),
   402  			template.HTMLEscapeString(tk.GetStatus()),
   403  			template.HTMLEscapeString(tk.GetPrev().String()),
   404  		}
   405  		*resultList = append(*resultList, result)
   406  	}
   407  
   408  	content["Fields"] = fields
   409  	content["Data"] = resultList
   410  	data["Content"] = content
   411  	data["Title"] = "Tasks"
   412  	writeTemplate(rw, data, tasksTpl, defaultScriptsTpl)
   413  }
   414  
   415  func writeTemplate(rw http.ResponseWriter, data map[interface{}]interface{}, tpls ...string) {
   416  	tmpl := template.Must(template.New("dashboard").Parse(dashboardTpl))
   417  	for _, tpl := range tpls {
   418  		tmpl = template.Must(tmpl.Parse(tpl))
   419  	}
   420  	tmpl.Execute(rw, data)
   421  }
   422  
   423  // adminApp is an http.HandlerFunc map used as beeAdminApp.
   424  type adminApp struct {
   425  	routers map[string]http.HandlerFunc
   426  }
   427  
   428  // Route adds http.HandlerFunc to adminApp with url pattern.
   429  func (admin *adminApp) Route(pattern string, f http.HandlerFunc) {
   430  	admin.routers[pattern] = f
   431  }
   432  
   433  // Run adminApp http server.
   434  // Its addr is defined in configuration file as adminhttpaddr and adminhttpport.
   435  func (admin *adminApp) Run() {
   436  	if len(toolbox.AdminTaskList) > 0 {
   437  		toolbox.StartTask()
   438  	}
   439  	addr := BConfig.Listen.AdminAddr
   440  
   441  	if BConfig.Listen.AdminPort != 0 {
   442  		addr = fmt.Sprintf("%s:%d", BConfig.Listen.AdminAddr, BConfig.Listen.AdminPort)
   443  	}
   444  	for p, f := range admin.routers {
   445  		http.Handle(p, f)
   446  	}
   447  	logs.Info("Admin server Running on %s", addr)
   448  
   449  	var err error
   450  	if BConfig.Listen.Graceful {
   451  		err = grace.ListenAndServe(addr, nil)
   452  	} else {
   453  		err = http.ListenAndServe(addr, nil)
   454  	}
   455  	if err != nil {
   456  		logs.Critical("Admin ListenAndServe: ", err, fmt.Sprintf("%d", os.Getpid()))
   457  	}
   458  }