From b6d7357d2a5d44f011d362df0257c1acc51d5561 Mon Sep 17 00:00:00 2001 From: Mihai Todor Date: Thu, 6 Apr 2017 16:07:04 +0100 Subject: [PATCH 1/5] Add backend connection stats --- server.go | 7 ++++- web.go | 81 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/server.go b/server.go index f9c93e7ee4..f7c652bd8c 100644 --- a/server.go +++ b/server.go @@ -53,6 +53,7 @@ type Server struct { loggerMiddleware *middlewares.Logger routinesPool *safe.Pool leadership *cluster.Leadership + backendConnLimits map[string]*connlimit.ConnLimiter } type serverEntryPoints map[string]*serverEntryPoint @@ -88,6 +89,7 @@ func NewServer(globalConfiguration GlobalConfiguration) *Server { // leadership creation if cluster mode server.leadership = cluster.NewLeadership(server.routinesPool.Ctx(), globalConfiguration.Cluster) } + server.backendConnLimits = make(map[string]*connlimit.ConnLimiter) return server } @@ -732,12 +734,15 @@ func (server *Server) loadConfig(configurations configs, globalConfiguration Glo continue frontend } log.Debugf("Creating load-balancer connlimit") - lb, err = connlimit.New(lb, extractFunc, maxConns.Amount, connlimit.Logger(oxyLogger)) + cl, err := connlimit.New(lb, extractFunc, maxConns.Amount, connlimit.Logger(oxyLogger)) if err != nil { log.Errorf("Error creating connlimit: %v", err) log.Errorf("Skipping frontend %s...", frontendName) continue frontend } + + lb = cl + server.backendConnLimits[frontend.Backend] = cl } // retry ? if globalConfiguration.Retry != nil { diff --git a/web.go b/web.go index e71e75f20d..dd43cc5239 100644 --- a/web.go +++ b/web.go @@ -6,6 +6,7 @@ import ( "fmt" "io/ioutil" "net/http" + "reflect" "runtime" "github.com/codegangsta/negroni" @@ -20,6 +21,7 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" thoas_stats "github.com/thoas/stats" "github.com/unrolled/render" + "github.com/vulcand/oxy/connlimit" ) var ( @@ -122,6 +124,10 @@ func (provider *WebProvider) Provide(configurationChan chan<- types.ConfigMessag systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/frontends/{frontend}/routes").HandlerFunc(provider.getRoutesHandler) systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/frontends/{frontend}/routes/{route}").HandlerFunc(provider.getRouteHandler) + // Expose connection stats + systemRouter.Methods("GET").Path(provider.Path + "api/conn_stats").HandlerFunc(provider.getConnStatsHandler) + systemRouter.Methods("GET").Path(provider.Path + "api/providers/{provider}/backends/{backend}/conn_stats").HandlerFunc(provider.getBackendConnStatsHandler) + // Expose dashboard systemRouter.Methods("GET").Path(provider.Path).HandlerFunc(func(response http.ResponseWriter, request *http.Request) { http.Redirect(response, request, provider.Path+"dashboard/", 302) @@ -330,3 +336,78 @@ func expvarHandler(w http.ResponseWriter, r *http.Request) { }) fmt.Fprintf(w, "\n}\n") } + +type connStats struct { + MaxConn int64 `json:"max_conn"` + TotalConn int64 `json:"total_conn"` +} + +func (provider *WebProvider) getBackendConnStatsHandler(response http.ResponseWriter, request *http.Request) { + vars := mux.Vars(request) + providerID := vars["provider"] + backendID := vars["backend"] + currentConfigurations := provider.server.currentConfigurations.Get().(configs) + if providerConf, ok := currentConfigurations[providerID]; ok { + if backendConf, ok := providerConf.Backends[backendID]; ok { + if connLimiter, ok := provider.server.backendConnLimits[backendID]; ok { + if totalConn, ok := getTotalConn(connLimiter); ok { + templatesRenderer.JSON(response, http.StatusOK, &connStats{ + MaxConn: backendConf.MaxConn.Amount, + TotalConn: totalConn, + }) + return + } + } + } + } + + http.NotFound(response, request) +} + +type providers struct { + Backends map[string]connStats `json:"backends"` +} + +func (provider *WebProvider) getConnStatsHandler(response http.ResponseWriter, request *http.Request) { + payload := make(map[string]providers) + + currentConfigurations := provider.server.currentConfigurations.Get().(configs) + for p := range currentConfigurations { + payload[p] = providers{ + Backends: make(map[string]connStats), + } + for b := range currentConfigurations[p].Backends { + if connLimiter, ok := provider.server.backendConnLimits[b]; ok { + if totalConn, ok := getTotalConn(connLimiter); ok { + payload[p].Backends[b] = connStats{ + MaxConn: currentConfigurations[p].Backends[b].MaxConn.Amount, + TotalConn: totalConn, + } + } + } + } + } + + templatesRenderer.JSON(response, http.StatusOK, payload) +} + +func getTotalConn(cl *connlimit.ConnLimiter) (int64, bool) { + clPtr := reflect.ValueOf(cl) + if !clPtr.IsValid() && clPtr.Kind() != reflect.Ptr { + log.Debugf("Expecting Ptr type but got %s instead", clPtr.Kind()) + return 0, false + } + + clData := clPtr.Elem() + if !clData.IsValid() && clData.Kind() != reflect.Struct { + log.Debugf("Expecting Struct type but got %s instead", clData.Kind()) + return 0, false + } + + totalConnField := clData.FieldByName("totalConnections") + if !totalConnField.IsValid() && totalConnField.Kind() != reflect.Int64 { + log.Debugf("Expecting Int64 type but got %s instead", totalConnField.Kind()) + return 0, false + } + return totalConnField.Int(), true +} From 1e632c3292a9bfac4b436762e5987cbbdd179ea1 Mon Sep 17 00:00:00 2001 From: Mihai Todor Date: Fri, 7 Apr 2017 23:30:57 +0100 Subject: [PATCH 2/5] Add backend connection stats UI --- webui/readme.md | 2 + webui/src/app/core/conn_stats.resource.js | 14 ++ .../conn_stats/conn_stats.controller.js | 163 ++++++++++++++++++ .../app/sections/conn_stats/conn_stats.html | 14 ++ .../sections/conn_stats/conn_stats.module.js | 24 +++ webui/src/app/sections/sections.js | 4 +- webui/src/index.html | 1 + 7 files changed, 221 insertions(+), 1 deletion(-) create mode 100644 webui/src/app/core/conn_stats.resource.js create mode 100644 webui/src/app/sections/conn_stats/conn_stats.controller.js create mode 100644 webui/src/app/sections/conn_stats/conn_stats.html create mode 100644 webui/src/app/sections/conn_stats/conn_stats.module.js diff --git a/webui/readme.md b/webui/readme.md index 7c42346f42..fb92bf7805 100644 --- a/webui/readme.md +++ b/webui/readme.md @@ -7,6 +7,7 @@ Access to Træfɪk Web UI, ex: http://localhost:8080 Træfɪk Web UI provide 2 types of informations: - Providers with their backends and frontends information. - Health of the web server. +- Connection stats for each backend. ## How to build (for backends developer) @@ -55,6 +56,7 @@ make generate-webui # Generate static contents in `traefik/static/` folder. - Træfɪk API connections are defined in: - `webui/src/app/core/health.resource.js` - `webui/src/app/core/providers.resource.js` + - `webui/src/app/core/conn_stats.resource.js` - The pages contents are in the directory `webui/src/app/sections`. diff --git a/webui/src/app/core/conn_stats.resource.js b/webui/src/app/core/conn_stats.resource.js new file mode 100644 index 0000000000..ebb13843b3 --- /dev/null +++ b/webui/src/app/core/conn_stats.resource.js @@ -0,0 +1,14 @@ +'use strict'; +var angular = require('angular'); + +var traefikCoreConnStats = 'traefik.core.conn_stats'; +module.exports = traefikCoreConnStats; + +angular + .module(traefikCoreConnStats, ['ngResource']) + .factory('ConnStats', ConnStats); + + /** @ngInject */ + function ConnStats($resource) { + return $resource('../api/conn_stats'); + } diff --git a/webui/src/app/sections/conn_stats/conn_stats.controller.js b/webui/src/app/sections/conn_stats/conn_stats.controller.js new file mode 100644 index 0000000000..ecf6c53d9a --- /dev/null +++ b/webui/src/app/sections/conn_stats/conn_stats.controller.js @@ -0,0 +1,163 @@ +'use strict'; +var d3 = require('d3'); + +/** @ngInject */ +function ConnStatsController($scope, $interval, $log, $filter, ConnStats) { + var vm = this, + refreshInterval = 2000, // milliseconds + // Store and display data for the past hour + maxHistoricDataPointCount = 3600 / (refreshInterval / 1000), + dataPointStorageTime = 3600 * 1000; + + vm.graph = { + historicalConnCount: {}, + currentConnCount: {} + }; + + vm.graph.historicalConnCount.options = { + chart: { + type: 'lineChart', + height: 300, + margin: { + top: 20, + right: 40, + bottom: 40, + left: 55 + }, + xScale: d3.time.scale(), + xAxis: { + tickFormat: function (d) { + return d3.time.format('%X')(new Date(d)); + } + }, + forceY: [0, 1], // This prevents the chart from showing -1 on Oy when all the input data points + // have y = 0. It won't disable the automatic adjustment of the max value. + useInteractiveGuideline: true, + duration: 0 // Bug: Markers will not be drawn if you set this to some other value... + }, + title: { + enable: true, + text: 'Historical Connection Counts' + } + }; + + vm.graph.currentConnCount.options = { + chart: { + type: 'discreteBarChart', + height: 300, + margin: { + top: 20, + right: 40, + bottom: 40, + left: 55 + }, + x: function(d) { return d.label; }, + y: function(d) { return d.value; }, + valueFormat: d3.format('d'), + showValues: true, + staggerLabels: true + }, + title: { + enable: true, + text: 'Current Connection Counts' + } + }; + + function updateHistoricalConnCountGraph() { + var currentDate = Date.now(); + + angular.forEach(vm.connStats, function(provider, providerId) { + angular.forEach(provider.backends, function(backend, backendId) { + var dataPointKey = providerId + '/' + backendId, + newDataPoint = { + x: currentDate, + y: backend['total_conn'] + }; + + // Check if the new data point belongs to an existing plot + var existingPlot = $filter('filter')(vm.graph.historicalConnCount.data, {'key': dataPointKey}) + if (existingPlot.length > 0) { + // There should be only one + existingPlot[0].values.push(newDataPoint); + existingPlot[0].lastUpdated = currentDate; + } else { + vm.graph.historicalConnCount.data.push({ + values: [newDataPoint], + key: dataPointKey, + type: 'line', + lastUpdated: currentDate + }); + } + }); + }); + + // Limit plot data points + for (var i = 0; i < vm.graph.historicalConnCount.data.length; i++) { + var values = vm.graph.historicalConnCount.data[i].values; + if (values.length > maxHistoricDataPointCount) { + values.shift(); + } + + // Remove dead entries + if (values.length == 0 || + values[values.length - 1].lastUpdated < Date.now() - dataPointStorageTime) { + + vm.graph.historicalConnCount.data.splice(i, 1); + } + } + } + + function updateCurrentConnCountGraph() { + vm.graph.currentConnCount.data = [{ + values: [] + }]; + + angular.forEach(vm.connStats, function(provider, providerId) { + angular.forEach(provider.backends, function(backend, backendId) { + var dataPointLabel = providerId + '/' + backendId, + maxConnDataPoint = { + label: dataPointLabel, + value: backend['max_conn'] + }, + totalConnDataPoint = { + label: dataPointLabel, + value: backend['total_conn'] + }; + + vm.graph.currentConnCount.data[0].values.push(maxConnDataPoint); + vm.graph.currentConnCount.data[0].values.push(totalConnDataPoint); + }); + }); + } + + function loadData(connStats) { + // Set the current data point + vm.connStats = connStats; + + updateHistoricalConnCountGraph(); + updateCurrentConnCountGraph(); + } + + function errData(error) { + vm.connStats = {}; + $log.error(error); + } + + // Get the initial data points + vm.connStats = ConnStats.get(loadData, errData); + + // Initialize the view data for the historical connection count chart + vm.graph.historicalConnCount.data = []; + + var intervalId = $interval(function () { + ConnStats.get(loadData, errData); + }, + refreshInterval + ); + + $scope.$on('$destroy', function () { + $interval.cancel(intervalId); + }); +} + +module.exports = ConnStatsController; diff --git a/webui/src/app/sections/conn_stats/conn_stats.html b/webui/src/app/sections/conn_stats/conn_stats.html new file mode 100644 index 0000000000..61ba77082d --- /dev/null +++ b/webui/src/app/sections/conn_stats/conn_stats.html @@ -0,0 +1,14 @@ +
+
+
+
+ +
+
+
+
+ +
+
+
+
diff --git a/webui/src/app/sections/conn_stats/conn_stats.module.js b/webui/src/app/sections/conn_stats/conn_stats.module.js new file mode 100644 index 0000000000..aa2ed5e9fd --- /dev/null +++ b/webui/src/app/sections/conn_stats/conn_stats.module.js @@ -0,0 +1,24 @@ +'use strict'; +var angular = require('angular'); +var traefikCoreConnStats = require('../../core/conn_stats.resource'); +var ConnStatsController = require('./conn_stats.controller'); + +var traefikConnStats = 'traefik.conn_stats'; +module.exports = traefikConnStats; + +angular + .module(traefikConnStats, [traefikCoreConnStats]) + .controller('ConnStatsController', ConnStatsController) + .config(config); + + /** @ngInject */ + function config($stateProvider) { + + $stateProvider.state('conn_stats', { + url: '/conn_stats', + template: require('./conn_stats.html'), + controller: 'ConnStatsController', + controllerAs: 'connStatsCtrl' + }); + + } diff --git a/webui/src/app/sections/sections.js b/webui/src/app/sections/sections.js index 1dd926ff04..34c2ee9cab 100644 --- a/webui/src/app/sections/sections.js +++ b/webui/src/app/sections/sections.js @@ -4,6 +4,7 @@ require('nvd3'); var ndv3 = require('angular-nvd3'); var traefikSectionHealth = require('./health/health.module'); var traefikSectionProviders = require('./providers/providers.module'); +var traefikSectionConnStats = require('./conn_stats/conn_stats.module'); var traefikSection = 'traefik.section'; module.exports = traefikSection; @@ -14,7 +15,8 @@ angular 'ui.bootstrap', ndv3, traefikSectionProviders, - traefikSectionHealth + traefikSectionHealth, + traefikSectionConnStats ]) .config(config); diff --git a/webui/src/index.html b/webui/src/index.html index bec9ce4d5e..26ad2153d4 100644 --- a/webui/src/index.html +++ b/webui/src/index.html @@ -25,6 +25,7 @@