Application superstructure (or supersingleton, if you want).
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
Got rid of context thing which misleads due to existance of stdlib's context package. Also fixed golangci-lint configuration. Fixes #20.
This commit is contained in:
124
internal/application/application.go
Normal file
124
internal/application/application.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/rs/zerolog"
|
||||
databaseinterface "go.dev.pztrn.name/fastpastebin/internal/database/interface"
|
||||
)
|
||||
|
||||
// Application is a main application superstructure. It passes around all parts of application
|
||||
// and serves as lifecycle management thing as well as kind-of-dependency-injection thing.
|
||||
type Application struct {
|
||||
Config *Config
|
||||
Database databaseinterface.Interface
|
||||
Echo *echo.Echo
|
||||
Log zerolog.Logger
|
||||
services map[string]Service
|
||||
servicesMutex sync.RWMutex
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
// New creates new application superstructure.
|
||||
func New() *Application {
|
||||
//nolint:exhaustruct
|
||||
appl := &Application{}
|
||||
appl.initialize()
|
||||
|
||||
return appl
|
||||
}
|
||||
|
||||
// GetContext returns application-wide context.
|
||||
func (a *Application) GetContext() context.Context {
|
||||
return a.ctx
|
||||
}
|
||||
|
||||
// GetService returns interface{} with requested service or error if service wasn't registered.
|
||||
func (a *Application) GetService(name string) (Service, error) {
|
||||
a.servicesMutex.RLock()
|
||||
srv, found := a.services[name]
|
||||
a.servicesMutex.RUnlock()
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf("%s: %w", ErrApplicationError, ErrApplicationServiceNotRegistered)
|
||||
}
|
||||
|
||||
return srv, nil
|
||||
}
|
||||
|
||||
// Initializes internal state.
|
||||
func (a *Application) initialize() {
|
||||
a.Log = zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||
a.Log.Info().Msg("Initializing Application...")
|
||||
|
||||
a.ctx, a.cancelFunc = context.WithCancel(context.Background())
|
||||
|
||||
a.services = make(map[string]Service)
|
||||
|
||||
cfg, err := newConfig(a)
|
||||
if err != nil {
|
||||
a.Log.Fatal().Err(err).Msg("Failed to initialize configuration!")
|
||||
}
|
||||
|
||||
a.Config = cfg
|
||||
|
||||
a.initializeLogger()
|
||||
a.initializeHTTPServer()
|
||||
}
|
||||
|
||||
// RegisterService registers service for later re-use everywhere it's needed.
|
||||
func (a *Application) RegisterService(srv Service) error {
|
||||
a.servicesMutex.Lock()
|
||||
_, found := a.services[srv.GetName()]
|
||||
a.servicesMutex.Unlock()
|
||||
|
||||
if found {
|
||||
return fmt.Errorf("%s: %w", ErrApplicationError, ErrApplicationServiceAlreadyRegistered)
|
||||
}
|
||||
|
||||
if err := srv.Initialize(); err != nil {
|
||||
return fmt.Errorf("%s: %s: %w", ErrApplicationError, ErrApplicationServiceRegister, err)
|
||||
}
|
||||
|
||||
a.services[srv.GetName()] = srv
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown shutdowns application.
|
||||
func (a *Application) Shutdown() error {
|
||||
a.cancelFunc()
|
||||
|
||||
a.servicesMutex.RLock()
|
||||
defer a.servicesMutex.RUnlock()
|
||||
|
||||
for _, service := range a.services {
|
||||
if err := service.Shutdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start starts application.
|
||||
func (a *Application) Start() error {
|
||||
a.initializeLoggerPost()
|
||||
a.startHTTPServer()
|
||||
|
||||
a.servicesMutex.RLock()
|
||||
defer a.servicesMutex.RUnlock()
|
||||
|
||||
for _, service := range a.services {
|
||||
if err := service.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
18
internal/application/application_errors.go
Normal file
18
internal/application/application_errors.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package application
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrApplicationError indicates that error belongs to Application.
|
||||
ErrApplicationError = errors.New("application")
|
||||
|
||||
// ErrApplicationServiceRegister appears when trying to register (and initialize) a service
|
||||
// but something went wrong.
|
||||
ErrApplicationServiceRegister = errors.New("service registering and initialization")
|
||||
|
||||
// ErrApplicationServiceAlreadyRegistered appears when trying to register service with already used name.
|
||||
ErrApplicationServiceAlreadyRegistered = errors.New("service already registered")
|
||||
|
||||
// ErrApplicationServiceNotRegistered appears when trying to obtain a service that wasn't previously registered.
|
||||
ErrApplicationServiceNotRegistered = errors.New("service not registered")
|
||||
)
|
48
internal/application/application_http_server.go
Normal file
48
internal/application/application_http_server.go
Normal file
@@ -0,0 +1,48 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/labstack/echo"
|
||||
"github.com/labstack/echo/middleware"
|
||||
"go.dev.pztrn.name/fastpastebin/assets"
|
||||
)
|
||||
|
||||
// Wrapper around previous function.
|
||||
func (a *Application) echoReqLogger() echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(ectx echo.Context) error {
|
||||
a.Log.Info().
|
||||
Str("IP", ectx.RealIP()).
|
||||
Str("Host", ectx.Request().Host).
|
||||
Str("Method", ectx.Request().Method).
|
||||
Str("Path", ectx.Request().URL.Path).
|
||||
Str("UA", ectx.Request().UserAgent()).
|
||||
Msg("HTTP request")
|
||||
|
||||
return next(ectx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Application) initializeHTTPServer() {
|
||||
a.Echo = echo.New()
|
||||
a.Echo.Use(a.echoReqLogger())
|
||||
a.Echo.Use(middleware.Recover())
|
||||
a.Echo.Use(middleware.BodyLimit(a.Config.HTTP.MaxBodySizeMegabytes + "M"))
|
||||
a.Echo.DisableHTTP2 = true
|
||||
a.Echo.HideBanner = true
|
||||
a.Echo.HidePort = true
|
||||
|
||||
// Static files.
|
||||
a.Echo.GET("/static/*", echo.WrapHandler(http.FileServer(http.FS(assets.Data))))
|
||||
}
|
||||
|
||||
func (a *Application) startHTTPServer() {
|
||||
listenAddress := a.Config.HTTP.Address + ":" + a.Config.HTTP.Port
|
||||
|
||||
go func() {
|
||||
a.Echo.Logger.Fatal(a.Echo.Start(listenAddress))
|
||||
}()
|
||||
a.Log.Info().Str("address", listenAddress).Msg("Started HTTP server")
|
||||
}
|
79
internal/application/application_logger.go
Normal file
79
internal/application/application_logger.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Puts memory usage into log lines.
|
||||
func (a *Application) getMemoryUsage(event *zerolog.Event, level zerolog.Level, message string) {
|
||||
var memstats runtime.MemStats
|
||||
|
||||
runtime.ReadMemStats(&memstats)
|
||||
|
||||
event.Str("memalloc", fmt.Sprintf("%dMB", memstats.Alloc/1024/1024))
|
||||
event.Str("memsys", fmt.Sprintf("%dMB", memstats.Sys/1024/1024))
|
||||
event.Str("numgc", fmt.Sprintf("%d", memstats.NumGC))
|
||||
}
|
||||
|
||||
// Initializes logger.
|
||||
func (a *Application) initializeLogger() {
|
||||
// Устанавливаем форматирование логгера.
|
||||
//nolint:exhaustruct
|
||||
output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: false, TimeFormat: time.RFC3339}
|
||||
output.FormatLevel = func(lvlRaw interface{}) string {
|
||||
var lvl string
|
||||
|
||||
if lvlAsString, ok := lvlRaw.(string); ok {
|
||||
lvlAsString = strings.ToUpper(lvlAsString)
|
||||
switch lvlAsString {
|
||||
case "DEBUG":
|
||||
lvl = fmt.Sprintf("\x1b[30m%-5s\x1b[0m", lvlAsString)
|
||||
case "ERROR":
|
||||
lvl = fmt.Sprintf("\x1b[31m%-5s\x1b[0m", lvlAsString)
|
||||
case "FATAL":
|
||||
lvl = fmt.Sprintf("\x1b[35m%-5s\x1b[0m", lvlAsString)
|
||||
case "INFO":
|
||||
lvl = fmt.Sprintf("\x1b[32m%-5s\x1b[0m", lvlAsString)
|
||||
case "PANIC":
|
||||
lvl = fmt.Sprintf("\x1b[36m%-5s\x1b[0m", lvlAsString)
|
||||
case "WARN":
|
||||
lvl = fmt.Sprintf("\x1b[33m%-5s\x1b[0m", lvlAsString)
|
||||
default:
|
||||
lvl = lvlAsString
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf("| %s |", lvl)
|
||||
}
|
||||
|
||||
a.Log = zerolog.New(output).With().Timestamp().Logger()
|
||||
|
||||
a.Log = a.Log.Hook(zerolog.HookFunc(a.getMemoryUsage))
|
||||
}
|
||||
|
||||
// Initialize logger after configuration parse.
|
||||
func (a *Application) initializeLoggerPost() {
|
||||
// Set log level.
|
||||
a.Log.Info().Msgf("Setting logger level: %s", a.Config.Logging.LogLevel)
|
||||
|
||||
switch a.Config.Logging.LogLevel {
|
||||
case "DEBUG":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "INFO":
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
case "WARN":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "ERROR":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
case "FATAL":
|
||||
zerolog.SetGlobalLevel(zerolog.FatalLevel)
|
||||
case "PANIC":
|
||||
zerolog.SetGlobalLevel(zerolog.PanicLevel)
|
||||
}
|
||||
}
|
13
internal/application/application_service.go
Normal file
13
internal/application/application_service.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package application
|
||||
|
||||
// Service is a generic interface for all services of application.
|
||||
type Service interface {
|
||||
// GetName returns service name for registering with application superstructure.
|
||||
GetName() string
|
||||
// Initialize initializes service.
|
||||
Initialize() error
|
||||
// Shutdown shuts service down if needed. Also should block is shutdown should be done in synchronous manner.
|
||||
Shutdown() error
|
||||
// Start starts service if needed. Should not block execution.
|
||||
Start() error
|
||||
}
|
64
internal/application/config.go
Normal file
64
internal/application/config.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.dev.pztrn.name/fastpastebin/internal/helpers"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Config represents configuration structure.
|
||||
type Config struct {
|
||||
app *Application
|
||||
log zerolog.Logger
|
||||
|
||||
Database ConfigDatabase `yaml:"database"`
|
||||
Logging ConfigLogging `yaml:"logging"`
|
||||
HTTP ConfigHTTP `yaml:"http"`
|
||||
Pastes ConfigPastes `yaml:"pastes"`
|
||||
}
|
||||
|
||||
func newConfig(app *Application) (*Config, error) {
|
||||
//nolint:exhaustruct
|
||||
cfg := &Config{
|
||||
app: app,
|
||||
log: app.Log.With().Str("type", "core").Str("name", "configuration").Logger(),
|
||||
}
|
||||
|
||||
if err := cfg.initialize(); err != nil {
|
||||
return nil, fmt.Errorf("%s: %w", ErrConfigurationError, err)
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c *Config) initialize() error {
|
||||
c.log.Info().Msg("Initializing configuration...")
|
||||
|
||||
configPathRaw, found := os.LookupEnv("FASTPASTEBIN_CONFIG")
|
||||
if !found {
|
||||
return fmt.Errorf("%s: %w", ErrConfigurationLoad, ErrConfigurationPathNotDefined)
|
||||
}
|
||||
|
||||
configPath, err := helpers.NormalizePath(configPathRaw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", ErrConfigurationLoad, err)
|
||||
}
|
||||
|
||||
c.log.Info().Str("config path", configPath).Msg("Reading configuration file...")
|
||||
|
||||
fileData, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: %w", ErrConfigurationLoad, err)
|
||||
}
|
||||
|
||||
if err := yaml.Unmarshal(fileData, c); err != nil {
|
||||
return fmt.Errorf("%s: %w", ErrConfigurationLoad, err)
|
||||
}
|
||||
|
||||
c.log.Debug().Msgf("Configuration loaded: %+v", c)
|
||||
|
||||
return nil
|
||||
}
|
15
internal/application/config_errors.go
Normal file
15
internal/application/config_errors.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package application
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// ErrConfigurationError indicates that this error is related to configuration.
|
||||
ErrConfigurationError = errors.New("configuration")
|
||||
|
||||
// ErrConfigurationLoad indicates that error appears when trying to load
|
||||
// configuration data from file.
|
||||
ErrConfigurationLoad = errors.New("loading configuration")
|
||||
|
||||
// ErrConfigurationPathNotDefined indicates that CONFIG_PATH environment variable is empty or not defined.
|
||||
ErrConfigurationPathNotDefined = errors.New("configuration path (CONFIG_PATH) is empty or not defined")
|
||||
)
|
30
internal/application/config_structs.go
Normal file
30
internal/application/config_structs.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package application
|
||||
|
||||
// ConfigDatabase describes database configuration.
|
||||
type ConfigDatabase struct {
|
||||
Type string `yaml:"type"`
|
||||
Path string `yaml:"path"`
|
||||
Address string `yaml:"address"`
|
||||
Port string `yaml:"port"`
|
||||
Username string `yaml:"username"`
|
||||
Password string `yaml:"password"`
|
||||
Database string `yaml:"database"`
|
||||
}
|
||||
|
||||
// ConfigHTTP describes HTTP server configuration.
|
||||
type ConfigHTTP struct {
|
||||
Address string `yaml:"address"`
|
||||
Port string `yaml:"port"`
|
||||
MaxBodySizeMegabytes string `yaml:"max_body_size_megabytes"`
|
||||
AllowInsecure bool `yaml:"allow_insecure"`
|
||||
}
|
||||
|
||||
// ConfigLogging describes logger configuration.
|
||||
type ConfigLogging struct {
|
||||
LogLevel string `yaml:"loglevel"`
|
||||
}
|
||||
|
||||
// ConfigPastes describes pastes subsystem configuration.
|
||||
type ConfigPastes struct {
|
||||
Pagination int `yaml:"pagination"`
|
||||
}
|
6
internal/application/vars.go
Normal file
6
internal/application/vars.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package application
|
||||
|
||||
const (
|
||||
// Version .
|
||||
Version = "0.4.1"
|
||||
)
|
Reference in New Issue
Block a user