feat: Add check service (WIP)

This commit is contained in:
2025-07-21 01:59:03 +03:00
parent 691d1e7275
commit 87defbf391
9 changed files with 219 additions and 7 deletions

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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:"-"`

View File

@@ -10,6 +10,7 @@ const (
SERVER = "server"
SYSTEM = "system"
DATABASE = "database"
CRON = "cron"
)
var Global *LoggerManager

5
pkg/slice/chunk.go Normal file
View File

@@ -0,0 +1,5 @@
package slice
func ChunkSlice[T any](slice []T, chunkSize int) [][]T {
}

24
repository/status.go Normal file
View File

@@ -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
}

175
service/check.go Normal file
View File

@@ -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
}

View File

@@ -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")

View File

@@ -1,5 +1,5 @@
package version
func AppVersion() string {
return "0.0.1"
return "0.0.3"
}