mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-12 00:45:38 +00:00
An optional actions block in the config adds buttons to alerts. POST
/api/actions/{id} checks that the repository and the current alert match,
then sends the alert as JSON to the configured webhook with an optional
bearer token. The request refuses cross-site callers like POST /refresh.
GET /api/state lists the actions and merges per-alert status rows from an
optional status_url, cached for 10 seconds. Without an actions block the
API output does not change.
The ghtracker_action_requests_total counter records each result.
106 lines
2.9 KiB
Go
106 lines
2.9 KiB
Go
// Command ghtracker serves a dashboard and Prometheus metrics for a list of GitHub repositories.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/collectors"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
|
|
"github.com/lukaszraczylo/ghtracker/internal/collector"
|
|
"github.com/lukaszraczylo/ghtracker/internal/config"
|
|
"github.com/lukaszraczylo/ghtracker/internal/gh"
|
|
"github.com/lukaszraczylo/ghtracker/internal/metrics"
|
|
"github.com/lukaszraczylo/ghtracker/internal/web"
|
|
)
|
|
|
|
// distFS holds the built UI; dist/.gitkeep keeps the pattern valid before the first UI build.
|
|
//
|
|
//go:embed all:dist
|
|
var distFS embed.FS
|
|
|
|
// version is set at release time with -ldflags "-X main.version=...".
|
|
var version = "dev"
|
|
|
|
const (
|
|
httpTimeout = 30 * time.Second
|
|
shutdownTimeout = 10 * time.Second
|
|
headerTimeout = 10 * time.Second
|
|
)
|
|
|
|
func main() {
|
|
cfgPath := flag.String("config", "config.yaml", "path to the YAML config file")
|
|
showVersion := flag.Bool("version", false, "print the version and exit")
|
|
flag.Parse()
|
|
if *showVersion {
|
|
fmt.Println(version)
|
|
return
|
|
}
|
|
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
|
|
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
|
defer stop()
|
|
if err := run(ctx, *cfgPath, log); err != nil {
|
|
stop()
|
|
log.Error("fatal", "err", err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func run(ctx context.Context, cfgPath string, log *slog.Logger) error {
|
|
cfg, err := config.Load(cfgPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hc := &http.Client{Timeout: httpTimeout}
|
|
auth, err := gh.NewAppAuth(cfg.GitHub.AppID, cfg.GitHub.PrivateKeyPEM, cfg.GitHub.APIURL, cfg.GitHub.InstallationID, hc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
client := gh.NewClient(cfg.GitHub.APIURL, auth, hc, cfg.MaxItems)
|
|
coll := collector.New(client, cfg, log)
|
|
|
|
reg := prometheus.NewRegistry()
|
|
reg.MustRegister(collectors.NewGoCollector(), collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
|
|
metrics.New(coll, client.RateLimitRemaining))
|
|
ui, err := fs.Sub(distFS, "dist")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
srv := web.New(coll, promhttp.HandlerFor(reg, promhttp.HandlerOpts{}), ui, *cfg.ManualRefresh, log)
|
|
|
|
if len(cfg.Actions) > 0 {
|
|
srv.WithActions(web.NewActions(cfg.Actions, log, reg))
|
|
}
|
|
|
|
go coll.Run(ctx)
|
|
|
|
hs := &http.Server{Addr: cfg.Listen, Handler: srv.Handler(), ReadHeaderTimeout: headerTimeout}
|
|
errc := make(chan error, 1)
|
|
go func() { errc <- hs.ListenAndServe() }()
|
|
log.Info("listening", "version", version, "addr", cfg.Listen, "repos", len(cfg.Repos), "refresh", cfg.RefreshInterval)
|
|
|
|
select {
|
|
case err := <-errc:
|
|
return err
|
|
case <-ctx.Done():
|
|
}
|
|
sctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
|
defer cancel()
|
|
if err := hs.Shutdown(sctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|