From 87defbf3913dd19998787d501b5ce96858b080eb Mon Sep 17 00:00:00 2001 From: ostiwe Date: Mon, 21 Jul 2025 01:59:03 +0300 Subject: [PATCH] feat: Add check service (WIP) --- main.go | 5 ++ model/service.go | 3 +- model/status.go | 6 ++ modules/log/manager.go | 1 + pkg/slice/chunk.go | 5 ++ repository/status.go | 24 ++++++ service/check.go | 175 +++++++++++++++++++++++++++++++++++++++++ settings/settings.go | 5 -- version/version.go | 2 +- 9 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 pkg/slice/chunk.go create mode 100644 repository/status.go create mode 100644 service/check.go diff --git a/main.go b/main.go index bd8414f..705e64c 100644 --- a/main.go +++ b/main.go @@ -3,11 +3,13 @@ package main import ( "fmt" "net/http" + "time" "git.ostiwe.com/ostiwe-com/status/model" "git.ostiwe.com/ostiwe-com/status/modules/db" appLog "git.ostiwe.com/ostiwe-com/status/modules/log" "git.ostiwe.com/ostiwe-com/status/router" + "git.ostiwe.com/ostiwe-com/status/service" "git.ostiwe.com/ostiwe-com/status/version" "github.com/alexflint/go-arg" "github.com/go-chi/chi/v5" @@ -54,6 +56,9 @@ func main() { return } + appLog.Global.Get(appLog.SYSTEM).Info("Start service observer") + go service.NewCheck(20).Start(time.Second * 10) + appLog.Global.Put(appLog.SERVER, logrus.New()) appLog.Global.Get(appLog.SERVER).Info("Startup server on port: ", args.Server.Port) diff --git a/model/service.go b/model/service.go index b721924..428cf45 100644 --- a/model/service.go +++ b/model/service.go @@ -1,7 +1,8 @@ package model type HTTPConfig struct { - Authorization string `json:"authorization"` + Method string `json:"method"` + Headers map[string]string `json:"headers"` } type ServiceTypeCheckConfig struct { diff --git a/model/status.go b/model/status.go index 2b98e10..c7b2cbe 100644 --- a/model/status.go +++ b/model/status.go @@ -6,6 +6,12 @@ import ( "gorm.io/gorm" ) +const ( + StatusOK = "ok" // Means - response ok, service is alive + StatusFailed = "failed" // Means - response failed, all tries failed, service down + StatusWarn = "warn" // Means - response failed after N tries and still watched +) + type Status struct { ID int `gorm:"primary_key;auto_increment" json:"-"` ServiceID int `gorm:"one" json:"-"` diff --git a/modules/log/manager.go b/modules/log/manager.go index 6e507ed..0414956 100644 --- a/modules/log/manager.go +++ b/modules/log/manager.go @@ -10,6 +10,7 @@ const ( SERVER = "server" SYSTEM = "system" DATABASE = "database" + CRON = "cron" ) var Global *LoggerManager diff --git a/pkg/slice/chunk.go b/pkg/slice/chunk.go new file mode 100644 index 0000000..5bdc100 --- /dev/null +++ b/pkg/slice/chunk.go @@ -0,0 +1,5 @@ +package slice + +func ChunkSlice[T any](slice []T, chunkSize int) [][]T { + +} diff --git a/repository/status.go b/repository/status.go new file mode 100644 index 0000000..8cc9f27 --- /dev/null +++ b/repository/status.go @@ -0,0 +1,24 @@ +package repository + +import ( + "git.ostiwe.com/ostiwe-com/status/model" + "git.ostiwe.com/ostiwe-com/status/modules/db" +) + +type Status interface { + Add(status model.Status) error +} + +type status struct { + repository +} + +func NewStatusRepository() Status { + return &status{ + repository: repository{db: db.Global}, + } +} + +func (s status) Add(status model.Status) error { + return s.db.Create(&status).Error +} diff --git a/service/check.go b/service/check.go new file mode 100644 index 0000000..58e6e1f --- /dev/null +++ b/service/check.go @@ -0,0 +1,175 @@ +package service + +import ( + "context" + "errors" + "net" + "net/http" + "net/url" + "slices" + "sync" + "time" + + "git.ostiwe.com/ostiwe-com/status/model" + "git.ostiwe.com/ostiwe-com/status/modules/log" + "git.ostiwe.com/ostiwe-com/status/repository" + "github.com/sirupsen/logrus" +) + +var ( + httpObserveFailed = errors.New("http observe fail") + httpObserveMaxTries = errors.New("http observe max tries") +) + +func init() { + log.Global.Put(log.CRON, logrus.New()) +} + +type Check interface { + Start(interval time.Duration) +} + +type check struct { + serviceRepository repository.Service + maxServicesPerRoutine int + statusRepository repository.Status +} + +func NewCheck(maxServicesPerRoutine int) Check { + if maxServicesPerRoutine <= 0 { + maxServicesPerRoutine = 15 + } + + return &check{ + maxServicesPerRoutine: maxServicesPerRoutine, + serviceRepository: repository.NewServiceRepository(), + statusRepository: repository.NewStatusRepository(), + } +} + +func (c check) Start(interval time.Duration) { + for { + time.Sleep(interval) + + services, err := c.serviceRepository.All(context.Background(), -1, 0, false) + if err != nil { + log.Global.Get(log.CRON).Error(err) + + continue + } + + wg := new(sync.WaitGroup) + + chunked := slices.Chunk(services, c.maxServicesPerRoutine) + + for i := range chunked { + wg.Add(1) + + go c.observeChunk(wg, i) + } + + wg.Wait() + + } +} + +func (c check) observeChunk(wg *sync.WaitGroup, chunk []model.Service) { + defer wg.Done() + + for _, item := range chunk { + switch item.Type { + case "http": + err := c.observeHttp(item, 0) + if err == nil { + addErr := c.statusRepository.Add(model.Status{ + ServiceID: item.ID, + Status: model.StatusOK, + }) + if addErr != nil { + log.Global.Get(log.CRON).Error("Add status to status repository fail", item.ID, addErr) + } + + continue + } + + if errors.Is(err, httpObserveFailed) { + addErr := c.statusRepository.Add(model.Status{ + ServiceID: item.ID, + Status: model.StatusFailed, + }) + if addErr != nil { + log.Global.Get(log.CRON).Error("Add status to status repository fail", item.ID, addErr) + } + + continue + } + + log.Global.Get(log.CRON).Error("Unexpected observe fail", item.ID, err) + + break + case "tcp": + log.Global.Get(log.CRON).Warn("Cannot handle service with id: ", item.ID, ", because a tcp observer not implemented.") + continue + } + } +} + +func (c check) observeHttp(service model.Service, attempt int) error { + if attempt >= 20 { + return httpObserveMaxTries + } + + if attempt > 5 { + err := c.statusRepository.Add(model.Status{ + ServiceID: service.ID, + Status: model.StatusWarn, + }) + if err != nil { + return err + } + } + + if service.TypeConfig == nil || service.TypeConfig.HTTPConfig == nil { + return errors.New("service has no config for http") + } + + conf := service.TypeConfig.HTTPConfig + + client := &http.Client{ + Timeout: time.Second * 5, + } + + request, err := http.NewRequest(conf.Method, service.Host, nil) + if err != nil { + return err + } + + for s := range conf.Headers { + request.Header.Set(s, conf.Headers[s]) + } + + response, err := client.Do(request) + if err != nil && !errors.Is(err, context.DeadlineExceeded) { + var ( + e *url.Error + opErr *net.OpError + ) + if errors.As(err, &e) && errors.As(e.Err, &opErr) { + time.Sleep(2 * time.Second) + + return c.observeHttp(service, attempt+1) + } + + return err + } + + if errors.Is(err, context.DeadlineExceeded) { + return c.observeHttp(service, attempt+1) + } + + if response.StatusCode != http.StatusOK { + return httpObserveFailed + } + + return nil +} diff --git a/settings/settings.go b/settings/settings.go index cd4c758..df26b7d 100644 --- a/settings/settings.go +++ b/settings/settings.go @@ -10,7 +10,6 @@ import ( ) var ( - AppVersion string AppStartTime time.Time ) @@ -18,10 +17,6 @@ func init() { appLog.SetGlobalManager(appLog.NewManager()) appLog.Global.Put(appLog.SYSTEM, logrus.New()) - if AppVersion == "" { - AppVersion = "dev" - } - AppStartTime = time.Now() env := os.Getenv("APP_ENV") diff --git a/version/version.go b/version/version.go index c7878da..29add40 100644 --- a/version/version.go +++ b/version/version.go @@ -1,5 +1,5 @@ package version func AppVersion() string { - return "0.0.1" + return "0.0.3" }