Files
karma/main.go
Łukasz Mierzwa ba34e92791 fix(backend): correctly handle expires for static files
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
2019-04-09 19:48:48 +01:00

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)
}
}