123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272 |
- package main
- import (
- "encoding/json"
- "fmt"
- "html/template"
- "io"
- "log"
- "net/http"
- "runtime"
- "sort"
- "strconv"
- "time"
- "github.com/abh/geodns/monitor"
- "github.com/abh/geodns/zones"
- metrics "github.com/rcrowley/go-metrics"
- )
- type httpServer struct {
- mux *http.ServeMux
- zones zones.Zones
- serverInfo monitor.ServerInfo
- }
- type rate struct {
- Name string
- Count int64
- Metrics zones.ZoneMetrics
- }
- type Rates []*rate
- func (s Rates) Len() int { return len(s) }
- func (s Rates) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
- type RatesByCount struct{ Rates }
- func (s RatesByCount) Less(i, j int) bool {
- ic := s.Rates[i].Count
- jc := s.Rates[j].Count
- if ic == jc {
- return s.Rates[i].Name < s.Rates[j].Name
- }
- return ic > jc
- }
- type histogramData struct {
- Max int64
- Min int64
- Mean float64
- Pct90 float64
- Pct99 float64
- Pct999 float64
- StdDev float64
- }
- func setupHistogramData(met metrics.Histogram, dat *histogramData) {
- dat.Max = met.Max()
- dat.Min = met.Min()
- dat.Mean = met.Mean()
- dat.StdDev = met.StdDev()
- percentiles := met.Percentiles([]float64{0.90, 0.99, 0.999})
- dat.Pct90 = percentiles[0]
- dat.Pct99 = percentiles[1]
- dat.Pct999 = percentiles[2]
- }
- func topParam(req *http.Request, def int) int {
- req.ParseForm()
- topOption := def
- topParam := req.Form["top"]
- if len(topParam) > 0 {
- var err error
- topOption, err = strconv.Atoi(topParam[0])
- if err != nil {
- topOption = def
- }
- }
- return topOption
- }
- func NewHTTPServer(zones zones.Zones) *httpServer {
- hs := &httpServer{
- // todo: zones.MuxManager instead of zones?
- zones: zones,
- mux: &http.ServeMux{},
- }
- hs.mux.HandleFunc("/status", hs.StatusHandler())
- hs.mux.HandleFunc("/status.json", hs.StatusJSONHandler())
- hs.mux.HandleFunc("/", hs.mainServer)
- return hs
- }
- func (hs *httpServer) Mux() *http.ServeMux {
- return hs.mux
- }
- func (hs *httpServer) Run(listen string) {
- log.Println("Starting HTTP interface on", listen)
- log.Fatal(http.ListenAndServe(listen, &basicauth{h: hs.mux}))
- }
- func (hs *httpServer) StatusJSONHandler() func(http.ResponseWriter, *http.Request) {
- info := serverInfo
- return func(w http.ResponseWriter, req *http.Request) {
- zonemetrics := make(map[string]metrics.Registry)
- for name, zone := range hs.zones {
- zone.Lock()
- zonemetrics[name] = zone.Metrics.Registry
- zone.Unlock()
- }
- type statusData struct {
- Version string
- GoVersion string
- Uptime int64
- Platform string
- Zones map[string]metrics.Registry
- Global metrics.Registry
- ID string
- IP string
- UUID string
- Groups []string
- }
- uptime := int64(time.Since(info.Started).Seconds())
- status := statusData{
- Version: info.Version,
- GoVersion: runtime.Version(),
- Uptime: uptime,
- Platform: runtime.GOARCH + "-" + runtime.GOOS,
- Zones: zonemetrics,
- Global: metrics.DefaultRegistry,
- ID: hs.serverInfo.ID,
- IP: hs.serverInfo.IP,
- UUID: hs.serverInfo.UUID,
- Groups: hs.serverInfo.Groups,
- }
- b, err := json.Marshal(status)
- if err != nil {
- http.Error(w, "Error encoding JSON", 500)
- return
- }
- w.Header().Set("Content-Type", "application/json")
- w.Write(b)
- return
- }
- }
- func (hs *httpServer) StatusHandler() func(http.ResponseWriter, *http.Request) {
- return func(w http.ResponseWriter, req *http.Request) {
- topOption := topParam(req, 10)
- rates := make(Rates, 0)
- for name, zone := range hs.zones {
- count := zone.Metrics.Queries.Count()
- rates = append(rates, &rate{
- Name: name,
- Count: count,
- Metrics: zone.Metrics,
- })
- }
- sort.Sort(RatesByCount{rates})
- type statusData struct {
- Version string
- Zones Rates
- Uptime DayDuration
- Platform string
- Global struct {
- Queries metrics.Meter
- Histogram histogramData
- HistogramRecent histogramData
- }
- TopOption int
- }
- uptime := DayDuration{time.Since(hs.serverInfo.Started)}
- status := statusData{
- Version: VERSION,
- Zones: rates,
- Uptime: uptime,
- Platform: runtime.GOARCH + "-" + runtime.GOOS,
- TopOption: topOption,
- }
- status.Global.Queries = metrics.Get("queries").(*metrics.StandardMeter).Snapshot()
- setupHistogramData(metrics.Get("queries-histogram").(*metrics.StandardHistogram).Snapshot(), &status.Global.Histogram)
- statusTemplate, err := FSString(development, "/templates/status.html")
- if err != nil {
- log.Println("Could not read template:", err)
- w.WriteHeader(500)
- return
- }
- tmpl, err := template.New("status_html").Parse(statusTemplate)
- if err != nil {
- str := fmt.Sprintf("Could not parse template: %s", err)
- io.WriteString(w, str)
- return
- }
- err = tmpl.Execute(w, status)
- if err != nil {
- log.Println("Status template error", err)
- }
- }
- }
- func (hs *httpServer) mainServer(w http.ResponseWriter, req *http.Request) {
- if req.RequestURI != "/version" {
- http.NotFound(w, req)
- return
- }
- io.WriteString(w, `<html><head><title>GeoDNS `+
- hs.serverInfo.Version+`</title><body>`+
- `GeoDNS Server`+
- `</body></html>`)
- }
- type basicauth struct {
- h http.Handler
- }
- func (b *basicauth) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- // don't request passwords for the websocket interface (for now)
- // because 'wscat' doesn't support that.
- if r.RequestURI == "/monitor" {
- b.h.ServeHTTP(w, r)
- return
- }
- cfgMutex.RLock()
- user := Config.HTTP.User
- password := Config.HTTP.Password
- cfgMutex.RUnlock()
- if len(user) == 0 {
- b.h.ServeHTTP(w, r)
- return
- }
- ruser, rpass, ok := r.BasicAuth()
- if ok {
- if ruser == user && rpass == password {
- b.h.ServeHTTP(w, r)
- return
- }
- }
- w.Header().Set("WWW-Authenticate", fmt.Sprintf(`Basic realm=%q`, "GeoDNS Status"))
- http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
- return
- }
|