refactor: Switching to a cron system for tasks, restructuring service and status models

Added response time field for Status model, set log level from ENV
This commit is contained in:
2025-10-28 00:51:29 +03:00
parent 1958d9b3d9
commit b33df27b31
11 changed files with 254 additions and 63 deletions

View File

@@ -3,67 +3,154 @@ package service
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"git.ostiwe.com/ostiwe-com/status/model"
"git.ostiwe.com/ostiwe-com/status/modules/log"
"git.ostiwe.com/ostiwe-com/status/modules/scheduler"
"git.ostiwe.com/ostiwe-com/status/repository"
"github.com/go-co-op/gocron/v2"
"github.com/samber/lo"
"github.com/sirupsen/logrus"
)
var (
httpObserveFailed = errors.New("http observe fail")
httpObserveMaxTries = errors.New("http observe max tries")
GlobalCheckService Check
)
func init() {
log.Global.Put(log.CRON, logrus.New())
log.Global.Put(log.Observer, logrus.New())
}
type Check interface {
Observe(ctx context.Context, srv *model.Service) error
RegisterStatus(ctx context.Context, serviceID int, status model.StatusCode) error
RegisterStatus(ctx context.Context, serviceID uint64, status model.StatusCode, meta ResponseMeta) error
RegisterTasks(ctx context.Context) error
}
type check struct {
serviceRepository repository.Service
statusRepository repository.Status
appScheduler scheduler.AppScheduler
}
func NewCheck() Check {
func NewCheck(appScheduler scheduler.AppScheduler) Check {
return &check{
serviceRepository: repository.NewServiceRepository(),
statusRepository: repository.NewStatusRepository(),
appScheduler: appScheduler,
}
}
func (c check) Observe(ctx context.Context, srv *model.Service) error {
return c.observeHttp(srv)
func (c *check) RegisterTasks(ctx context.Context) error {
services, err := c.serviceRepository.All(ctx, -1, 0, false)
if err != nil {
return err
}
for _, service := range services {
job, jobCreateErr := c.appScheduler.NewServiceJob(
service.ID,
gocron.DurationJob(time.Duration(service.Config.Interval)*time.Second),
gocron.NewTask(c.Observe, ctx, &service),
gocron.WithName(fmt.Sprintf("task-service-%d", service.ID)),
)
if jobCreateErr != nil {
return jobCreateErr
}
log.Global.Get(log.CRON).Infof(
"Registered service observe task for service with id - %d; task id - UUID: %s; Job interval: %d(s)",
service.ID,
job.ID().String(),
service.Config.Interval,
)
}
return nil
}
func (c check) RegisterStatus(ctx context.Context, serviceID int, status model.StatusCode) error {
func (c *check) Observe(ctx context.Context, srv *model.Service) error {
// If the observation of an HTTP service failed, check the last N statuses.
// If the first one has a failure status, set it as failed.
// If all statuses in the last N are warning, set it as failed.
// Otherwise, set the status as warning.
if srv.Type == "http" {
meta, err := c.ObserveHttp(ctx, srv)
if err == nil {
return c.RegisterStatus(ctx, srv.ID, model.StatusOK, *meta)
}
lastNStatuses, err := c.statusRepository.LastNStatuses(ctx, srv.ID, uint(srv.Config.MaxFails))
if err != nil {
return err
}
if len(lastNStatuses) > 0 && lastNStatuses[0].Status == model.StatusFailed {
return c.RegisterStatus(ctx, srv.ID, model.StatusFailed, *meta)
}
everyWarn := lo.EveryBy(lastNStatuses, func(item model.Status) bool {
return item.Status == model.StatusWarn
})
if everyWarn {
return c.RegisterStatus(ctx, srv.ID, model.StatusFailed, *meta)
}
return c.RegisterStatus(ctx, srv.ID, model.StatusWarn, *meta)
}
// Todo: return err
return nil
}
func (c *check) RegisterStatus(ctx context.Context, serviceID uint64, status model.StatusCode, meta ResponseMeta) error {
return c.statusRepository.Add(ctx, model.Status{
ServiceID: serviceID,
Status: status,
CreatedAt: time.Now(),
ServiceID: serviceID,
Status: status,
ResponseTime: meta.ResponseTime,
CreatedAt: time.Now(),
})
}
func (c check) observeHttp(service *model.Service) error {
if service.TypeConfig == nil || service.TypeConfig.HTTPConfig == nil {
return errors.New("service has no config for http")
type ResponseMeta struct {
ResponseTime uint64
}
func (c *check) ObserveHttp(ctx context.Context, service *model.Service) (*ResponseMeta, error) {
var meta = &ResponseMeta{
ResponseTime: 0,
}
conf := service.TypeConfig.HTTPConfig
var startTime = time.Now()
defer func() {
meta.ResponseTime = uint64(time.Since(startTime).Milliseconds())
}()
if service.Config == nil || service.Config.HTTPConfig == nil {
return meta, errors.New("service has no config for http")
}
logger := log.Global.Get(log.Observer)
logger.Debugf("Start observe service - %d", service.ID)
conf := service.Config.HTTPConfig
client := &http.Client{
Timeout: time.Second * 5,
Timeout: time.Duration(service.Config.Timeout) * time.Second,
}
request, err := http.NewRequest(conf.Method, service.Host, nil)
request, err := http.NewRequestWithContext(ctx, conf.Method, service.Host, nil)
if err != nil {
return err
return meta, err
}
for s := range conf.Headers {
@@ -71,13 +158,13 @@ func (c check) observeHttp(service *model.Service) error {
}
response, err := client.Do(request)
if err != nil && !errors.Is(err, context.DeadlineExceeded) {
return err
if err != nil {
return meta, err
}
if response.StatusCode != http.StatusOK {
return httpObserveFailed
return meta, httpObserveFailed
}
return nil
return meta, nil
}