mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
We can't set any headers after c.Abort() was called (which will happen in the static files middleware if file was found) so we need to refactor this code. New logic will be: set all headers early, if file is found response will include those, later add second middleware that clears those headers, it will only be hit if file wasn't found, so we'll clear headers on all 404s
237 lines
7.0 KiB
Go
237 lines
7.0 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"html/template"
|
|
"net/http"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/prymitive/karma/internal/alertmanager"
|
|
"github.com/prymitive/karma/internal/config"
|
|
"github.com/prymitive/karma/internal/models"
|
|
"github.com/prymitive/karma/internal/transform"
|
|
|
|
"github.com/DeanThompson/ginpprof"
|
|
"github.com/gin-contrib/cors"
|
|
"github.com/gin-contrib/gzip"
|
|
"github.com/gin-contrib/static"
|
|
"github.com/gin-gonic/contrib/sentry"
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/spf13/pflag"
|
|
|
|
raven "github.com/getsentry/raven-go"
|
|
ginprometheus "github.com/mcuadros/go-gin-prometheus"
|
|
cache "github.com/patrickmn/go-cache"
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
var (
|
|
version = "dev"
|
|
|
|
// ticker is a timer used by background loop that will keep pulling
|
|
// data from Alertmanager
|
|
ticker *time.Ticker
|
|
|
|
// apiCache will be used to keep short lived copy of JSON reponses generated for the UI
|
|
// If there are requests with the same filter we should respond from cache
|
|
// rather than do all the filtering every time
|
|
apiCache *cache.Cache
|
|
|
|
staticBuildFileSystem = newBinaryFileSystem("ui/build")
|
|
staticSrcFileSystem = newBinaryFileSystem("ui/src")
|
|
)
|
|
|
|
func getViewURL(sub string) string {
|
|
u := path.Join(config.Config.Listen.Prefix, sub)
|
|
if strings.HasSuffix(sub, "/") && !strings.HasSuffix(u, "/") {
|
|
// if sub path had trailing slash then add it here, since path.Join will
|
|
// skip it
|
|
return u + "/"
|
|
}
|
|
return u
|
|
}
|
|
|
|
func setupRouter(router *gin.Engine) {
|
|
router.Use(gzip.Gzip(gzip.DefaultCompression))
|
|
|
|
router.Use(setStaticHeaders(getViewURL("/static/")))
|
|
router.Use(static.Serve(getViewURL("/"), staticBuildFileSystem))
|
|
// next 2 lines are to allow service raw sources so sentry can fetch source maps
|
|
router.Use(static.Serve(getViewURL("/static/js/"), staticSrcFileSystem))
|
|
// FIXME
|
|
// compressed sources are under /static/js/main.js and reference ../static/js/main.js
|
|
// so we end up with /static/static/js
|
|
router.Use(static.Serve(getViewURL("/static/static/js/"), staticSrcFileSystem))
|
|
router.Use(clearStaticHeaders(getViewURL("/static/")))
|
|
|
|
router.Use(cors.New(cors.Config{
|
|
// This works different than AllowAllOrigins=true
|
|
// 1. AllowAllOrigins will cause responses to include
|
|
// 'Access-Control-Allow-Origin: *' header in all responses
|
|
// 2. Setting AllowOriginFunc allows to validate origin URI and if it passes
|
|
// the response will include 'Access-Control-Allow-Origin: $origin'
|
|
// So the logic is the same, but implementation is different.
|
|
// We need second behavior since setting `credentials: include` on JS
|
|
// fetch() will fail with 'Access-Control-Allow-Origin: *' responses
|
|
AllowOriginFunc: func(origin string) bool {
|
|
return true
|
|
},
|
|
AllowCredentials: true,
|
|
AllowMethods: []string{"GET", "POST", "DELETE"},
|
|
AllowHeaders: []string{"Origin"},
|
|
ExposeHeaders: []string{"Content-Length"},
|
|
}))
|
|
|
|
router.GET(getViewURL("/"), index)
|
|
router.GET(getViewURL("/alerts.json"), alerts)
|
|
router.GET(getViewURL("/autocomplete.json"), autocomplete)
|
|
router.GET(getViewURL("/labelNames.json"), knownLabelNames)
|
|
router.GET(getViewURL("/labelValues.json"), knownLabelValues)
|
|
|
|
router.GET(getViewURL("/custom.css"), func(c *gin.Context) {
|
|
serveFileOr404(config.Config.Custom.CSS, "text/css", c)
|
|
})
|
|
router.GET(getViewURL("/custom.js"), func(c *gin.Context) {
|
|
serveFileOr404(config.Config.Custom.JS, "application/javascript", c)
|
|
})
|
|
|
|
router.NoRoute(notFound)
|
|
}
|
|
|
|
func setupUpstreams() {
|
|
for _, s := range config.Config.Alertmanager.Servers {
|
|
|
|
var httpTransport http.RoundTripper
|
|
var err error
|
|
// if either TLS root CA or client cert is configured then initialize custom transport where we have this setup
|
|
if s.TLS.CA != "" || s.TLS.Cert != "" || s.TLS.InsecureSkipVerify {
|
|
httpTransport, err = alertmanager.NewHTTPTransport(s.TLS.CA, s.TLS.Cert, s.TLS.Key, s.TLS.InsecureSkipVerify)
|
|
if err != nil {
|
|
log.Fatalf("Failed to create HTTP transport for Alertmanager '%s' with URI '%s': %s", s.Name, s.URI, err)
|
|
}
|
|
}
|
|
|
|
am, err := alertmanager.NewAlertmanager(
|
|
s.Name,
|
|
s.URI,
|
|
alertmanager.WithRequestTimeout(s.Timeout),
|
|
alertmanager.WithProxy(s.Proxy),
|
|
alertmanager.WithHTTPTransport(httpTransport), // we will pass a nil unless TLS.CA or TLS.Cert is set
|
|
alertmanager.WithHTTPHeaders(s.Headers),
|
|
)
|
|
if err != nil {
|
|
log.Fatalf("Failed to create Alertmanager '%s' with URI '%s': %s", s.Name, s.URI, err)
|
|
}
|
|
err = alertmanager.RegisterAlertmanager(am)
|
|
if err != nil {
|
|
log.Fatalf("Failed to register Alertmanager '%s' with URI '%s': %s", s.Name, s.URI, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func setupLogger() {
|
|
switch config.Config.Log.Level {
|
|
case "debug":
|
|
log.SetLevel(log.DebugLevel)
|
|
case "info":
|
|
log.SetLevel(log.InfoLevel)
|
|
case "warning":
|
|
log.SetLevel(log.WarnLevel)
|
|
case "error":
|
|
log.SetLevel(log.ErrorLevel)
|
|
case "fatal":
|
|
log.SetLevel(log.FatalLevel)
|
|
case "panic":
|
|
log.SetLevel(log.PanicLevel)
|
|
default:
|
|
log.Fatalf("Unknown log level '%s'", config.Config.Log.Level)
|
|
}
|
|
}
|
|
|
|
func main() {
|
|
printVersion := pflag.Bool("version", false, "Print version and exit")
|
|
pflag.Parse()
|
|
if *printVersion {
|
|
fmt.Println(version)
|
|
return
|
|
}
|
|
|
|
config.Config.Read()
|
|
setupLogger()
|
|
|
|
// timer duration cannot be zero second or a negative one
|
|
if config.Config.Alertmanager.Interval <= time.Second*0 {
|
|
log.Fatalf("Invalid AlertmanagerTTL value '%v'", config.Config.Alertmanager.Interval)
|
|
}
|
|
|
|
log.Infof("Version: %s", version)
|
|
if config.Config.Log.Config {
|
|
config.Config.LogValues()
|
|
}
|
|
|
|
jiraRules := []models.JiraRule{}
|
|
for _, rule := range config.Config.JIRA {
|
|
jiraRules = append(jiraRules, models.JiraRule{Regex: rule.Regex, URI: rule.URI})
|
|
}
|
|
transform.ParseRules(jiraRules)
|
|
|
|
apiCache = cache.New(cache.NoExpiration, 10*time.Second)
|
|
|
|
setupUpstreams()
|
|
|
|
if len(alertmanager.GetAlertmanagers()) == 0 {
|
|
log.Fatal("No valid Alertmanager URIs defined")
|
|
}
|
|
|
|
// before we start try to fetch data from Alertmanager
|
|
log.Info("Initial Alertmanager query")
|
|
pullFromAlertmanager()
|
|
log.Info("Done, starting HTTP server")
|
|
|
|
// background loop that will fetch updates from Alertmanager
|
|
ticker = time.NewTicker(config.Config.Alertmanager.Interval)
|
|
go Tick()
|
|
|
|
switch config.Config.Debug {
|
|
case true:
|
|
gin.SetMode(gin.DebugMode)
|
|
case false:
|
|
gin.SetMode(gin.ReleaseMode)
|
|
}
|
|
|
|
router := gin.New()
|
|
|
|
var t *template.Template
|
|
t = loadTemplate(t, "ui/build/index.html")
|
|
router.SetHTMLTemplate(t)
|
|
|
|
prom := ginprometheus.NewPrometheus("gin")
|
|
prom.MetricsPath = getViewURL("/metrics")
|
|
prom.Use(router)
|
|
|
|
if config.Config.Debug {
|
|
ginpprof.Wrapper(router)
|
|
}
|
|
|
|
if config.Config.Sentry.Public != "" {
|
|
raven.SetRelease(version)
|
|
router.Use(sentry.Recovery(raven.DefaultClient, false))
|
|
}
|
|
|
|
setupRouter(router)
|
|
for _, am := range alertmanager.GetAlertmanagers() {
|
|
err := setupRouterProxyHandlers(router, am)
|
|
if err != nil {
|
|
log.Fatalf("Failed to setup proxy handlers for Alertmanager '%s': %s", am.Name, err)
|
|
}
|
|
}
|
|
listen := fmt.Sprintf("%s:%d", config.Config.Listen.Address, config.Config.Listen.Port)
|
|
log.Infof("Listening on %s", listen)
|
|
err := router.Run(listen)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|