diff options
| author | zwlucas <lucas.fariamo08@gmail.com> | 2026-05-29 20:41:33 +0000 |
|---|---|---|
| committer | zwlucas <lucas.fariamo08@gmail.com> | 2026-05-29 20:41:33 +0000 |
| commit | 9218d22954d61296a32c49373ab0f17f0be59cfe (patch) | |
| tree | 595b94ce7c2b5eed9760f79e7781653d6b554d18 /backend/internal | |
| parent | d186a989b6808ff39fcd43a9f0b478aebc4aa346 (diff) | |
| download | yaum-9218d22954d61296a32c49373ab0f17f0be59cfe.tar.gz yaum-9218d22954d61296a32c49373ab0f17f0be59cfe.zip | |
feat: add SSL info tracking and server metrics collection
Signed-off-by: zwlucas <lucas.fariamo08@gmail.com>
Diffstat (limited to 'backend/internal')
| -rw-r--r-- | backend/internal/api/handler.go | 40 | ||||
| -rw-r--r-- | backend/internal/api/router.go | 3 | ||||
| -rw-r--r-- | backend/internal/model/types.go | 8 | ||||
| -rw-r--r-- | backend/internal/monitor/checker.go | 26 | ||||
| -rw-r--r-- | backend/internal/monitor/worker.go | 9 | ||||
| -rw-r--r-- | backend/internal/servermetrics/collector.go | 84 | ||||
| -rw-r--r-- | backend/internal/store/memory.go | 19 | ||||
| -rw-r--r-- | backend/internal/store/postgres.go | 28 | ||||
| -rw-r--r-- | backend/internal/store/store.go | 3 |
9 files changed, 212 insertions, 8 deletions
diff --git a/backend/internal/api/handler.go b/backend/internal/api/handler.go index 5f934da..8da4290 100644 --- a/backend/internal/api/handler.go +++ b/backend/internal/api/handler.go @@ -10,6 +10,7 @@ import ( "yaum/internal/monitor" "yaum/internal/model" + "yaum/internal/servermetrics" "yaum/internal/store" ) @@ -21,10 +22,11 @@ type ServiceResponse struct { type Handler struct { store store.Store timeout int + metrics *servermetrics.Collector } -func NewHandler(s store.Store, timeoutSec int) *Handler { - return &Handler{store: s, timeout: timeoutSec} +func NewHandler(s store.Store, timeoutSec int, metrics *servermetrics.Collector) *Handler { + return &Handler{store: s, timeout: timeoutSec, metrics: metrics} } func (h *Handler) ListServices(w http.ResponseWriter, r *http.Request) { @@ -91,9 +93,14 @@ func (h *Handler) TestService(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), time.Duration(h.timeout)*time.Second) defer cancel() - hb := monitor.CheckHTTP(ctx, svc.URL, svc.KeywordToFind) + hb, ssl := monitor.CheckHTTP(ctx, svc.URL, svc.KeywordToFind) hb.ServiceID = svc.ID + if ssl != nil { + ssl.ServiceID = svc.ID + _ = h.store.SaveSSLInfo(*ssl) + } + if saverr := h.store.AddHeartbeat(hb); saverr != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": saverr.Error()}) return @@ -323,6 +330,33 @@ func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, stats) } +func (h *Handler) ServerStats(w http.ResponseWriter, r *http.Request) { + if h.metrics == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "server metrics nao disponivel"}) + return + } + stats := h.metrics.Get() + writeJSON(w, http.StatusOK, stats) +} + +func (h *Handler) GetSSLInfo(w http.ResponseWriter, r *http.Request) { + id, err := strconv.Atoi(r.PathValue("id")) + if err != nil { + writeJSON(w, http.StatusBadRequest, map[string]string{"error": "id invalido"}) + return + } + info, err := h.store.GetSSLInfo(id) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + if info == nil { + writeJSON(w, http.StatusNotFound, map[string]string{"error": "ssl info nao disponivel"}) + return + } + writeJSON(w, http.StatusOK, info) +} + func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) diff --git a/backend/internal/api/router.go b/backend/internal/api/router.go index 8188b25..d28da71 100644 --- a/backend/internal/api/router.go +++ b/backend/internal/api/router.go @@ -19,6 +19,7 @@ func NewRouter(h *Handler, stream *StreamHandler, auth *AuthHandler) http.Handle mux.HandleFunc("DELETE /api/services/{id}", h.DeleteService) mux.HandleFunc("GET /api/services/{id}/history", h.GetHistory) mux.HandleFunc("GET /api/services/{id}/stats", h.GetStats) + mux.HandleFunc("GET /api/services/{id}/ssl", h.GetSSLInfo) mux.HandleFunc("GET /api/services/{id}/badge.svg", h.BadgeSVG) mux.HandleFunc("GET /api/active-maintenance", h.GetActiveMaintenance) @@ -28,6 +29,8 @@ func NewRouter(h *Handler, stream *StreamHandler, auth *AuthHandler) http.Handle mux.HandleFunc("PUT /api/maintenances/{id}", h.UpdateMaintenance) mux.HandleFunc("DELETE /api/maintenances/{id}", h.DeleteMaintenance) + mux.HandleFunc("GET /api/admin/server-stats", h.ServerStats) + mux.Handle("GET /api/stream", stream) return corsMiddleware(auth.Middleware(mux)) diff --git a/backend/internal/model/types.go b/backend/internal/model/types.go index 4e439b8..40dc006 100644 --- a/backend/internal/model/types.go +++ b/backend/internal/model/types.go @@ -50,3 +50,11 @@ type ServiceWithHeartbeat struct { Service LastHeartbeat *Heartbeat `json:"last_heartbeat,omitempty"` } + +type SSLInfo struct { + ServiceID int `json:"service_id"` + Issuer string `json:"issuer"` + ExpiresAt time.Time `json:"expires_at"` + DaysRemaining int `json:"days_remaining"` + CheckedAt time.Time `json:"checked_at"` +} diff --git a/backend/internal/monitor/checker.go b/backend/internal/monitor/checker.go index 07f807e..df386e6 100644 --- a/backend/internal/monitor/checker.go +++ b/backend/internal/monitor/checker.go @@ -12,14 +12,16 @@ import ( var httpClient = &http.Client{ Timeout: 10 * time.Second, + // do NOT set InsecureSkipVerify — we want proper TLS } -func CheckHTTP(ctx context.Context, url string, keyword string) model.Heartbeat { +func CheckHTTP(ctx context.Context, url string, keyword string) (model.Heartbeat, *model.SSLInfo) { start := time.Now() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return heartbeatFromError(start, err) + hb := heartbeatFromError(start, err) + return hb, nil } resp, err := httpClient.Do(req) @@ -32,7 +34,7 @@ func CheckHTTP(ctx context.Context, url string, keyword string) model.Heartbeat IsUp: false, ErrorMessage: err.Error(), TestedAt: time.Now(), - } + }, nil } defer resp.Body.Close() @@ -66,13 +68,29 @@ func CheckHTTP(ctx context.Context, url string, keyword string) model.Heartbeat } } - return model.Heartbeat{ + hb := model.Heartbeat{ StatusCode: resp.StatusCode, ResponseTimeMs: elapsed, IsUp: isUp, ErrorMessage: errMsg, TestedAt: time.Now(), } + + // Extract TLS certificate info + var ssl *model.SSLInfo + if resp.TLS != nil && len(resp.TLS.PeerCertificates) > 0 { + cert := resp.TLS.PeerCertificates[0] + days := int(time.Until(cert.NotAfter).Hours() / 24) + ssl = &model.SSLInfo{ + ServiceID: 0, // filled in by caller + Issuer: cert.Issuer.CommonName, + ExpiresAt: cert.NotAfter, + DaysRemaining: days, + CheckedAt: time.Now(), + } + } + + return hb, ssl } func heartbeatFromError(start time.Time, err error) model.Heartbeat { diff --git a/backend/internal/monitor/worker.go b/backend/internal/monitor/worker.go index 70855a2..34a5cba 100644 --- a/backend/internal/monitor/worker.go +++ b/backend/internal/monitor/worker.go @@ -166,9 +166,16 @@ func (w *Worker) checkService(ctx context.Context, svc model.Service) { checkCtx, cancel := context.WithTimeout(ctx, w.timeout) defer cancel() - hb := CheckHTTP(checkCtx, svc.URL, svc.KeywordToFind) + hb, ssl := CheckHTTP(checkCtx, svc.URL, svc.KeywordToFind) hb.ServiceID = svc.ID + if ssl != nil { + ssl.ServiceID = svc.ID + if err := w.store.SaveSSLInfo(*ssl); err != nil { + log.Printf("[worker] erro ao salvar ssl info: %v", err) + } + } + w.results <- checkResult{Heartbeat: hb, Service: svc} } diff --git a/backend/internal/servermetrics/collector.go b/backend/internal/servermetrics/collector.go new file mode 100644 index 0000000..6c85582 --- /dev/null +++ b/backend/internal/servermetrics/collector.go @@ -0,0 +1,84 @@ +package servermetrics + +import ( + "context" + "log" + "sync" + "time" + + "github.com/shirou/gopsutil/v4/cpu" + "github.com/shirou/gopsutil/v4/disk" + "github.com/shirou/gopsutil/v4/mem" +) + +type Stats struct { + CPUPercent float64 `json:"cpu_percent"` + MemoryUsed uint64 `json:"memory_used"` + MemoryTotal uint64 `json:"memory_total"` + MemoryPct float64 `json:"memory_percent"` + DiskUsed uint64 `json:"disk_used"` + DiskTotal uint64 `json:"disk_total"` + DiskPct float64 `json:"disk_percent"` +} + +type Collector struct { + mu sync.RWMutex + last Stats + diskPath string +} + +func New(diskPath string) *Collector { + return &Collector{diskPath: diskPath} +} + +func (c *Collector) Start(ctx context.Context) { + c.collectOnce(ctx, 500*time.Millisecond) + ticker := time.NewTicker(30 * time.Second) + defer ticker.Stop() + for { + select { + case <-ticker.C: + c.collectOnce(ctx, 0) + case <-ctx.Done(): + return + } + } +} + +func (c *Collector) collectOnce(ctx context.Context, cpuInterval time.Duration) { + cpuPct, err := cpu.PercentWithContext(ctx, cpuInterval, false) + if err != nil { + log.Printf("[servermetrics] erro cpu: %v", err) + return + } + + memStat, err := mem.VirtualMemoryWithContext(ctx) + if err != nil { + log.Printf("[servermetrics] erro mem: %v", err) + return + } + + diskStat, err := disk.UsageWithContext(ctx, c.diskPath) + if err != nil { + log.Printf("[servermetrics] erro disk: %v", err) + return + } + + c.mu.Lock() + c.last = Stats{ + CPUPercent: cpuPct[0], + MemoryUsed: memStat.Used, + MemoryTotal: memStat.Total, + MemoryPct: memStat.UsedPercent, + DiskUsed: diskStat.Used, + DiskTotal: diskStat.Total, + DiskPct: diskStat.UsedPercent, + } + c.mu.Unlock() +} + +func (c *Collector) Get() Stats { + c.mu.RLock() + defer c.mu.RUnlock() + return c.last +} diff --git a/backend/internal/store/memory.go b/backend/internal/store/memory.go index 1ee2e05..78defcd 100644 --- a/backend/internal/store/memory.go +++ b/backend/internal/store/memory.go @@ -14,6 +14,7 @@ type MemoryStore struct { services map[int]*model.Service heartbeats map[int][]*model.Heartbeat maintenances []model.Maintenance + sslInfo map[int]model.SSLInfo nextSvcID int nextHbID int nextMtnID int @@ -24,6 +25,7 @@ func NewMemoryStore() *MemoryStore { services: make(map[int]*model.Service), heartbeats: make(map[int][]*model.Heartbeat), maintenances: make([]model.Maintenance, 0), + sslInfo: make(map[int]model.SSLInfo), nextSvcID: 1, nextHbID: 1, nextMtnID: 1, @@ -346,3 +348,20 @@ func (s *MemoryStore) GetServiceStats(serviceID int) (*model.ServiceStats, error TotalChecks30d: t30, }, nil } + +func (s *MemoryStore) GetSSLInfo(serviceID int) (*model.SSLInfo, error) { + s.mu.RLock() + defer s.mu.RUnlock() + info, ok := s.sslInfo[serviceID] + if !ok { + return nil, nil + } + return &info, nil +} + +func (s *MemoryStore) SaveSSLInfo(info model.SSLInfo) error { + s.mu.Lock() + defer s.mu.Unlock() + s.sslInfo[info.ServiceID] = info + return nil +} diff --git a/backend/internal/store/postgres.go b/backend/internal/store/postgres.go index 57fa258..fb07bdc 100644 --- a/backend/internal/store/postgres.go +++ b/backend/internal/store/postgres.go @@ -393,3 +393,31 @@ func (s *PostgresStore) DeleteMaintenance(id int) error { } return nil } + +func (s *PostgresStore) GetSSLInfo(serviceID int) (*model.SSLInfo, error) { + var info model.SSLInfo + err := s.pool.QueryRow(context.Background(), + `SELECT service_id, issuer, expires_at, days_remaining, checked_at FROM ssl_info WHERE service_id = $1`, + serviceID, + ).Scan(&info.ServiceID, &info.Issuer, &info.ExpiresAt, &info.DaysRemaining, &info.CheckedAt) + if err != nil { + if err == pgx.ErrNoRows { + return nil, nil + } + return nil, fmt.Errorf("get ssl info: %w", err) + } + return &info, nil +} + +func (s *PostgresStore) SaveSSLInfo(info model.SSLInfo) error { + _, err := s.pool.Exec(context.Background(), + `INSERT INTO ssl_info (service_id, issuer, expires_at, days_remaining, checked_at) + VALUES ($1, $2, $3, $4, $5) + ON CONFLICT (service_id) DO UPDATE SET issuer=$2, expires_at=$3, days_remaining=$4, checked_at=$5`, + info.ServiceID, info.Issuer, info.ExpiresAt, info.DaysRemaining, info.CheckedAt, + ) + if err != nil { + return fmt.Errorf("save ssl info: %w", err) + } + return nil +} diff --git a/backend/internal/store/store.go b/backend/internal/store/store.go index 2c34b1c..3fc348d 100644 --- a/backend/internal/store/store.go +++ b/backend/internal/store/store.go @@ -25,4 +25,7 @@ type Store interface { AddMaintenance(model.Maintenance) (*model.Maintenance, error) UpdateMaintenance(int, model.Maintenance) (*model.Maintenance, error) DeleteMaintenance(int) error + + GetSSLInfo(int) (*model.SSLInfo, error) + SaveSSLInfo(model.SSLInfo) error } |