This is an automated email from the git hooks/post-receive script. New commit to branch bow-v2-go in repository bow. See https://gitlab.nuiton.org/chorem/bow.git commit 94d51198a9fb5813df8c9f705f0d7779b7809afe Author: Benjamin <poussin@codelutin.com> Date: Sun May 17 23:15:52 2020 +0200 ajout de statistique sur les appels --- pkg/http/router.go | 53 +++++++++++++++++++--------- pkg/http/systemResource.go | 12 +++++++ pkg/utils/stats.go | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 17 deletions(-) diff --git a/pkg/http/router.go b/pkg/http/router.go index 706618a..c7c7c81 100644 --- a/pkg/http/router.go +++ b/pkg/http/router.go @@ -2,6 +2,7 @@ package http import ( "context" + "fmt" "io" "log" "net/http" @@ -20,6 +21,8 @@ import ( var BowPublicURL string +var stats = utils.NewStats() + /* Start web server */ @@ -36,6 +39,7 @@ func Start(bowPublicURL string, addr string) { s := router.PathPrefix("/api/v1").Subrouter() s.HandleFunc("/system/liveness", isAlive).Methods(http.MethodGet, http.MethodOptions) + s.HandleFunc("/system/stats", getStats).Methods(http.MethodGet, http.MethodOptions) s.HandleFunc("/users", createUser).Methods(http.MethodPost, http.MethodOptions) s.HandleFunc("/users/auth", createAuth).Methods(http.MethodPost, http.MethodOptions) @@ -55,7 +59,7 @@ func Start(bowPublicURL string, addr string) { u.HandleFunc("/autofavicon", updateUserAutoFavicon).Methods(http.MethodPut, http.MethodOptions) u.HandleFunc("/maxtagincloud", updateUserMaxTagInCloud).Methods(http.MethodPut, http.MethodOptions) u.HandleFunc("/maxresult", updateUserMaxResult).Methods(http.MethodPut, http.MethodOptions) - + s.HandleFunc("/bookmarks", getBookmarks).Methods(http.MethodGet, http.MethodOptions) s.HandleFunc("/bookmarks", addBookmark).Methods(http.MethodPost, http.MethodOptions) s.HandleFunc("/bookmarks/tags", getTags).Methods(http.MethodGet, http.MethodOptions) @@ -65,7 +69,7 @@ func Start(bowPublicURL string, addr string) { s.HandleFunc("/bookmarks/{id}", updateBookmark).Methods(http.MethodPut, http.MethodOptions) s.HandleFunc("/bookmarks/{id}/visit", addOneVisit).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) s.HandleFunc("/bookmarks/{id}/authenticationinfo", updateBookmarkAuthenticationInfo).Methods(http.MethodPut, http.MethodOptions) - + s.HandleFunc("/opensearch", doActions).Methods(http.MethodGet, http.MethodPost, http.MethodOptions).Queries(constant.Action, "{ask}") s.HandleFunc("/opensearch", doSuggestion).Methods(http.MethodGet, http.MethodPost, http.MethodOptions).Queries(constant.Suggestion, "{ask}") s.HandleFunc("/opensearch", opensearchFile).Methods(http.MethodGet, http.MethodOptions) @@ -93,7 +97,7 @@ func logAll(next http.Handler) http.Handler { query := r.URL.Query() debug := query.Get("bow_debug") - if (os.Getenv("BOW_DEBUG") == "true" || debug == "true") { + if os.Getenv("BOW_DEBUG") == "true" || debug == "true" { log.Println("logAll", r) } next.ServeHTTP(w, r) @@ -120,8 +124,9 @@ func cors(next http.Handler) http.Handler { func withoutAuthenticationEndpoint(r *http.Request) bool { result := !strings.HasPrefix(r.URL.Path, "/api") || // no auth for SPA (html, css, ...) - strings.HasSuffix(r.URL.Path, "/auth") || // no auth to create/delete auth + strings.HasSuffix(r.URL.Path, "/auth") || // no auth to create/delete auth strings.HasSuffix(r.URL.Path, "/system/liveness") || // no auth to test if server is up + strings.HasSuffix(r.URL.Path, "/system/stats") || // no auth to test if server is up strings.HasSuffix(r.URL.Path, "/users") // no auth for creation of user return result } @@ -145,7 +150,8 @@ func authentication(next http.Handler) http.Handler { return } - authTime := time.Now() + authStat := stats.StartStat("authentication") + canBeAppToken := false // 1 as query param @@ -211,15 +217,28 @@ func authentication(next http.Handler) http.Handler { ctx := context.WithValue(r.Context(), constant.User, user) r = r.WithContext(ctx) - utils.Duration("authentication", authTime) + authStat.Stop() - restTime := time.Now() + routeName := getRouteName(r) + restStat := stats.StartStat(fmt.Sprintf("rest(%v)", routeName)) next.ServeHTTP(w, r) - utils.Duration("rest", restTime) + restStat.Stop() }) } +func getRouteName(r *http.Request) string { + currentRoute := mux.CurrentRoute(r) + result := currentRoute.GetName() + if result == "" { + result, _ = currentRoute.GetPathTemplate() + if result == "" { + result, _ = currentRoute.GetPathRegexp() + } + } + return result +} + // spaHandler implements the http.Handler interface, so we can use it // to respond to HTTP requests. The path to the static directory and // path to the index file within that static directory are used to @@ -235,31 +254,31 @@ type spaHandler struct { // is suitable behavior for serving an SPA (single page application). func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { log.Println("serve web content", r.URL.Path) - // get the absolute path to prevent directory traversal + // get the absolute path to prevent directory traversal path, err := filepath.Abs(r.URL.Path) if err != nil { - // if we failed to get the absolute path respond with a 400 bad request - // and stop + // if we failed to get the absolute path respond with a 400 bad request + // and stop http.Error(w, err.Error(), http.StatusBadRequest) return } - // prepend the path with the path to the static directory + // prepend the path with the path to the static directory path = filepath.Join(h.staticPath, path) - // check whether a file exists at the given path + // check whether a file exists at the given path _, err = os.Stat(path) if os.IsNotExist(err) { // file does not exist, serve index.html http.ServeFile(w, r, filepath.Join(h.staticPath, h.indexPath)) return } else if err != nil { - // if we got an error (that wasn't that the file doesn't exist) stating the - // file, return a 500 internal server error and stop + // if we got an error (that wasn't that the file doesn't exist) stating the + // file, return a 500 internal server error and stop http.Error(w, err.Error(), http.StatusInternalServerError) return } - // otherwise, use http.FileServer to serve the static dir + // otherwise, use http.FileServer to serve the static dir http.FileServer(http.Dir(h.staticPath)).ServeHTTP(w, r) -} \ No newline at end of file +} diff --git a/pkg/http/systemResource.go b/pkg/http/systemResource.go index 482e97f..2185d85 100644 --- a/pkg/http/systemResource.go +++ b/pkg/http/systemResource.go @@ -2,11 +2,23 @@ package http import ( "encoding/json" + // "io" "log" "net/http" ) func isAlive(w http.ResponseWriter, r *http.Request) { log.Println("http liveness") + + w.Header().Add("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]bool{"alive": true}) } + +func getStats(w http.ResponseWriter, r *http.Request) { + log.Println("http stats", stats) + + w.Header().Add("Content-Type", "application/json") + // io.WriteString(w, stats.String()) + // je voulais faire un export json, mais ça reste vide, donc comme le toSting est un json :p + json.NewEncoder(w).Encode(stats) +} diff --git a/pkg/utils/stats.go b/pkg/utils/stats.go new file mode 100644 index 0000000..0d282d5 --- /dev/null +++ b/pkg/utils/stats.go @@ -0,0 +1,88 @@ +package utils + +import ( + "fmt" + "math" + "strings" + "sync" + "time" +) + +type Stat struct { + Nb int64 `json:"call"` + Min time.Duration `json:"min"` + Max time.Duration `json:"max"` + Avg time.Duration `json:"avg"` + StdDeviation time.Duration `json:"stddeviation"` + + variance float64 + + // pour l'ecart type (standard deviation) + delta int64 + M2 int64 + + // pour que les requetes concurrente de corrompe pas les donnees + sync.Mutex +} + +type Stats struct { + Values map[string]*Stat `json:"stats"` + + // pour que les requetes concurrente de corrompe pas les donnees + sync.Mutex +} + +type OneCall struct { + name string + start time.Time + all *Stats +} + +func NewStats() *Stats { + return &Stats{Values: make(map[string]*Stat)} +} + +func (all *Stats) String() string { + var result []string + for name, s := range all.Values { + result = append(result, fmt.Sprintf(`%v: {"call": %v, "min": %q, "max": %q, "avg": %q, "stdderivation": %v}`, name, s.Nb, s.Min, s.Max, s.Avg, s.StdDeviation)) + } + return fmt.Sprintf("{%v}", strings.Join(result, ",")) +} + +func (s *Stats) StartStat(name string) *OneCall { + return &OneCall{name: name, start: time.Now(), all: s} +} + +func (call *OneCall) Stop() { + duration := time.Since(call.start) + s := call.all.Values[call.name] + if s == nil { + call.all.Lock() + s = call.all.Values[call.name] + if s == nil { + s = &Stat{} + call.all.Values[call.name] = s + } + call.all.Unlock() + } + + s.Lock() + defer s.Unlock() + + s.Nb++ + if s.Min == 0 || s.Min > duration { + s.Min = duration + } + + if s.Max < duration { + s.Max = duration + } + + s.delta = duration.Nanoseconds() - s.Avg.Nanoseconds() + s.Avg = time.Duration(s.Avg.Nanoseconds() + s.delta/s.Nb) + s.M2 = s.M2 + s.delta*(duration.Nanoseconds()-s.Avg.Nanoseconds()) + + variance := float64(s.M2) / math.Max(1, float64(s.Nb-1)) + s.StdDeviation = time.Duration(int64(math.Sqrt(variance))) +} -- To stop receiving notification emails like this one, please contact chorem.org SCM administrator <admin+scm@chorem.org>.