diff --git a/debug.md b/debug.md new file mode 100644 index 0000000000000000000000000000000000000000..94f3e74abb734df7faf29f42be78a51776d1bad3 --- /dev/null +++ b/debug.md @@ -0,0 +1,10 @@ +### Debug information + +This is a useful Prometheus query to show: + +* Service replicas +* Rate of invocation +* Execution time of events + +http://localhost:9090/graph?g0.range_input=15m&g0.expr=gateway_service_count&g0.tab=0&g1.range_input=15m&g1.expr=rate(gateway_function_invocation_total%5B20s%5D)&g1.tab=0&g2.range_input=15m&g2.expr=gateway_functions_seconds_sum+%2F+gateway_functions_seconds_count&g2.tab=0 + diff --git a/gateway/assets/script/bootstrap.js b/gateway/assets/script/bootstrap.js index 2d0ea18c6354b0008350022400ca468e1240db30..a86b944d44f1922d4663bfa2d677ce5dc9ead058 100644 --- a/gateway/assets/script/bootstrap.js +++ b/gateway/assets/script/bootstrap.js @@ -35,7 +35,11 @@ app.controller("home", ['$scope', '$log', '$http', '$location', '$timeout', '$md $http(options) .then(function(response) { - $scope.invocationResponse = response.data; + if($scope.invocation && $scope.invocation.contentType == "json") { + $scope.invocationResponse = JSON.stringify(response.data, -1, " "); + } else { + $scope.invocationResponse = response.data; + } $scope.invocationStatus = response.status; }).catch(function(error1) { $scope.invocationResponse = error1.statusText + "\n" + error1.data; diff --git a/gateway/build.sh b/gateway/build.sh index 6be0d12925bc0b4d95154de31f876faf9ac91d88..9195ed8f643c451429b8acbc3acdb78748358898 100755 --- a/gateway/build.sh +++ b/gateway/build.sh @@ -2,12 +2,9 @@ echo Building functions/gateway:build docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \ - -t functions/gateway:build . -f Dockerfile.build - -docker create --name gateway_extract functions/gateway:build -docker cp gateway_extract:/go/src/github.com/alexellis/faas/gateway/app ./gateway -docker rm -f gateway_extract - -echo Building functions/gateway:latest - + -t functions/gateway:build . -f Dockerfile.build && \ + docker create --name gateway_extract functions/gateway:build && \ + docker cp gateway_extract:/go/src/github.com/alexellis/faas/gateway/app ./gateway && \ + docker rm -f gateway_extract && \ +echo Building functions/gateway:latest && \ docker build --no-cache -t functions/gateway:latest-dev . diff --git a/gateway/handlers/functionshandler.go b/gateway/handlers/functionshandler.go index 50c0d2895b005651f683c1cc99880dcf71259bff..d493630ae137428091eb8b1c8312fc9133baea96 100644 --- a/gateway/handlers/functionshandler.go +++ b/gateway/handlers/functionshandler.go @@ -21,7 +21,9 @@ import ( func getCounterValue(service string, code string, metricsOptions *metrics.MetricOptions) float64 { - metric, err := metricsOptions.GatewayFunctionInvocation.GetMetricWith(prometheus.Labels{"function_name": service, "code": code}) + metric, err := metricsOptions.GatewayFunctionInvocation. + GetMetricWith(prometheus.Labels{"function_name": service, "code": code}) + if err != nil { return 0 } diff --git a/gateway/handlers/proxy.go b/gateway/handlers/proxy.go index 1b37c8ebd5e3a87fc24771b3d6f0bbd144dde922..b2e6ecf85f1b8c38503bb4a40d10244a4bc4c3d7 100644 --- a/gateway/handlers/proxy.go +++ b/gateway/handlers/proxy.go @@ -21,7 +21,6 @@ import ( // MakeProxy creates a proxy for HTTP web requests which can be routed to a function. func MakeProxy(metrics metrics.MetricOptions, wildcard bool, c *client.Client, logger *logrus.Logger) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - metrics.GatewayRequestsTotal.Inc() if r.Method == "POST" { logger.Infoln(r.Header) @@ -55,8 +54,14 @@ func writeHead(service string, metrics metrics.MetricOptions, code int, w http.R // metrics.GatewayFunctionInvocation.WithLabelValues(service).Add(1) } +func trackTime(then time.Time, metrics metrics.MetricOptions, name string) { + since := time.Since(then) + metrics.GatewayFunctionsHistogram.WithLabelValues(name).Observe(since.Seconds()) +} + func lookupInvoke(w http.ResponseWriter, r *http.Request, metrics metrics.MetricOptions, name string, c *client.Client, logger *logrus.Logger) { exists, err := lookupSwarmService(name, c) + if err != nil || exists == false { if err != nil { logger.Fatalln(err) @@ -64,8 +69,11 @@ func lookupInvoke(w http.ResponseWriter, r *http.Request, metrics metrics.Metric writeHead(name, metrics, http.StatusInternalServerError, w) w.Write([]byte("Error resolving service.")) defer r.Body.Close() + return } - if exists == true { + + if exists { + defer trackTime(time.Now(), metrics, name) requestBody, _ := ioutil.ReadAll(r.Body) invokeService(w, r, metrics, name, requestBody, logger) } @@ -81,10 +89,16 @@ func lookupSwarmService(serviceName string, c *client.Client) (bool, error) { } func invokeService(w http.ResponseWriter, r *http.Request, metrics metrics.MetricOptions, service string, requestBody []byte, logger *logrus.Logger) { - stamp := strconv.FormatInt(time.Now().Unix(), 10) - start := time.Now() + defer func(when time.Time) { + seconds := time.Since(when).Seconds() + + fmt.Printf("[%s] took %f seconds\n", stamp, seconds) + metrics.GatewayFunctionsHistogram.WithLabelValues(service).Observe(seconds) + }(time.Now()) + + // start := time.Now() buf := bytes.NewBuffer(requestBody) url := "http://" + service + ":" + strconv.Itoa(8080) + "/" contentType := r.Header.Get("Content-Type") @@ -118,9 +132,4 @@ func invokeService(w http.ResponseWriter, r *http.Request, metrics metrics.Metri writeHead(service, metrics, http.StatusOK, w) w.Write(responseBody) - - seconds := time.Since(start).Seconds() - fmt.Printf("[%s] took %f seconds\n", stamp, seconds) - metrics.GatewayServerlessServedTotal.Inc() - metrics.GatewayFunctions.Observe(seconds) } diff --git a/gateway/metrics/metrics.go b/gateway/metrics/metrics.go index 44bba64d02f479ebd1dc1ff3296782f4eb608dc6..ea8503eb48361ded40fb46fe9e4dbcb756d0b7fc 100644 --- a/gateway/metrics/metrics.go +++ b/gateway/metrics/metrics.go @@ -8,10 +8,9 @@ import ( // MetricOptions to be used by web handlers type MetricOptions struct { - GatewayRequestsTotal prometheus.Counter - GatewayServerlessServedTotal prometheus.Counter - GatewayFunctions prometheus.Histogram - GatewayFunctionInvocation *prometheus.CounterVec + GatewayFunctionInvocation *prometheus.CounterVec + GatewayFunctionsHistogram *prometheus.HistogramVec + ServiceReplicasCounter *prometheus.GaugeVec } // PrometheusHandler Bootstraps prometheus for metrics collection @@ -19,20 +18,14 @@ func PrometheusHandler() http.Handler { return prometheus.Handler() } +// BuildMetricsOptions builds metrics for tracking functions in the API gateway func BuildMetricsOptions() MetricOptions { - GatewayRequestsTotal := prometheus.NewCounter(prometheus.CounterOpts{ - Name: "gateway_requests_total", - Help: "Total amount of HTTP requests to the gateway", - }) - GatewayServerlessServedTotal := prometheus.NewCounter(prometheus.CounterOpts{ - Name: "gateway_serverless_invocation_total", - Help: "Total amount of serverless function invocations", - }) - GatewayFunctions := prometheus.NewHistogram(prometheus.HistogramOpts{ - Name: "gateway_functions", - Help: "Gateway functions", - }) - GatewayFunctionInvocation := prometheus.NewCounterVec( + gatewayFunctionsHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "gateway_functions_seconds", + Help: "Function time taken", + }, []string{"function_name"}) + + gatewayFunctionInvocation := prometheus.NewCounterVec( prometheus.CounterOpts{ Name: "gateway_function_invocation_total", Help: "Individual function metrics", @@ -40,19 +33,26 @@ func BuildMetricsOptions() MetricOptions { []string{"function_name", "code"}, ) + serviceReplicas := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "gateway_service_count", + Help: "Docker service replicas", + }, + []string{"function_name"}, + ) + metricsOptions := MetricOptions{ - GatewayRequestsTotal: GatewayRequestsTotal, - GatewayServerlessServedTotal: GatewayServerlessServedTotal, - GatewayFunctions: GatewayFunctions, - GatewayFunctionInvocation: GatewayFunctionInvocation, + GatewayFunctionsHistogram: gatewayFunctionsHistogram, + GatewayFunctionInvocation: gatewayFunctionInvocation, + ServiceReplicasCounter: serviceReplicas, } return metricsOptions } +//RegisterMetrics registers with Prometheus for tracking func RegisterMetrics(metricsOptions MetricOptions) { - prometheus.Register(metricsOptions.GatewayRequestsTotal) - prometheus.Register(metricsOptions.GatewayServerlessServedTotal) - prometheus.Register(metricsOptions.GatewayFunctions) prometheus.Register(metricsOptions.GatewayFunctionInvocation) + prometheus.Register(metricsOptions.GatewayFunctionsHistogram) + prometheus.Register(metricsOptions.ServiceReplicasCounter) } diff --git a/gateway/metrics/swarmwatcher.go b/gateway/metrics/swarmwatcher.go new file mode 100644 index 0000000000000000000000000000000000000000..8d61ecdab16b8fedcbfa9b24e3b194952397b28f --- /dev/null +++ b/gateway/metrics/swarmwatcher.go @@ -0,0 +1,48 @@ +package metrics + +import ( + "context" + "fmt" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "github.com/docker/docker/client" +) + +// AttachSwarmWatcher adds a go-route to monitor the amount of service replicas in the swarm +// matching a 'function' label. +func AttachSwarmWatcher(dockerClient *client.Client, metricsOptions MetricOptions) { + ticker := time.NewTicker(1 * time.Second) + quit := make(chan struct{}) + + go func() { + for { + select { + case <-ticker.C: + serviceFilter := filters.NewArgs() + + options := types.ServiceListOptions{ + Filters: serviceFilter, + } + + services, err := dockerClient.ServiceList(context.Background(), options) + if err != nil { + fmt.Println(err) + } + + for _, service := range services { + if len(service.Spec.TaskTemplate.ContainerSpec.Labels["function"]) > 0 { + metricsOptions.ServiceReplicasCounter. + WithLabelValues(service.Spec.Name). + Set(float64(*service.Spec.Mode.Replicated.Replicas)) + } + } + break + case <-quit: + return + } + } + }() + +} diff --git a/gateway/server.go b/gateway/server.go index 8f389dd0ea4781f73fe38f46218a36cd3672d4d1..d43b4f5149ea00fcddaf385039b118534262cbd9 100644 --- a/gateway/server.go +++ b/gateway/server.go @@ -33,7 +33,7 @@ func main() { metrics.RegisterMetrics(metricsOptions) r := mux.NewRouter() - // r.StrictSlash(false) + // r.StrictSlash(false) // This didn't work, so register routes twice. functionHandler := faasHandlers.MakeProxy(metricsOptions, true, dockerClient, &logger) r.HandleFunc("/function/{name:[a-zA-Z_0-9]+}", functionHandler) @@ -47,6 +47,7 @@ func main() { metricsHandler := metrics.PrometheusHandler() r.Handle("/metrics", metricsHandler) + metrics.AttachSwarmWatcher(dockerClient, metricsOptions) r.PathPrefix("/").Handler(http.FileServer(http.Dir("./assets/"))).Methods("GET") s := &http.Server{