mirror of
https://github.com/project-zot/zot.git
synced 2026-06-16 04:17:55 +08:00
Implement an API for performance monitoring
Signed-off-by: Alexei Dodon <adodon@cisco.com>
This commit is contained in:
committed by
Ramkumar Chinchani
parent
061dfb333b
commit
8e4d828867
Executable → Regular
@@ -14,6 +14,7 @@ import (
|
||||
var (
|
||||
Commit string // nolint: gochecknoglobals
|
||||
BinaryType string // nolint: gochecknoglobals
|
||||
GoVersion string // nolint: gochecknoglobals
|
||||
)
|
||||
|
||||
type StorageConfig struct {
|
||||
@@ -102,6 +103,7 @@ type Policy struct {
|
||||
|
||||
type Config struct {
|
||||
Version string
|
||||
GoVersion string
|
||||
Commit string
|
||||
BinaryType string
|
||||
AccessControl *AccessControlConfig
|
||||
@@ -114,6 +116,7 @@ type Config struct {
|
||||
func New() *Config {
|
||||
return &Config{
|
||||
Version: distspec.Version,
|
||||
GoVersion: GoVersion,
|
||||
Commit: Commit,
|
||||
BinaryType: BinaryType,
|
||||
Storage: GlobalStorageConfig{GC: true, Dedupe: true},
|
||||
|
||||
+16
-4
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/anuvu/zot/errors"
|
||||
"github.com/anuvu/zot/pkg/api/config"
|
||||
ext "github.com/anuvu/zot/pkg/extensions"
|
||||
"github.com/anuvu/zot/pkg/extensions/monitoring"
|
||||
"github.com/anuvu/zot/pkg/log"
|
||||
"github.com/anuvu/zot/pkg/storage"
|
||||
"github.com/gorilla/handlers"
|
||||
@@ -29,6 +30,7 @@ type Controller struct {
|
||||
Log log.Logger
|
||||
Audit *log.Logger
|
||||
Server *http.Server
|
||||
Metrics monitoring.MetricServer
|
||||
}
|
||||
|
||||
func NewController(config *config.Config) *Controller {
|
||||
@@ -72,17 +74,26 @@ func (c *Controller) Run() error {
|
||||
|
||||
engine := mux.NewRouter()
|
||||
engine.Use(DefaultHeaders(),
|
||||
log.SessionLogger(c.Log),
|
||||
SessionLogger(c),
|
||||
handlers.RecoveryHandler(handlers.RecoveryLogger(c.Log),
|
||||
handlers.PrintRecoveryStack(false)))
|
||||
|
||||
if c.Audit != nil {
|
||||
engine.Use(log.SessionAuditLogger(c.Audit))
|
||||
engine.Use(SessionAuditLogger(c.Audit))
|
||||
}
|
||||
|
||||
c.Router = engine
|
||||
c.Router.UseEncodedPath()
|
||||
|
||||
var enabled bool
|
||||
if c.Config != nil &&
|
||||
c.Config.Extensions != nil &&
|
||||
c.Config.Extensions.Metrics != nil &&
|
||||
c.Config.Extensions.Metrics.Enable {
|
||||
enabled = true
|
||||
}
|
||||
|
||||
c.Metrics = monitoring.NewMetricsServer(enabled, c.Log)
|
||||
c.StoreController = storage.StoreController{}
|
||||
|
||||
if c.Config.Storage.RootDirectory != "" {
|
||||
@@ -97,7 +108,7 @@ func (c *Controller) Run() error {
|
||||
}
|
||||
|
||||
defaultStore := storage.NewImageStore(c.Config.Storage.RootDirectory,
|
||||
c.Config.Storage.GC, c.Config.Storage.Dedupe, c.Log)
|
||||
c.Config.Storage.GC, c.Config.Storage.Dedupe, c.Log, c.Metrics)
|
||||
|
||||
c.StoreController.DefaultStore = defaultStore
|
||||
|
||||
@@ -131,7 +142,7 @@ func (c *Controller) Run() error {
|
||||
}
|
||||
|
||||
subImageStore[route] = storage.NewImageStore(storageConfig.RootDirectory,
|
||||
storageConfig.GC, storageConfig.Dedupe, c.Log)
|
||||
storageConfig.GC, storageConfig.Dedupe, c.Log, c.Metrics)
|
||||
|
||||
// Enable extensions if extension config is provided
|
||||
if c.Config != nil && c.Config.Extensions != nil {
|
||||
@@ -143,6 +154,7 @@ func (c *Controller) Run() error {
|
||||
}
|
||||
}
|
||||
|
||||
monitoring.SetServerInfo(c.Metrics, c.Config.Commit, c.Config.BinaryType, c.Config.GoVersion, c.Config.Version)
|
||||
_ = NewRouteHandler(c)
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", c.Config.HTTP.Address, c.Config.HTTP.Port)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package api_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anuvu/zot/pkg/api"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestUnknownCodeError(t *testing.T) {
|
||||
Convey("Retrieve a new error with unknown code", t, func() {
|
||||
So(func() { _ = api.NewError(123456789, nil) }, ShouldPanic)
|
||||
})
|
||||
}
|
||||
+13
-2
@@ -96,8 +96,14 @@ func (rh *RouteHandler) SetupRoutes() {
|
||||
// swagger swagger "/swagger/v2/index.html"
|
||||
rh.c.Router.PathPrefix("/swagger/v2/").Methods("GET").Handler(httpSwagger.WrapHandler)
|
||||
// Setup Extensions Routes
|
||||
if rh.c.Config != nil && rh.c.Config.Extensions != nil {
|
||||
ext.SetupRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log)
|
||||
if rh.c.Config != nil {
|
||||
if rh.c.Config.Extensions == nil {
|
||||
// minimal build
|
||||
g.HandleFunc("/metrics", rh.GetMetrics).Methods("GET")
|
||||
} else {
|
||||
// extended build
|
||||
ext.SetupRoutes(rh.c.Config, rh.c.Router, rh.c.StoreController, rh.c.Log)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1177,6 +1183,11 @@ func (rh *RouteHandler) ListRepositories(w http.ResponseWriter, r *http.Request)
|
||||
WriteJSON(w, http.StatusOK, is)
|
||||
}
|
||||
|
||||
func (rh *RouteHandler) GetMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
m := rh.c.Metrics.ReceiveMetrics()
|
||||
WriteJSON(w, http.StatusOK, m)
|
||||
}
|
||||
|
||||
// helper routines
|
||||
|
||||
func getContentRange(r *http.Request) (int64 /* from */, int64 /* to */, error) {
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/anuvu/zot/pkg/extensions/monitoring"
|
||||
"github.com/anuvu/zot/pkg/log"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type statusWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
length int
|
||||
}
|
||||
|
||||
func (w *statusWriter) WriteHeader(status int) {
|
||||
w.status = status
|
||||
w.ResponseWriter.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (w *statusWriter) Write(b []byte) (int, error) {
|
||||
if w.status == 0 {
|
||||
w.status = 200
|
||||
}
|
||||
|
||||
n, err := w.ResponseWriter.Write(b)
|
||||
w.length += n
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// SessionLogger logs session details.
|
||||
func SessionLogger(c *Controller) mux.MiddlewareFunc {
|
||||
l := c.Log.With().Str("module", "http").Logger()
|
||||
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Start timer
|
||||
start := time.Now()
|
||||
path := r.URL.Path
|
||||
raw := r.URL.RawQuery
|
||||
|
||||
sw := statusWriter{ResponseWriter: w}
|
||||
|
||||
// Process request
|
||||
next.ServeHTTP(&sw, r)
|
||||
|
||||
// Stop timer
|
||||
end := time.Now()
|
||||
latency := end.Sub(start)
|
||||
if latency > time.Minute {
|
||||
// Truncate in a golang < 1.8 safe way
|
||||
latency -= latency % time.Second
|
||||
}
|
||||
clientIP := r.RemoteAddr
|
||||
method := r.Method
|
||||
headers := map[string][]string{}
|
||||
username := ""
|
||||
log := l.Info()
|
||||
for key, value := range r.Header {
|
||||
if key == "Authorization" { // anonymize from logs
|
||||
s := strings.SplitN(value[0], " ", 2)
|
||||
if len(s) == 2 && strings.EqualFold(s[0], "basic") {
|
||||
b, err := base64.StdEncoding.DecodeString(s[1])
|
||||
if err == nil {
|
||||
pair := strings.SplitN(string(b), ":", 2)
|
||||
// nolint:gomnd
|
||||
if len(pair) == 2 {
|
||||
username = pair[0]
|
||||
log = log.Str("username", username)
|
||||
}
|
||||
}
|
||||
}
|
||||
value = []string{"******"}
|
||||
}
|
||||
headers[key] = value
|
||||
}
|
||||
statusCode := sw.status
|
||||
bodySize := sw.length
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
if path != "/v2/metrics" {
|
||||
// In order to test metrics feture,the instrumentation related to node exporter
|
||||
// should be handled by node exporter itself (ex: latency)
|
||||
monitoring.IncHTTPConnRequests(c.Metrics, method, strconv.Itoa(statusCode))
|
||||
monitoring.ObserveHTTPRepoLatency(c.Metrics, path, latency) // summary
|
||||
monitoring.ObserveHTTPMethodLatency(c.Metrics, method, latency) // histogram
|
||||
}
|
||||
|
||||
log.Str("clientIP", clientIP).
|
||||
Str("method", method).
|
||||
Str("path", path).
|
||||
Int("statusCode", statusCode).
|
||||
Str("latency", latency.String()).
|
||||
Int("bodySize", bodySize).
|
||||
Interface("headers", headers).
|
||||
Msg("HTTP API")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func SessionAuditLogger(audit *log.Logger) mux.MiddlewareFunc {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
raw := r.URL.RawQuery
|
||||
|
||||
sw := statusWriter{ResponseWriter: w}
|
||||
|
||||
// Process request
|
||||
next.ServeHTTP(&sw, r)
|
||||
|
||||
clientIP := r.RemoteAddr
|
||||
method := r.Method
|
||||
username := ""
|
||||
|
||||
for key, value := range r.Header {
|
||||
if key == "Authorization" { // anonymize from logs
|
||||
s := strings.SplitN(value[0], " ", 2)
|
||||
if len(s) == 2 && strings.EqualFold(s[0], "basic") {
|
||||
b, err := base64.StdEncoding.DecodeString(s[1])
|
||||
if err == nil {
|
||||
pair := strings.SplitN(string(b), ":", 2)
|
||||
// nolint:gomnd
|
||||
if len(pair) == 2 {
|
||||
username = pair[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
statusCode := sw.status
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
if (method == http.MethodPost || method == http.MethodPut ||
|
||||
method == http.MethodPatch || method == http.MethodDelete) &&
|
||||
(statusCode == http.StatusOK || statusCode == http.StatusCreated || statusCode == http.StatusAccepted) {
|
||||
audit.Info().
|
||||
Str("clientIP", clientIP).
|
||||
Str("subject", username).
|
||||
Str("action", method).
|
||||
Str("object", path).
|
||||
Int("status", statusCode).
|
||||
Msg("HTTP API Audit")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user