Application superstructure (or supersingleton, if you want).
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:
2022-08-19 21:52:49 +05:00
parent b87921c811
commit 5fc6d3a181
35 changed files with 589 additions and 440 deletions

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

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

View 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")
}

View 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)
}
}

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

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

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

View 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"`
}

View File

@@ -0,0 +1,6 @@
package application
const (
// Version .
Version = "0.4.1"
)