Skip to content
This repository has been archived by the owner on Dec 7, 2020. It is now read-only.

Commit

Permalink
Metrics (#324)
Browse files Browse the repository at this point in the history
- adding additional metrics and instrumentation
- adding metrics covering the latency on providers interctions
- adding metrics for tokens issues, renewed and exchanged
  • Loading branch information
gambol99 authored Mar 4, 2018
1 parent b6244d2 commit c878552
Show file tree
Hide file tree
Showing 7 changed files with 79 additions and 29 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@

FEATURES:
* Added a --enable-default-deny option to make denial by default [#PR320](https://github.com/gambol99/keycloak-proxy/pull/320)
* Added spelling check to the tests [#PR322](https://github.com/gambol99/keycloak-proxy/pull/322)
* Added the X-Auth-Audience to the upstream headers [#PR319](https://github.com/gambol99/keycloak-proxy/pull/319)
* Added the ability to control the timeout on the initial openid configuration from .well-known/openid-configuration [#PR315](https://github.com/gambol99/keycloak-proxy/pull/315)
* Adding additional metrics covering provider request latency, token breakdown [#PR324](https://github.com/gambol99/keycloak-proxy/pull/324)
* Changed the upstream-keepalive to default to true [#PR321](https://github.com/gambol99/keycloak-proxy/pull/321)
* Updated the docker base image alpine 3.7 [#PR313](https://github.com/gambol99/keycloak-proxy/pull/313)
* Updated to Golang version 1.10 [#PR316](https://github.com/gambol99/keycloak-proxy/pull/316)
Expand Down
31 changes: 31 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"time"

"github.com/gambol99/go-oidc/jose"
"github.com/prometheus/client_golang/prometheus"
)

var (
Expand Down Expand Up @@ -73,6 +74,36 @@ const (
headerXRequestID = "X-Request-ID"
)

var (
oauthTokensMetric = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "proxy_oauth_tokens_total",
Help: "A summary of the tokens issuesd, renewed or failed logins",
},
[]string{"action"},
)
oauthLatencyMetric = prometheus.NewSummaryVec(
prometheus.SummaryOpts{
Name: "proxy_oauth_request_latency_sec",
Help: "A summary of the request latancy for requests against the openid provider",
},
[]string{"action"},
)
latencyMetric = prometheus.NewSummary(
prometheus.SummaryOpts{
Name: "proxy_request_duration_sec",
Help: "A summary of the http request latency for proxy requests",
},
)
statusMetric = prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "proxy_request_status_total",
Help: "The HTTP requests partitioned by status code",
},
[]string{"code", "method"},
)
)

var (
// ErrSessionNotFound no session found in the request
ErrSessionNotFound = errors.New("authentication session not found")
Expand Down
16 changes: 16 additions & 0 deletions handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"time"

"github.com/gambol99/go-oidc/oauth2"

"github.com/pressly/chi"
"go.uber.org/zap"
)
Expand Down Expand Up @@ -161,6 +162,9 @@ func (r *oauthProxy) oauthCallbackHandler(w http.ResponseWriter, req *http.Reque
zap.String("expires", identity.ExpiresAt.Format(time.RFC3339)),
zap.String("duration", time.Until(identity.ExpiresAt).String()))

// @metric a token has beeb issued
oauthTokensMetric.WithLabelValues("issued").Inc()

// step: does the response has a refresh token and we are NOT ignore refresh tokens?
if r.config.EnableRefreshTokens && resp.RefreshToken != "" {
var encrypted string
Expand Down Expand Up @@ -224,13 +228,16 @@ func (r *oauthProxy) loginHandler(w http.ResponseWriter, req *http.Request) {
return "unable to create the oauth client for user_credentials request", http.StatusInternalServerError, err
}

start := time.Now()
token, err := client.UserCredsToken(username, password)
if err != nil {
if strings.HasPrefix(err.Error(), oauth2.ErrorInvalidGrant) {
return "invalid user credentials provided", http.StatusUnauthorized, err
}
return "unable to request the access token via grant_type 'password'", http.StatusInternalServerError, err
}
// @metric observe the time taken for a login request
oauthLatencyMetric.WithLabelValues("login").Observe(time.Since(start).Seconds())

_, identity, err := parseToken(token.AccessToken)
if err != nil {
Expand All @@ -239,6 +246,9 @@ func (r *oauthProxy) loginHandler(w http.ResponseWriter, req *http.Request) {

r.dropAccessTokenCookie(req, w, token.AccessToken, time.Until(identity.ExpiresAt))

// @metric a token has been issued
oauthTokensMetric.WithLabelValues("login").Inc()

w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(tokenResponse{
IDToken: token.IDToken,
Expand Down Expand Up @@ -286,6 +296,9 @@ func (r *oauthProxy) logoutHandler(w http.ResponseWriter, req *http.Request) {
}
r.clearAllCookies(req, w)

// @metric increment the logout counter
oauthTokensMetric.WithLabelValues("logout").Inc()

// step: check if the user has a state session and if so revoke it
if r.useStore() {
go func() {
Expand Down Expand Up @@ -322,15 +335,18 @@ func (r *oauthProxy) logoutHandler(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
return
}

// step: add the authentication headers and content-type
request.SetBasicAuth(encodedID, encodedSecret)
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")

start := time.Now()
response, err := client.HttpClient().Do(request)
if err != nil {
r.log.Error("unable to post to revocation endpoint", zap.Error(err))
return
}
oauthLatencyMetric.WithLabelValues("revocation").Observe(time.Since(start).Seconds())

// step: check the response
switch response.StatusCode {
Expand Down
29 changes: 6 additions & 23 deletions middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"github.com/PuerkitoBio/purell"
"github.com/gambol99/go-oidc/jose"
"github.com/go-chi/chi/middleware"
"github.com/prometheus/client_golang/prometheus"
"github.com/unrolled/secure"
"go.uber.org/zap"
)
Expand All @@ -49,11 +48,16 @@ func entrypointMiddleware(next http.Handler) http.Handler {
req.RequestURI = req.URL.RawPath
req.URL.RawPath = req.URL.Path

// continue the flow
// @step: create a context for the request
scope := &RequestScope{}
resp := middleware.NewWrapResponseWriter(w, 1)
start := time.Now()
next.ServeHTTP(resp, req.WithContext(context.WithValue(req.Context(), contextScopeName, scope)))

// @metric record the time taken then response code
latencyMetric.Observe(time.Since(start).Seconds())
statusMetric.WithLabelValues(fmt.Sprintf("%d", resp.Status()), req.Method).Inc()

// place back the original uri for proxying request
req.URL.Path = keep
req.URL.RawPath = keep
Expand All @@ -78,27 +82,6 @@ func (r *oauthProxy) loggingMiddleware(next http.Handler) http.Handler {
})
}

// metricsMiddleware is responsible for collecting metrics
func (r *oauthProxy) metricsMiddleware(next http.Handler) http.Handler {
r.log.Info("enabled the service metrics middleware, available on", zap.String("path", fmt.Sprintf("%s%s", oauthURL, metricsURL)))

statusMetrics := prometheus.NewCounterVec(
prometheus.CounterOpts{
Name: "http_request_total",
Help: "The HTTP requests partitioned by status code",
},
[]string{"code", "method"},
)
prometheus.MustRegister(statusMetrics)

return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
resp := w.(middleware.WrapResponseWriter)
statusMetrics.WithLabelValues(fmt.Sprintf("%d", resp.Status()), req.Method).Inc()

next.ServeHTTP(w, req)
})
}

// authenticationMiddleware is responsible for verifying the access token
func (r *oauthProxy) authenticationMiddleware(resource *Resource) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
Expand Down
2 changes: 1 addition & 1 deletion middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ func TestMetricsMiddleware(t *testing.T) {
{
URI: oauthURL + metricsURL,
ExpectedCode: http.StatusOK,
ExpectedContentContains: "http_request_total",
ExpectedContentContains: "proxy_request_status_total",
},
{
URI: oauthURL + metricsURL,
Expand Down
17 changes: 16 additions & 1 deletion oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,22 @@ func getUserinfo(client *oauth2.Client, endpoint string, token string) (jose.Cla

// getToken retrieves a code from the provider, extracts and verified the token
func getToken(client *oauth2.Client, grantType, code string) (oauth2.TokenResponse, error) {
return client.RequestToken(grantType, code)
start := time.Now()
token, err := client.RequestToken(grantType, code)
if err != nil {
return token, err
}
taken := time.Since(start).Seconds()
switch grantType {
case oauth2.GrantTypeAuthCode:
oauthTokensMetric.WithLabelValues("exchange").Inc()
oauthLatencyMetric.WithLabelValues("exchange").Observe(taken)
case oauth2.GrantTypeRefreshToken:
oauthTokensMetric.WithLabelValues("renew").Inc()
oauthLatencyMetric.WithLabelValues("renew").Observe(taken)
}

return token, err
}

// parseToken retrieve the user identity from the token
Expand Down
11 changes: 7 additions & 4 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import (

httplog "log"

"github.com/armon/go-proxyproto"
proxyproto "github.com/armon/go-proxyproto"
"github.com/gambol99/go-oidc/oidc"
"github.com/gambol99/goproxy"
"github.com/pressly/chi"
Expand Down Expand Up @@ -65,6 +65,11 @@ type oauthProxy struct {
func init() {
time.LoadLocation("UTC") // ensure all time is in UTC
runtime.GOMAXPROCS(runtime.NumCPU()) // set the core
// @step: register the instrumentation
prometheus.MustRegister(latencyMetric)
prometheus.MustRegister(oauthLatencyMetric)
prometheus.MustRegister(oauthTokensMetric)
prometheus.MustRegister(statusMetric)
}

// newProxy create's a new proxy from configuration
Expand Down Expand Up @@ -161,9 +166,6 @@ func (r *oauthProxy) createReverseProxy() error {
if r.config.EnableLogging {
engine.Use(r.loggingMiddleware)
}
if r.config.EnableMetrics {
engine.Use(r.metricsMiddleware)
}
if r.config.EnableSecurityFilter {
engine.Use(r.securityMiddleware)
}
Expand Down Expand Up @@ -194,6 +196,7 @@ func (r *oauthProxy) createReverseProxy() error {
e.Get(tokenURL, r.tokenHandler)
e.Post(loginURL, r.loginHandler)
if r.config.EnableMetrics {
r.log.Info("enabled the service metrics middleware, available on", zap.String("path", fmt.Sprintf("%s%s", oauthURL, metricsURL)))
e.Get(metricsURL, r.proxyMetricsHandler)
}
})
Expand Down

0 comments on commit c878552

Please sign in to comment.