Files
glauth/v2/pkg/frontend/webapi.go
2023-07-22 13:02:24 -07:00

183 lines
4.2 KiB
Go

package frontend
import (
"encoding/json"
"fmt"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog"
"io"
"log"
"net/http"
"regexp"
"strings"
"github.com/glauth/glauth/v2/pkg/assets"
)
// RunAPI provides a basic REST API
func RunAPI(opts ...Option) {
options := newOptions(opts...)
log := options.Logger
cfg := options.Config
register(http.DefaultServeMux, log, cfg.TLS, cfg.Listen)
if cfg.TLS {
log.Info().Str("address", cfg.Listen).Msg("Starting HTTPS server")
err := http.ListenAndServeTLS(cfg.Listen, cfg.Cert, cfg.Key, nil)
if err != nil {
log.Error().Err(err).Msg("error starting HTTPS server")
return
}
} else {
log.Info().Str("address", cfg.Listen).Msg("Starting HTTP server")
err := http.ListenAndServe(cfg.Listen, nil)
if err != nil {
log.Error().Err(err).Msg("error starting HTTP server")
return
}
}
}
func register(mux *http.ServeMux, log zerolog.Logger, tls bool, listen string) {
fs := http.FileServer(http.FS(assets.Content))
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Info().Str("path", r.URL.Path).Msg("Web")
if r.URL.Path != "/" {
log.Info().Msg("Web 404")
http.NotFound(w, r)
return
}
fs.ServeHTTP(w, r)
})
mux.Handle("/assets/", http.StripPrefix("/assets/", fs))
var url string
if tls {
url = fmt.Sprintf("https://%s/debug/vars", listen)
} else {
url = fmt.Sprintf("http://%s/debug/vars", listen)
}
c := &collector{
url: url,
names: map[string]string{
"proxy": "glauth_proxy",
"proxy_frontend": "glauth_proxy_frontend",
"proxy_backend": "glauth_proxy_backend",
},
helps: map[string]string{
"proxy": "General Metrics",
"proxy_frontend": "Frontend Metrics",
"proxy_backend": "Backend Metrics",
},
labelName: map[string]string{
"proxy": "metric",
"proxy_frontend": "metric",
"proxy_backend": "metric",
},
}
prometheus.MustRegister(c)
mux.Handle("/metrics", promhttp.Handler())
}
// The code below liberally borrows from https://github.com/albertito/prometheus-expvar-exporter
// and is adapted (just a little) for our purpose.
type collector struct {
url string
names map[string]string
helps map[string]string
labelName map[string]string
}
func (c *collector) Describe(ch chan<- *prometheus.Desc) {
return
}
func (c *collector) Collect(ch chan<- prometheus.Metric) {
resp, err := http.Get(c.url)
if err != nil {
log.Printf("Error scraping %q: %v", c.url, err)
return
}
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Printf("Error reading body of %q: %v", c.url, err)
return
}
// Replace "\xNN" with "?" because the default parser doesn't handle them
// well.
re := regexp.MustCompile(`\\x..`)
body = re.ReplaceAllFunc(body, func(s []byte) []byte {
return []byte("?")
})
var vs map[string]interface{}
err = json.Unmarshal(body, &vs)
if err != nil {
log.Printf("Error unmarshalling json from %q: %v", c.url, err)
return
}
for k, v := range vs {
name := strings.ReplaceAll(k, "/", "_")
if n, ok := c.names[k]; ok {
name = n
}
help := fmt.Sprintf("expvar %q", k)
if h, ok := c.helps[k]; ok {
help = h
}
lnames := []string{}
if ln, ok := c.labelName[k]; ok {
lnames = append(lnames, ln)
}
desc := prometheus.NewDesc(name, help, lnames, nil)
switch v := v.(type) {
case float64:
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue, v)
case bool:
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue,
valToFloat(v))
case map[string]interface{}:
// We only support explicitly written label names.
if len(lnames) != 1 {
continue
}
for lk, lv := range v {
ch <- prometheus.MustNewConstMetric(desc, prometheus.UntypedValue,
valToFloat(lv), lk)
}
case string:
// Not supported by Prometheus.
continue
case []interface{}:
// Not supported by Prometheus.
continue
default:
continue
}
}
}
func valToFloat(v interface{}) float64 {
switch v := v.(type) {
case float64:
return v
case bool:
if v {
return 1.0
}
return 0.0
case string:
return 0.0
}
log.Printf("unexpected value type: %#v", v)
return 0.0
}