feat(backend): add job metrics
ci-agent / build (push) Failing after 3m1s

This commit is contained in:
2026-04-04 23:00:53 +03:00
parent 7be99f8e91
commit 428140ff15
3 changed files with 80 additions and 3 deletions
+5 -1
View File
@@ -98,7 +98,9 @@ func main() {
scriptSvc := service.NewScriptServiceWithInterpreters(h.Repo, scriptRepo) scriptSvc := service.NewScriptServiceWithInterpreters(h.Repo, scriptRepo)
scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdTracker) scriptHandlers := handlers.NewScriptHandlers(scriptSvc, cmdTracker)
jobsHandlers := handlers.NewJobsHandlers(cmdTracker, scriptSvc, jobsHandlers := handlers.NewJobsHandlers(cmdTracker, scriptSvc,
os.Getenv("WHEREAMI") /* our address for redirects */) os.Getenv("WHEREAMI"), /* our address for redirects */
jobRepo,
)
// Initialize script management service and handlers // Initialize script management service and handlers
scriptManageSvc := service.NewScriptService(h.Repo) scriptManageSvc := service.NewScriptService(h.Repo)
@@ -206,6 +208,8 @@ func main() {
{ {
jobsGroup.POST("", jobsHandlers.AddJob) jobsGroup.POST("", jobsHandlers.AddJob)
jobsGroup.POST("/:id/wait", jobsHandlers.WaitJob) jobsGroup.POST("/:id/wait", jobsHandlers.WaitJob)
jobsGroup.GET("/metrics", jobsHandlers.GetJobMetrics)
jobsGroup.POST("/check_cmd", jobsHandlers.CheckCmd)
} }
// Agent registration // Agent registration
+48 -2
View File
@@ -6,9 +6,11 @@ import (
"net/http" "net/http"
"os/exec" "os/exec"
"strconv" "strconv"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/grpcsrv/commander"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/repository"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -17,10 +19,11 @@ type JobsHandlers struct {
tracker *commander.ConnTracker tracker *commander.ConnTracker
svc *service.ScriptService svc *service.ScriptService
whereami string whereami string
jobRepo *repository.JobRepository
} }
func NewJobsHandlers(tracker *commander.ConnTracker, svc *service.ScriptService, whereami string) JobsHandlers { func NewJobsHandlers(tracker *commander.ConnTracker, svc *service.ScriptService, whereami string, jobRepo *repository.JobRepository) JobsHandlers {
return JobsHandlers{tracker: tracker, svc: svc, whereami: whereami} return JobsHandlers{tracker: tracker, svc: svc, whereami: whereami, jobRepo: jobRepo}
} }
// AddJobIn is the request body for creating a job. // AddJobIn is the request body for creating a job.
@@ -197,3 +200,46 @@ type CheckCmdIn struct {
type CheckCmdOut struct { type CheckCmdOut struct {
Exists bool `json:"exists"` Exists bool `json:"exists"`
} }
// JobMetricsOut is the response body for the job metrics endpoint.
type JobMetricsOut struct {
Total int `json:"total"`
Success int `json:"success"`
Failed int `json:"failed"`
Pending int `json:"pending"`
Period string `json:"period"`
}
// GetJobMetrics returns job success metrics over a parameterized period.
// @Summary Get job metrics
// @Description Returns total, successful, failed, and pending job counts over the given period
// @Tags jobs
// @Produce json
// @Param period query string false "Time period (e.g. 1h, 24h, 7d)" default(24h)
// @Success 200 {object} JobMetricsOut
// @Failure 400 {object} map[string]string
// @Security Bearer
// @Router /jobs/metrics [get]
func (h *JobsHandlers) GetJobMetrics(c *gin.Context) {
periodStr := c.DefaultQuery("period", "24h")
period, err := time.ParseDuration(periodStr)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid period, use Go duration format (e.g. 1h, 24h, 7d)"})
return
}
since := time.Now().Add(-period)
metrics, err := h.jobRepo.GetJobMetrics(c.Request.Context(), since)
if err != nil {
c.Error(err)
return
}
c.JSON(http.StatusOK, JobMetricsOut{
Total: metrics.Total,
Success: metrics.Success,
Failed: metrics.Failed,
Pending: metrics.Pending,
Period: periodStr,
})
}
@@ -5,6 +5,7 @@ import (
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"fmt" "fmt"
"time"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/models"
"gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage" "gitea.d3m0k1d.ru/d3m0k1d/HellreigN/backend/internal/storage"
@@ -103,3 +104,29 @@ func (r *JobRepository) GetJobByID(ctx context.Context, jid int64) (models.Job,
job.Stdin = stdinVal job.Stdin = stdinVal
return job, nil return job, nil
} }
type JobMetrics struct {
Total int
Success int
Failed int
Pending int
}
// GetJobMetrics returns job success metrics for jobs updated since the given time.
// A successful job has status == 0, failed has status != 0, pending has status == 0 with empty stdout/stderr.
func (r *JobRepository) GetJobMetrics(ctx context.Context, since time.Time) (JobMetrics, error) {
var m JobMetrics
err := r.DB.QueryRowContext(ctx,
`SELECT
COUNT(*),
SUM(CASE WHEN status = 0 AND (stdout != '' OR stderr != '') THEN 1 ELSE 0 END),
SUM(CASE WHEN status != 0 THEN 1 ELSE 0 END),
SUM(CASE WHEN status = 0 AND stdout = '' AND stderr = '' THEN 1 ELSE 0 END)
FROM jobs WHERE updated_at >= ?`,
since,
).Scan(&m.Total, &m.Success, &m.Failed, &m.Pending)
if err != nil {
return JobMetrics{}, err
}
return m, nil
}