Skip to content
This repository was archived by the owner on Sep 26, 2018. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 142 additions & 0 deletions integration/conn_stats_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package main

import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"os"
"os/exec"
"strings"
"sync"
"time"

"github.com/containous/traefik/integration/utils"
"github.com/go-check/check"

checker "github.com/vdemeester/shakers"
)

// ConnStats test suites (using libcompose)
type ConnStatsSuite struct {
BaseSuite
dummyConnStats ConnStats
whoamiReq *http.Request
}

type ConnStats struct {
File struct {
Backends struct {
Whoami struct {
MaxConn int `json:"max_conn"`
TotalConn int `json:"total_conn"`
} `json:"whoami"`
} `json:"backends"`
} `json:"file"`
}

func (s *ConnStatsSuite) SetUpSuite(c *check.C) {
s.createComposeProject(c, "conn_stats")
s.composeProject.Start(c)

err := json.Unmarshal(
[]byte(`{"file":{"backends":{"whoami":{"max_conn":2,"total_conn":0}}}}`),
&s.dummyConnStats,
)
c.Assert(err, checker.IsNil)

s.whoamiReq, err = http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami", nil)
c.Assert(err, checker.IsNil)
}

func (s *ConnStatsSuite) TearDownSuite(c *check.C) {
if s.composeProject != nil {
s.composeProject.Stop(c)
}
}

func (s *ConnStatsSuite) ValidateWhoamiResponse(c *check.C, expectedResponseStatusCode int) {
whoamiResp, err := http.DefaultClient.Do(s.whoamiReq)
c.Assert(err, checker.IsNil)
c.Assert(whoamiResp.StatusCode, checker.Equals, expectedResponseStatusCode)
}

func (s *ConnStatsSuite) ValidateConnStats(c *check.C, totalConn int) {
resp, err := http.Get("http://127.0.0.1:8080/api/conn_stats")
c.Assert(err, checker.IsNil)

connStatsPayload, err := ioutil.ReadAll(resp.Body)
c.Assert(err, checker.IsNil)

var connStats ConnStats
err = json.Unmarshal(
connStatsPayload,
&connStats,
)
c.Assert(err, checker.IsNil)

s.dummyConnStats.File.Backends.Whoami.TotalConn = totalConn
c.Assert(connStats, checker.DeepEquals, s.dummyConnStats)
}

func (s *ConnStatsSuite) TestEndToEndWorkflow(c *check.C) {
whoamiHost := s.composeProject.Container(c, "whoami").NetworkSettings.IPAddress

file := s.adaptFile(c, "fixtures/conn_stats/simple.toml", struct {
Server string
}{whoamiHost})
defer os.Remove(file)
cmd := exec.Command(traefikBinary, "--configFile="+file)

err := cmd.Start()
c.Assert(err, checker.IsNil)
defer cmd.Process.Kill()

// Wait for traefik to start
err = utils.TryRequest(
"http://127.0.0.1:8080/api/providers",
60*time.Second,
func(res *http.Response) error {
body, err := ioutil.ReadAll(res.Body)
if err != nil {
return err
}

if !strings.Contains(string(body), "Path:/whoami") {
return errors.New("Incorrect traefik config: " + string(body))
}
return nil
},
)
c.Assert(err, checker.IsNil)

// Make sure we can reach the whoami backend server
s.ValidateWhoamiResponse(c, http.StatusOK)
s.ValidateConnStats(c, 0)

// Start two long running requests to the backend to reach the connection limit
whoamiWaitReq, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/whoami?wait=2s", nil)
c.Assert(err, checker.IsNil)
var wg sync.WaitGroup
wg.Add(2)
for i := 0; i < 2; i++ {
go func() {
resp, err := http.DefaultClient.Do(whoamiWaitReq)
c.Assert(err, checker.IsNil)
c.Assert(resp.StatusCode, checker.Equals, http.StatusOK)
wg.Done()
}()
}

// Wait a bit for the backend to become saturated
time.Sleep(time.Second * 1)

s.ValidateWhoamiResponse(c, http.StatusTooManyRequests)
s.ValidateConnStats(c, 2)

// Wait for the backend to become available again
wg.Wait()

s.ValidateWhoamiResponse(c, http.StatusOK)
s.ValidateConnStats(c, 0)
}
25 changes: 25 additions & 0 deletions integration/fixtures/conn_stats/simple.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
defaultEntryPoints = ["http"]

logLevel = "DEBUG"

[entryPoints]
[entryPoints.http]
address = ":8000"

[web]
address = ":8080"

[file]
[frontends]
[frontends.whoami]
backend = "whoami"
[frontends.whoami.routes.route]
rule = "Path:/whoami"

[backends]
[backends.whoami]
[backends.whoami.maxconn]
amount = 2
extractorfunc = "request.host"
[backends.whoami.servers.server]
url = "http://{{.Server}}:80"
1 change: 1 addition & 0 deletions integration/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ func init() {
check.Suite(&EurekaSuite{})
check.Suite(&AcmeSuite{})
check.Suite(&DynamoDBSuite{})
check.Suite(&ConnStatsSuite{})
}

var traefikBinary = "../dist/traefik"
Expand Down
2 changes: 2 additions & 0 deletions integration/resources/compose/conn_stats.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
whoami:
image: emilevauge/whoami
7 changes: 6 additions & 1 deletion server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down
81 changes: 81 additions & 0 deletions web.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"reflect"
"runtime"

"github.com/codegangsta/negroni"
Expand All @@ -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 (
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am using these reflection hacks for now, but I'll talk with the project maintainers about customising their oxy fork to expose the totalConnections field (and maybe add more stats). Not sure what they're planning to do with that fork, considering that the maintainers of vulcand/oxy aren't very responsive.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just FYI, I noticed a while back their vendored oxy is different than the main oxy so I think they're already doing some customization.

Copy link
Author

@mihaitodor mihaitodor May 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, I can send them a PR for that lib too, but let's first see if they're interested in it and if they want any changes in the design. Before I send them the PR, I was hoping to get it running in our system to make sure everything is fine. There are loads of moving pieces...

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
}
2 changes: 2 additions & 0 deletions webui/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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`.

Expand Down
14 changes: 14 additions & 0 deletions webui/src/app/core/conn_stats.resource.js
Original file line number Diff line number Diff line change
@@ -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');
}
Loading