implemented observability

This commit is contained in:
2026-05-12 20:32:30 +02:00
parent c7df53708c
commit e360f3697e
11 changed files with 478 additions and 22 deletions
+126
View File
@@ -0,0 +1,126 @@
package handlers
import (
"net/http"
"time"
"xorm.io/xorm"
"github.com/forgeo/forgebucket/internal/events"
"github.com/forgeo/forgebucket/internal/models"
"github.com/forgeo/forgebucket/internal/observability"
)
// ── /health ───────────────────────────────────────────────────────────────────
type HealthHandler struct {
db *xorm.Engine
bus events.EventBus
}
func NewHealthHandler(db *xorm.Engine, bus events.EventBus) *HealthHandler {
return &HealthHandler{db: db, bus: bus}
}
func (h *HealthHandler) Health(w http.ResponseWriter, r *http.Request) {
status := observability.Check(h.db, h.bus)
if status.Status != "healthy" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusServiceUnavailable)
jsonOK(w, status)
return
}
jsonOK(w, status)
}
// ── /api/v1/repos/{owner}/{repo}/health ──────────────────────────────────────
type RepoHealthHandler struct{ db *xorm.Engine }
func NewRepoHealthHandler(db *xorm.Engine) *RepoHealthHandler {
return &RepoHealthHandler{db: db}
}
type latestDeployment struct {
EnvName string `json:"envName"`
Status string `json:"status"`
SHA string `json:"sha"`
FinishedAt *time.Time `json:"finishedAt"`
}
type repoHealthResponse struct {
CIPassRate7d float64 `json:"ciPassRate7d"`
TotalRuns7d int `json:"totalRuns7d"`
LatestRun *models.PipelineRun `json:"latestRun"`
LatestDeployments []latestDeployment `json:"latestDeployments"`
OpenDriftCount int `json:"openDriftCount"`
OpenPRCount int `json:"openPRCount"`
}
// Get returns an operational health summary for a repository.
// This feeds the repo page header: CI pass rate, latest deploy per env, drift count.
func (h *RepoHealthHandler) Get(w http.ResponseWriter, r *http.Request) {
repoID, ok := resolveRepoID(h.db, w, r)
if !ok {
return
}
since7d := time.Now().UTC().Add(-7 * 24 * time.Hour)
// CI pass rate over last 7 days.
var runs []models.PipelineRun
h.db.Where("repo_id = ? AND created_at >= ?", repoID, since7d).Find(&runs)
total := len(runs)
succeeded := 0
for _, run := range runs {
if run.Status == "succeeded" {
succeeded++
}
}
var passRate float64
if total > 0 {
passRate = float64(succeeded) / float64(total)
}
// Latest run overall.
var latestRun models.PipelineRun
var hasLatest bool
hasLatest, _ = h.db.Where("repo_id = ?", repoID).Desc("id").Limit(1).Get(&latestRun)
// Latest deployment per environment.
var envs []models.Environment
h.db.Where("repo_id = ?", repoID).Find(&envs)
deploys := make([]latestDeployment, 0, len(envs))
for _, env := range envs {
var d models.Deployment
if found, _ := h.db.Where("env_id = ?", env.ID).Desc("id").Limit(1).Get(&d); found {
deploys = append(deploys, latestDeployment{
EnvName: env.Name,
Status: string(d.Status),
SHA: d.SHA,
FinishedAt: d.FinishedAt,
})
}
}
// Open drift count (GitOpsConfigs where sync_status = 'drifted').
driftCount, _ := h.db.Where("repo_id = ? AND sync_status = 'drifted'", repoID).
Count(&models.GitOpsConfig{})
// Open PR count.
prCount, _ := h.db.Where("repo_id = ? AND status = 'open'", repoID).
Count(&models.PullRequest{})
resp := repoHealthResponse{
CIPassRate7d: passRate,
TotalRuns7d: total,
LatestDeployments: deploys,
OpenDriftCount: int(driftCount),
OpenPRCount: int(prCount),
}
if hasLatest {
resp.LatestRun = &latestRun
}
jsonOK(w, resp)
}
+14 -8
View File
@@ -14,10 +14,13 @@ import (
"github.com/gorilla/sessions"
"xorm.io/xorm"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/forgeo/forgebucket/internal/api/handlers"
"github.com/forgeo/forgebucket/internal/api/middleware"
"github.com/forgeo/forgebucket/internal/config"
"github.com/forgeo/forgebucket/internal/events"
"github.com/forgeo/forgebucket/internal/observability"
)
func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, bus events.EventBus, artifactRoot string, staticFiles fs.FS) http.Handler {
@@ -26,6 +29,7 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, bus even
r.Use(chimiddleware.Logger)
r.Use(chimiddleware.RealIP)
r.Use(chimiddleware.Recoverer)
r.Use(observability.Middleware())
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"http://localhost:5173", cfg.InstanceURL},
AllowedMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
@@ -53,9 +57,11 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, bus even
webhookH := handlers.NewWebhookHandler(engine)
prSettingsH := handlers.NewPRSettingsHandler(engine)
lfsH := handlers.NewLFSHandler(engine)
exploreH := handlers.NewExploreHandler(engine)
dashH := handlers.NewDashboardHandler(engine)
auditH := handlers.NewAuditHandler(engine)
exploreH := handlers.NewExploreHandler(engine)
dashH := handlers.NewDashboardHandler(engine)
auditH := handlers.NewAuditHandler(engine)
healthH := handlers.NewHealthHandler(engine, bus)
repoHealthH := handlers.NewRepoHealthHandler(engine)
artifactH := handlers.NewArtifactHandler(engine, artifactRoot)
runnerH := handlers.NewRunnerHandler(engine)
gitopsH := handlers.NewGitOpsHandler(engine, bus)
@@ -74,17 +80,16 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, bus even
r.Post("/git-receive-pack", gitH.ServeGit)
})
// ── Ops endpoints (root-level, no auth, standard paths for k8s/Prometheus) ──
r.Get("/health", healthH.Health)
r.Get("/metrics", promhttp.Handler().ServeHTTP)
r.Route("/api/v1", func(r chi.Router) {
// ── Public ────────────────────────────────────────────────────────────
r.Get("/explore/repos", exploreH.Repos)
r.Get("/explore/users", exploreH.Users)
r.Get("/health", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"ok"}`))
})
// Generates a CSRF token + cookie. SPA calls this once on load.
r.Get("/csrf", func(w http.ResponseWriter, r *http.Request) {
token, err := middleware.NewCSRFToken(w, !cfg.Debug)
@@ -240,6 +245,7 @@ func New(cfg *config.Config, engine *xorm.Engine, store sessions.Store, bus even
r.With(csrf).Delete("/secrets/{name}", secretH.DeleteRepoSecret)
r.Get("/lfs-settings", lfsH.Get)
r.With(csrf).Put("/lfs-settings", lfsH.Update)
r.Get("/health", repoHealthH.Get)
r.Route("/environments", func(r chi.Router) {
r.Get("/", envH.ListEnvironments)
r.With(csrf).Post("/", envH.CreateEnvironment)