init: first steps

This commit is contained in:
2025-07-20 23:50:47 +03:00
commit 5f26ad6941
25 changed files with 1134 additions and 0 deletions

6
.env Normal file
View File

@@ -0,0 +1,6 @@
DATABASE_HOST=
DATABASE_PORT=
DATABASE_USER=
DATABASE_PASS=
DATABASE_DB=
DATABASE_TZ=Europe/Moscow

55
.gitignore vendored Normal file
View File

@@ -0,0 +1,55 @@
cmake-build-*/
*.iws
out/
.idea_modules/
atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
.idea/
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
*.exe
*.exe~
*.dll
*.so
*.dylib
*.test
*.out
go.work
go.work.sum
.env.*
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
*.stackdump
[Dd]esktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msix
*.msm
*.msp
*.lnk
.DS_Store
.AppleDouble
.LSOverride
Icon
._*
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

16
docker-compose.yml Normal file
View File

@@ -0,0 +1,16 @@
services:
postgresql:
image: postgres:latest
environment:
- POSTGRES_USER=status
- POSTGRES_DB=status
- POSTGRES_PASSWORD=status
- POSTGRES_HOST_AUTH_METHOD=trust
volumes:
- database_postgres:/var/lib/postgresql/data
ports:
- "5444:5432"
volumes:
database_postgres:

9
dto/service.go Normal file
View File

@@ -0,0 +1,9 @@
package dto
import "git.ostiwe.com/ostiwe-com/status/model"
type PublicService struct {
Name string `json:"name"`
Description string `json:"description"`
Statuses []model.Status `json:"statuses"`
}

33
go.mod Normal file
View File

@@ -0,0 +1,33 @@
module git.ostiwe.com/ostiwe-com/status
go 1.24.2
require (
github.com/alexflint/go-arg v1.6.0
github.com/go-andiamo/chioas v1.16.4
github.com/go-chi/chi/v5 v5.2.2
github.com/joho/godotenv v1.5.1
github.com/sirupsen/logrus v1.9.3
go.uber.org/mock v0.5.2
gorm.io/driver/postgres v1.6.0
gorm.io/gorm v1.30.0
)
require (
github.com/alexflint/go-scalar v1.2.0 // indirect
github.com/go-andiamo/splitter v1.2.5 // indirect
github.com/go-andiamo/urit v1.2.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.5 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/kr/pretty v0.3.1 // indirect
golang.org/x/crypto v0.40.0 // indirect
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.27.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

80
go.sum Normal file
View File

@@ -0,0 +1,80 @@
github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo=
github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8=
github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw=
github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-andiamo/chioas v1.16.4 h1:aHtA3KLmfQfHRsGxjYPNjrD8cMR2uTncbjvcxY64B3Q=
github.com/go-andiamo/chioas v1.16.4/go.mod h1:5ZZYYuGwlF/amxErKFUu3eXz6hZ5GEYu5vCk+Guw+uc=
github.com/go-andiamo/splitter v1.2.5 h1:P3NovWMY2V14TJJSolXBvlOmGSZo3Uz+LtTl2bsV/eY=
github.com/go-andiamo/splitter v1.2.5/go.mod h1:8WHU24t9hcMKU5FXDQb1hysSEC/GPuivIp0uKY1J8gw=
github.com/go-andiamo/urit v1.2.1 h1:5JHJb+TuzuGvXw9Y/LK/lQltCL2gpgHkCznI2vv9ZeY=
github.com/go-andiamo/urit v1.2.1/go.mod h1:9kgXBxUPHFZvXwlOPN1GimDuHl+JCfQuetN5nxBNlpQ=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618=
github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/pgx/v5 v5.7.5 h1:JHGfMnQY+IEtGM63d+NGMjoRpysB2JBwDr5fsngwmJs=
github.com/jackc/pgx/v5 v5.7.5/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df h1:UA2aFVmmsIlefxMk29Dp2juaUSth8Pyn3Tq5Y5mJGME=
golang.org/x/exp v0.0.0-20230626212559-97b1e661b5df/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 h1:R9PFI6EUdfVKgwKjZef7QIwGcBKu86OEFpJ9nUEP2l4=
golang.org/x/exp v0.0.0-20250718183923-645b1fa84792/go.mod h1:A+z0yzpGtvnG90cToK5n2tu8UJVP2XUATh+r+sfOOOc=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=

100
main.go Normal file
View File

@@ -0,0 +1,100 @@
package main
import (
"fmt"
"net/http"
"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/version"
"github.com/alexflint/go-arg"
"github.com/go-chi/chi/v5"
"github.com/sirupsen/logrus"
_ "git.ostiwe.com/ostiwe-com/status/settings"
)
type ServerCmd struct {
Port string `arg:"-p,--port" help:"Port to listen on" default:"8080"`
}
type ServerDocumentationCmd struct {
Port string `arg:"-p,--port" help:"Port to listen on" default:"8081"`
Plain bool `arg:"--plain" help:"Enable plain text output" default:"true"`
PlainFormat string `arg:"--plain-format" help:"Set format for output (json, yaml)" default:"yaml"`
}
type appArgs struct {
Server *ServerCmd `arg:"subcommand:server" help:"Start the api server"`
ServerDocumentation *ServerDocumentationCmd `arg:"subcommand:server-docs" help:"Generate documentation for api server"`
}
var args appArgs
func (appArgs) Version() string {
return version.AppVersion()
}
func main() {
arg.MustParse(&args)
connect, err := db.Connect()
if err != nil {
appLog.Global.Get(appLog.SYSTEM).Error(fmt.Sprintf("Startup server error, failed connect to database: %v", err))
return
}
db.SetGlobal(connect)
appLog.Global.Get(appLog.SYSTEM).Info("Run db migration")
if err = runMigrate(); err != nil {
appLog.Global.Get(appLog.SYSTEM).Error(fmt.Sprintf("Migration failed, error: %v", err))
return
}
if args.Server != nil {
appLog.Global.Put(appLog.SERVER, logrus.New())
appLog.Global.Get(appLog.SERVER).Info("Startup server on port: ", args.Server.Port)
err := http.ListenAndServe(fmt.Sprintf(":%s", args.Server.Port), router.InitRoutes())
if err != nil {
appLog.Global.Get(appLog.SERVER).Error(fmt.Sprintf("Startup server error: %v", err))
}
return
}
if args.ServerDocumentation != nil {
appLog.Global.Get(appLog.SYSTEM).Info("Collect documentation")
docs := router.Documentate()
if !args.ServerDocumentation.Plain {
chiRouter := chi.NewRouter()
err := docs.SetupRoutes(chiRouter, docs)
if err != nil {
appLog.Global.Get(appLog.SYSTEM).Error(fmt.Sprintf("Setup docs routes error: %v", err))
return
}
appLog.Global.Get(appLog.SYSTEM).Info(fmt.Sprintf("Start documentation server on port: %s", args.ServerDocumentation.Port))
err = http.ListenAndServe(fmt.Sprintf(":%s", args.ServerDocumentation.Port), chiRouter)
if err != nil {
appLog.Global.Get(appLog.SYSTEM).Error(fmt.Sprintf("Startup server error: %v", err))
}
return
}
}
appLog.Global.Get(appLog.SYSTEM).Info("Exit from application")
}
func runMigrate() error {
return db.Global.AutoMigrate(
model.Service{},
model.Status{},
)
}

34
model/service.go Normal file
View File

@@ -0,0 +1,34 @@
package model
type HTTPConfig struct {
Authorization string `json:"authorization"`
}
type ServiceTypeCheckConfig struct {
Version string `json:"version"`
HTTPConfig *HTTPConfig `json:"http_config"`
}
type Service struct {
// Unique ID for entity
ID int `gorm:"primary_key;auto_increment" json:"id"`
// Human-readable service name
Name string `gorm:"size:255;not null" json:"name"`
// Human-readable service description
Description string `gorm:"size:255" json:"description"`
PublicDescription string `gorm:"size:255" json:"public_description"`
Public *bool `gorm:"default:false" json:"public"`
// Host to check, for example 192.168.1.44
Host string `gorm:"size:255;not null" json:"host"`
// Port to check, for example 5432 (postgresql)
Port *int `gorm:"default:null" json:"port"`
// Type for check, for now is TCP or HTTP
Type string `gorm:"size:255;not null" json:"type"`
TypeConfig *ServiceTypeCheckConfig `gorm:"serializer:json" json:"type_config"`
Statuses []Status `gorm:"foreignkey:ServiceID" json:"statuses"`
}
func (Service) TableName() string {
return "service"
}

25
model/status.go Normal file
View File

@@ -0,0 +1,25 @@
package model
import (
"time"
"gorm.io/gorm"
)
type Status struct {
ID int `gorm:"primary_key;auto_increment" json:"-"`
ServiceID int `gorm:"one" json:"-"`
Status string `gorm:"size:255;not null" json:"status"`
Description *string `gorm:"size:255" json:"description"`
CreatedAt time.Time `json:"created_at"`
}
func (Status) TableName() string {
return "status"
}
func (s *Status) BeforeCreate(*gorm.DB) error {
s.CreatedAt = time.Now()
return nil
}

53
modules/db/db.go Normal file
View File

@@ -0,0 +1,53 @@
package db
import (
"fmt"
"os"
"time"
appLog "git.ostiwe.com/ostiwe-com/status/modules/log"
"github.com/sirupsen/logrus"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var Global *gorm.DB
func init() {
appLog.Global.Put(appLog.DATABASE, logrus.New())
}
func Connect() (*gorm.DB, error) {
dsn := fmt.Sprintf(
"host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=%s",
os.Getenv("DATABASE_HOST"),
os.Getenv("DATABASE_USER"),
os.Getenv("DATABASE_PASS"),
os.Getenv("DATABASE_DB"),
os.Getenv("DATABASE_PORT"),
os.Getenv("DATABASE_TZ"),
)
newLogger := logger.New(
appLog.Global.Get(appLog.DATABASE),
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
ParameterizedQueries: false, // Don't include params in the SQL log
Colorful: false, // Disable color
},
)
return gorm.Open(
postgres.Open(dsn),
&gorm.Config{
Logger: newLogger,
},
)
}
func SetGlobal(bd *gorm.DB) {
Global = bd
}

28
modules/log/formatter.go Normal file
View File

@@ -0,0 +1,28 @@
package log
import (
"fmt"
"github.com/sirupsen/logrus"
)
type TextFormatter struct {
logrusFormatter logrus.TextFormatter
prefix []byte
}
func NewTextFormatter(prefix string) *TextFormatter {
return &TextFormatter{
logrusFormatter: logrus.TextFormatter{},
prefix: []byte(fmt.Sprintf("[%s] ", prefix)),
}
}
func (t *TextFormatter) Format(item *logrus.Entry) ([]byte, error) {
original, err := t.logrusFormatter.Format(item)
if err != nil {
return nil, err
}
return append(t.prefix, original...), nil
}

47
modules/log/manager.go Normal file
View File

@@ -0,0 +1,47 @@
package log
import (
"sync"
"github.com/sirupsen/logrus"
)
const (
SERVER = "server"
SYSTEM = "system"
DATABASE = "database"
)
var Global *LoggerManager
func SetGlobalManager(manager *LoggerManager) {
Global = manager
}
type LoggerManager struct {
loggers map[string]*logrus.Logger
mu sync.Mutex
}
func (m *LoggerManager) Get(name string) *logrus.Logger {
if logger, ok := m.loggers[name]; ok {
return logger
}
return nil
}
func (m *LoggerManager) Put(name string, logger *logrus.Logger) {
m.mu.Lock()
defer m.mu.Unlock()
logger.Formatter = NewTextFormatter(name)
m.loggers[name] = logger
}
func NewManager() *LoggerManager {
return &LoggerManager{
loggers: make(map[string]*logrus.Logger),
}
}

118
pkg/http/errors.go Normal file
View File

@@ -0,0 +1,118 @@
package http
import (
"encoding/json"
"net/http"
"github.com/go-chi/chi/v5/middleware"
)
type ResponseErr struct {
Message string `json:"message"`
Trace string `json:"trace"`
Details map[string]any `json:"details"`
}
type ResponseErrReadyToSend interface {
Send(http.ResponseWriter) error
}
type ResponseErrBuilder interface {
WithDetails(map[string]any) ResponseErrBuilder
WithMessage(string) ResponseErrBuilder
WithStatusCode(int) ResponseErrBuilder
WithTrace(string) ResponseErrBuilder
Ready() ResponseErrReadyToSend
Send(http.ResponseWriter, *http.Request) error
}
type responseErrBuilder struct {
details map[string]any
message string
status int
trace string
ready *ResponseErr
}
type readyResponseErr struct {
builder responseErrBuilder
}
func (r readyResponseErr) Send(response http.ResponseWriter) error {
format, err := json.Marshal(r.builder.ready)
if err != nil {
return err
}
if r.builder.status == 0 {
r.builder.status = http.StatusInternalServerError
}
response.WriteHeader(r.builder.status)
_, err = response.Write(format)
return err
}
func (r responseErrBuilder) WithTrace(s string) ResponseErrBuilder {
r.trace = s
return r
}
func (r responseErrBuilder) WithStatusCode(i int) ResponseErrBuilder {
r.status = i
return r
}
func NewResponseErrBuilder() ResponseErrBuilder {
return &responseErrBuilder{}
}
func (r responseErrBuilder) WithDetails(m map[string]any) ResponseErrBuilder {
r.details = m
return r
}
func (r responseErrBuilder) WithMessage(s string) ResponseErrBuilder {
r.message = s
return r
}
func (r responseErrBuilder) Ready() ResponseErrReadyToSend {
r.ready = &ResponseErr{
Message: r.message,
Trace: r.trace,
Details: r.details,
}
return readyResponseErr{
builder: r,
}
}
func (r responseErrBuilder) Send(response http.ResponseWriter, request *http.Request) error {
if r.ready == nil {
r.ready = &ResponseErr{
Message: r.message,
Trace: middleware.GetReqID(request.Context()),
Details: r.details,
}
}
format, err := json.Marshal(r.ready)
if err != nil {
return err
}
if r.status == 0 {
r.status = http.StatusInternalServerError
}
response.WriteHeader(r.status)
_, err = response.Write(format)
return err
}

46
pkg/http/extract.go Normal file
View File

@@ -0,0 +1,46 @@
package http
import (
"net/http"
"strconv"
)
func ExtractLimitOffset(r *http.Request) (int, int, ResponseErrReadyToSend) {
limit, offset := r.URL.Query().Get("limit"), r.URL.Query().Get("offset")
if limit == "" {
limit = "20"
}
if offset == "" {
offset = "0"
}
limitInt, err := strconv.Atoi(limit)
if err != nil {
writeErr := NewResponseErrBuilder().
WithMessage("Incorrect limit parameter").
WithDetails(map[string]interface{}{
"err": err.Error(),
}).
WithStatusCode(http.StatusBadRequest).
Ready()
return 0, 0, writeErr
}
offsetInt, err := strconv.Atoi(offset)
if err != nil {
writeErr := NewResponseErrBuilder().
WithMessage("Incorrect offset parameter").
WithDetails(map[string]interface{}{
"err": err.Error(),
}).
WithStatusCode(http.StatusBadRequest).
Ready()
return 0, 0, writeErr
}
return limitInt, offsetInt, nil
}

19
pkg/http/response.go Normal file
View File

@@ -0,0 +1,19 @@
package http
import (
"encoding/json"
"net/http"
)
func JSON(w http.ResponseWriter, i any, httpCode int) error {
response, err := json.Marshal(i)
if err != nil {
return err
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(httpCode)
_, err = w.Write(response)
return err
}

View File

@@ -0,0 +1,57 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: service.go
//
// Generated by this command:
//
// mockgen -source=service.go -destination=mocks/service.go
//
// Package mock_repository is a generated GoMock package.
package mock_repository
import (
context "context"
reflect "reflect"
model "git.ostiwe.com/ostiwe-com/status/model"
gomock "go.uber.org/mock/gomock"
)
// MockService is a mock of Service interface.
type MockService struct {
ctrl *gomock.Controller
recorder *MockServiceMockRecorder
isgomock struct{}
}
// MockServiceMockRecorder is the mock recorder for MockService.
type MockServiceMockRecorder struct {
mock *MockService
}
// NewMockService creates a new mock instance.
func NewMockService(ctrl *gomock.Controller) *MockService {
mock := &MockService{ctrl: ctrl}
mock.recorder = &MockServiceMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockService) EXPECT() *MockServiceMockRecorder {
return m.recorder
}
// All mocks base method.
func (m *MockService) All(ctx context.Context, limit, offset int) ([]model.Service, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "All", ctx, limit, offset)
ret0, _ := ret[0].([]model.Service)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// All indicates an expected call of All.
func (mr *MockServiceMockRecorder) All(ctx, limit, offset any) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "All", reflect.TypeOf((*MockService)(nil).All), ctx, limit, offset)
}

7
repository/repository.go Normal file
View File

@@ -0,0 +1,7 @@
package repository
import "gorm.io/gorm"
type repository struct {
db *gorm.DB
}

50
repository/service.go Normal file
View File

@@ -0,0 +1,50 @@
package repository
//go:generate mockgen -source=$GOFILE -destination=mocks/$GOFILE
import (
"context"
"time"
"git.ostiwe.com/ostiwe-com/status/model"
"git.ostiwe.com/ostiwe-com/status/modules/db"
"gorm.io/gorm"
)
type Service interface {
All(ctx context.Context, limit, offset int, publicOnly bool) ([]model.Service, error)
}
type service struct {
repository
}
func NewServiceRepository() Service {
return &service{
repository{db: db.Global},
}
}
func (s *service) All(ctx context.Context, limit, offset int, publicOnly bool) ([]model.Service, error) {
items := make([]model.Service, 0)
query := s.db.
WithContext(ctx).
Preload("Statuses", func(db *gorm.DB) *gorm.DB {
return db.
Where("created_at > ?", time.Now().Truncate(24*time.Hour)).
Order("created_at desc")
})
if publicOnly {
query = query.Where("public = ?", true)
}
query = query.
Limit(limit).
Offset(offset).
Find(&items)
return items, query.
Error
}

View File

@@ -0,0 +1,12 @@
package controller
import (
"github.com/go-andiamo/chioas"
"github.com/go-chi/chi/v5"
)
type Controller interface {
New() Controller
Group(r chi.Router)
Documentate() (*chioas.Paths, *chioas.Components)
}

View File

@@ -0,0 +1,57 @@
package ping
import (
"net/http"
"git.ostiwe.com/ostiwe-com/status/modules/log"
"git.ostiwe.com/ostiwe-com/status/router/controller"
"github.com/go-andiamo/chioas"
"github.com/go-chi/chi/v5"
)
type Controller struct {
}
func (Controller) New() controller.Controller {
return &Controller{}
}
func (c *Controller) Group(r chi.Router) {
r.Get("/ping", c.PingHandler)
}
func (c *Controller) PingHandler(w http.ResponseWriter, _ *http.Request) {
_, err := w.Write([]byte("pong"))
if err != nil {
log.Global.Get(log.SERVER).Error(err)
return
}
}
func (c *Controller) Documentate() (*chioas.Paths, *chioas.Components) {
return &chioas.Paths{
"/ping": chioas.Path{
Methods: map[string]chioas.Method{
http.MethodGet: {
Handler: c.PingHandler,
Responses: chioas.Responses{
http.StatusOK: {
ContentType: "plain/text",
Schema: chioas.Schema{Type: "string"},
Examples: chioas.Examples{
{
Name: "200 OK",
Value: "pong",
},
},
},
},
},
},
Tag: "service",
Comment: "Route for check service is alive",
},
}, nil
}

View File

@@ -0,0 +1,106 @@
package service
import (
"net/http"
"git.ostiwe.com/ostiwe-com/status/modules/log"
http2 "git.ostiwe.com/ostiwe-com/status/pkg/http"
"git.ostiwe.com/ostiwe-com/status/repository"
"git.ostiwe.com/ostiwe-com/status/router/controller"
"git.ostiwe.com/ostiwe-com/status/transform"
"github.com/go-andiamo/chioas"
"github.com/go-chi/chi/v5"
)
type Controller struct {
serviceRepository repository.Service
}
func (c Controller) New() controller.Controller {
return &Controller{
serviceRepository: repository.NewServiceRepository(),
}
}
func (c *Controller) Group(r chi.Router) {
c.public(r)
c.internal(r)
}
func (c *Controller) public(r chi.Router) {
r.Get("/api/public/service", c.GetAllServicesPublic)
}
func (c *Controller) internal(r chi.Router) {
r.Get("/api/v1/service", c.GetAllServices)
}
func (c *Controller) GetAllServicesPublic(w http.ResponseWriter, r *http.Request) {
limit, offset, errReady := http2.ExtractLimitOffset(r)
if errReady != nil {
err := errReady.Send(w)
if err != nil {
log.Global.Get(log.SERVER).Error(err)
return
}
return
}
items, err := c.serviceRepository.All(r.Context(), limit, offset, true)
if err != nil {
log.Global.Get(log.SERVER).Error(err)
writeErr := http2.NewResponseErrBuilder().
WithMessage("Fetch service error").
WithStatusCode(http.StatusInternalServerError).
Send(w, r)
if writeErr != nil {
log.Global.Get(log.SERVER).Error(writeErr)
}
return
}
err = http2.JSON(w, transform.PublicServices(items...), http.StatusOK)
if err != nil {
log.Global.Get(log.SERVER).Error(err)
}
}
func (c *Controller) GetAllServices(w http.ResponseWriter, r *http.Request) {
limit, offset, errReady := http2.ExtractLimitOffset(r)
if errReady != nil {
err := errReady.Send(w)
if err != nil {
log.Global.Get(log.SERVER).Error(err)
return
}
return
}
items, err := c.serviceRepository.All(r.Context(), limit, offset, false)
if err != nil {
log.Global.Get(log.SERVER).Error(err)
writeErr := http2.NewResponseErrBuilder().
WithMessage("Fetch service error").
WithStatusCode(http.StatusInternalServerError).
Send(w, r)
if writeErr != nil {
log.Global.Get(log.SERVER).Error(writeErr)
}
return
}
err = http2.JSON(w, items, http.StatusOK)
if err != nil {
log.Global.Get(log.SERVER).Error(err)
}
}
func (c *Controller) Documentate() (*chioas.Paths, *chioas.Components) {
return nil, nil
}

99
router/init.go Normal file
View File

@@ -0,0 +1,99 @@
package router
import (
"fmt"
"maps"
"time"
"git.ostiwe.com/ostiwe-com/status/modules/log"
"git.ostiwe.com/ostiwe-com/status/router/controller"
"git.ostiwe.com/ostiwe-com/status/router/controller/ping"
"git.ostiwe.com/ostiwe-com/status/router/controller/service"
"git.ostiwe.com/ostiwe-com/status/version"
"github.com/go-andiamo/chioas"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
func getControllers() []controller.Controller {
return []controller.Controller{
ping.Controller{}.New(),
service.Controller{}.New(),
}
}
func InitRoutes() *chi.Mux {
log.Global.Get(log.SERVER).Info("Setting up routers")
startTime := time.Now()
httpLogger := middleware.RequestLogger(&middleware.DefaultLogFormatter{Logger: log.Global.Get(log.SERVER), NoColor: false})
r := chi.NewRouter()
r.Use(httpLogger)
r.Use(
middleware.RequestID,
middleware.RealIP,
middleware.StripSlashes,
middleware.Recoverer,
middleware.CleanPath,
middleware.Timeout(15*time.Second),
)
ctrlList := getControllers()
for _, ctrl := range ctrlList {
r.Group(ctrl.Group)
}
log.Global.Get(log.SERVER).Info(fmt.Sprintf("Initialized %d routers", len(ctrlList)))
log.Global.Get(log.SERVER).Info(fmt.Sprintf("Setting up routers is done for %dms, start server", time.Since(startTime).Milliseconds()))
return r
}
func Documentate() chioas.Definition {
ctrlList := getControllers()
apiDoc := chioas.Definition{
AutoHeadMethods: true,
DocOptions: chioas.DocOptions{
ServeDocs: true,
HideHeadMethods: true,
},
Info: chioas.Info{
Version: version.AppVersion(),
Title: "Status page API Documentation",
},
Paths: make(chioas.Paths),
Components: &chioas.Components{
Schemas: make(chioas.Schemas, 0),
Requests: make(chioas.CommonRequests),
Responses: make(chioas.CommonResponses),
Examples: make(chioas.Examples, 0),
Parameters: make(chioas.CommonParameters),
SecuritySchemes: make(chioas.SecuritySchemes, 0),
Extensions: make(chioas.Extensions),
},
}
for _, ctrl := range ctrlList {
documentatePaths, components := ctrl.Documentate()
if documentatePaths != nil {
for path, pathDoc := range *documentatePaths {
apiDoc.Paths[path] = pathDoc
}
}
if components != nil {
apiDoc.Components.Schemas = append(apiDoc.Components.Schemas, components.Schemas...)
apiDoc.Components.Examples = append(apiDoc.Components.Examples, components.Examples...)
apiDoc.Components.SecuritySchemes = append(apiDoc.Components.SecuritySchemes, components.SecuritySchemes...)
maps.Copy(apiDoc.Components.Requests, components.Requests)
maps.Copy(apiDoc.Components.Responses, components.Responses)
maps.Copy(apiDoc.Components.Parameters, components.Parameters)
maps.Copy(apiDoc.Components.Extensions, components.Extensions)
}
}
return apiDoc
}

48
settings/settings.go Normal file
View File

@@ -0,0 +1,48 @@
package settings
import (
"os"
"time"
appLog "git.ostiwe.com/ostiwe-com/status/modules/log"
"github.com/joho/godotenv"
"github.com/sirupsen/logrus"
)
var (
AppVersion string
AppStartTime time.Time
)
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")
if env == "" {
env = "development"
}
appLog.Global.Get(appLog.SYSTEM).Info("Load environment variables for ", env, " env")
err := godotenv.Load()
if err != nil {
appLog.Global.Get(appLog.SYSTEM).Errorf("Error loading .env file %v", err)
appLog.Global.Get(appLog.SYSTEM).Exit(1)
}
appLog.Global.Get(appLog.SYSTEM).Info("Loaded .env file")
appLog.Global.Get(appLog.SYSTEM).Info("Try to load .env.local file")
if err = godotenv.Overload(".env.local"); err != nil {
appLog.Global.Get(appLog.SYSTEM).Info("Failed to load .env.local file")
} else {
appLog.Global.Get(appLog.SYSTEM).Info("Loaded .env.local file")
}
}

24
transform/service.go Normal file
View File

@@ -0,0 +1,24 @@
package transform
import (
"git.ostiwe.com/ostiwe-com/status/dto"
"git.ostiwe.com/ostiwe-com/status/model"
)
func PublicServices(items ...model.Service) []dto.PublicService {
result := make([]dto.PublicService, 0, len(items))
for _, item := range items {
result = append(result, PublicService(item))
}
return result
}
func PublicService(item model.Service) dto.PublicService {
return dto.PublicService{
Name: item.Name,
Description: item.Description,
Statuses: item.Statuses,
}
}

5
version/version.go Normal file
View File

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