120 lines
3.5 KiB
Go
120 lines
3.5 KiB
Go
// Copyright 2015 ISRG. All rights reserved
|
|
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
|
|
package metrics
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/cactus/go-statsd-client/statsd"
|
|
"github.com/letsencrypt/boulder/Godeps/_workspace/src/github.com/jmhodges/clock"
|
|
)
|
|
|
|
// HTTPMonitor stores some server state
|
|
type HTTPMonitor struct {
|
|
stats statsd.Statter
|
|
statsPrefix string
|
|
handler http.Handler
|
|
connectionsInFlight int64
|
|
}
|
|
|
|
// NewHTTPMonitor returns a new initialized HTTPMonitor
|
|
func NewHTTPMonitor(stats statsd.Statter, handler http.Handler, prefix string) HTTPMonitor {
|
|
return HTTPMonitor{
|
|
stats: stats,
|
|
handler: handler,
|
|
statsPrefix: prefix,
|
|
connectionsInFlight: 0,
|
|
}
|
|
}
|
|
|
|
// Handle wraps handlers and records various metrics about requests to these handlers
|
|
// and sends them to StatsD
|
|
func (h *HTTPMonitor) Handle() http.Handler {
|
|
return http.HandlerFunc(h.watchAndServe)
|
|
}
|
|
|
|
func (h *HTTPMonitor) watchAndServe(w http.ResponseWriter, r *http.Request) {
|
|
h.stats.Inc(fmt.Sprintf("%s.HTTP.Rate", h.statsPrefix), 1, 1.0)
|
|
inFlight := atomic.AddInt64(&h.connectionsInFlight, 1)
|
|
h.stats.Gauge(fmt.Sprintf("%s.HTTP.OpenConnections", h.statsPrefix), inFlight, 1.0)
|
|
|
|
cOpened := time.Now()
|
|
h.handler.ServeHTTP(w, r)
|
|
cClosed := time.Since(cOpened)
|
|
|
|
inFlight = atomic.AddInt64(&h.connectionsInFlight, -1)
|
|
h.stats.Gauge(fmt.Sprintf("%s.HTTP.ConnectionsInFlight", h.statsPrefix), inFlight, 1.0)
|
|
|
|
endpoint := ""
|
|
// If request fails don't record the endpoint as an attacker could use this to
|
|
// eat up all our memory by just hitting 404s all day
|
|
if w.Header().Get("Content-Type") == "application/problem+json" {
|
|
endpoint = "Failed"
|
|
} else {
|
|
// If r.URL has more than two segments throw the rest away to simplify metrics
|
|
segments := strings.Split(r.URL.Path, "/")
|
|
if len(segments) > 3 {
|
|
segments = segments[:3]
|
|
}
|
|
endpoint = strings.Join(segments, "/")
|
|
}
|
|
h.stats.TimingDuration(fmt.Sprintf("%s.HTTP.ResponseTime.%s", h.statsPrefix, endpoint), cClosed, 1.0)
|
|
}
|
|
|
|
// FBAdapter provides a facebookgo/stats client interface that sends metrics via
|
|
// a StatsD client
|
|
type FBAdapter struct {
|
|
stats statsd.Statter
|
|
prefix string
|
|
clk clock.Clock
|
|
}
|
|
|
|
// NewFBAdapter returns a new adapter
|
|
func NewFBAdapter(stats statsd.Statter, prefix string, clock clock.Clock) FBAdapter {
|
|
return FBAdapter{stats: stats, prefix: prefix, clk: clock}
|
|
}
|
|
|
|
// BumpAvg is essentially statsd.Statter.Gauge
|
|
func (fba FBAdapter) BumpAvg(key string, val float64) {
|
|
fba.stats.Gauge(fmt.Sprintf("%s.%s", fba.prefix, key), int64(val), 1.0)
|
|
}
|
|
|
|
// BumpSum is essentially statsd.Statter.Inc (httpdown only ever uses positive
|
|
// deltas)
|
|
func (fba FBAdapter) BumpSum(key string, val float64) {
|
|
fba.stats.Inc(fmt.Sprintf("%s.%s", fba.prefix, key), int64(val), 1.0)
|
|
}
|
|
|
|
type btHolder struct {
|
|
key string
|
|
stats statsd.Statter
|
|
started time.Time
|
|
}
|
|
|
|
func (bth btHolder) End() {
|
|
bth.stats.TimingDuration(bth.key, time.Since(bth.started), 1.0)
|
|
}
|
|
|
|
// BumpTime is essentially a (much better) statsd.Statter.TimingDuration
|
|
func (fba FBAdapter) BumpTime(key string) interface {
|
|
End()
|
|
} {
|
|
return btHolder{
|
|
key: fmt.Sprintf("%s.%s", fba.prefix, key),
|
|
started: fba.clk.Now(),
|
|
stats: fba.stats,
|
|
}
|
|
}
|
|
|
|
// BumpHistogram isn't used by facebookgo/httpdown
|
|
func (fba FBAdapter) BumpHistogram(_ string, _ float64) {
|
|
return
|
|
}
|