Codevault initial commit
This commit is contained in:
74
internal/config/config.go
Normal file
74
internal/config/config.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
var ErrLoggingDisabled error = errors.New("logging is disabled")
|
||||
|
||||
type Config struct {
|
||||
Environment Environment `yaml:"environment"`
|
||||
Server ServerConfig `yaml:"server"`
|
||||
Database DatabaseConfig `yaml:"database"`
|
||||
RateLimit RateLimitConfig `yaml:"rate_limit"`
|
||||
Observability ObservabilityConfig `yaml:"observability"`
|
||||
}
|
||||
|
||||
func (c *Config) Validate() error {
|
||||
var err error
|
||||
|
||||
err = addValidationError(err, "environment validation error", c.Environment.PreprocessAndValidate())
|
||||
err = addValidationError(err, "server section validation error", c.Server.Validate())
|
||||
err = addValidationError(err, "database section validation error", c.Database.Validate())
|
||||
err = addValidationError(err, "rate_limit section validation error", c.RateLimit.Validate())
|
||||
err = addValidationError(err, "observability section validation error", c.Observability.Validate())
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func ReadConfig(path string) (Config, error) {
|
||||
var c Config
|
||||
yamlFile, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("could not read config: %w", err)
|
||||
}
|
||||
err = yaml.Unmarshal(yamlFile, &c)
|
||||
if err != nil {
|
||||
return c, fmt.Errorf("could not parse yaml: %w", err)
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type Environment string
|
||||
|
||||
var (
|
||||
ProductionEnvironment Environment = "production"
|
||||
DevelopmentEnvironment Environment = "development"
|
||||
)
|
||||
|
||||
func (e *Environment) PreprocessAndValidate() error {
|
||||
preprocessed := strings.ToLower(string(*e))
|
||||
switch preprocessed {
|
||||
case string(ProductionEnvironment), "prod":
|
||||
*e = ProductionEnvironment
|
||||
return nil
|
||||
case string(DevelopmentEnvironment), "dev":
|
||||
*e = DevelopmentEnvironment
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid Environment value: '%v'", *e)
|
||||
}
|
||||
}
|
||||
|
||||
func addValidationError(err error, text string, validationError error) error {
|
||||
if validationError == nil {
|
||||
return err
|
||||
}
|
||||
return multierr.Append(err, fmt.Errorf("%s: %w", text, validationError))
|
||||
}
|
||||
14
internal/config/database.go
Normal file
14
internal/config/database.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package config
|
||||
|
||||
import "fmt"
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Path string `yaml:"path"`
|
||||
}
|
||||
|
||||
func (dc *DatabaseConfig) Validate() error {
|
||||
if dc.Path == "" {
|
||||
return fmt.Errorf("path is missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
131
internal/config/observability.go
Normal file
131
internal/config/observability.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
type ObservabilityConfig struct {
|
||||
Logging LoggingConfig `yaml:"logging"`
|
||||
Metrics MetricsConfig `yaml:"metrics"`
|
||||
}
|
||||
|
||||
func (oc *ObservabilityConfig) Validate() error {
|
||||
var err error
|
||||
|
||||
err = addValidationError(err, "logging section validation error", oc.Logging.Validate())
|
||||
err = addValidationError(err, "metrics section validation error", oc.Metrics.Validate())
|
||||
return err
|
||||
}
|
||||
|
||||
type LoggingConfig struct {
|
||||
Stderr LoggingLevel `yaml:"stderr"`
|
||||
Files map[LoggingLevel]string `yaml:"files"`
|
||||
Exporter LogsExporterConfig `yaml:"exporter"`
|
||||
}
|
||||
|
||||
func (lc *LoggingConfig) Validate() error {
|
||||
var err error
|
||||
|
||||
_, loggingErr := lc.Stderr.ToZapLevel()
|
||||
if !errors.Is(loggingErr, ErrLoggingDisabled) {
|
||||
err = addValidationError(err, "stderr logging level validation error", loggingErr)
|
||||
}
|
||||
for level, filename := range lc.Files {
|
||||
_, loggingErr = level.ToZapLevel()
|
||||
err = addValidationError(err, fmt.Sprintf("logging level validation error for file '%s'", filename), loggingErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type LogsExporterConfig struct {
|
||||
Level LoggingLevel `yaml:"level"`
|
||||
Endpoint string `yaml:"endpoint"`
|
||||
Transport OTLPTransportType `yaml:"transport"`
|
||||
}
|
||||
|
||||
func (lec *LogsExporterConfig) Validate() error {
|
||||
var err error
|
||||
|
||||
_, loggingErr := lec.Level.ToZapLevel()
|
||||
if errors.Is(loggingErr, ErrLoggingDisabled) {
|
||||
return nil
|
||||
}
|
||||
err = addValidationError(err, "logging level validation error", loggingErr)
|
||||
if !strings.HasPrefix(lec.Endpoint, "http") {
|
||||
return fmt.Errorf("endpoint must has http(s) prefix")
|
||||
}
|
||||
err = addValidationError(err, "transport type validation error", lec.Transport.PreprocessAndValidate())
|
||||
return err
|
||||
}
|
||||
|
||||
type OTLPTransportType string
|
||||
|
||||
var (
|
||||
GRPCTransportType OTLPTransportType = "grpc"
|
||||
HTTPTransportType OTLPTransportType = "http"
|
||||
)
|
||||
|
||||
func (otp *OTLPTransportType) PreprocessAndValidate() error {
|
||||
if *otp == "" {
|
||||
*otp = GRPCTransportType
|
||||
return nil
|
||||
}
|
||||
preprocessed := OTLPTransportType(strings.ToLower(string(*otp)))
|
||||
switch preprocessed {
|
||||
case GRPCTransportType, HTTPTransportType:
|
||||
*otp = preprocessed
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("invalid OTLPTransportType: '%v'", *otp)
|
||||
}
|
||||
}
|
||||
|
||||
type MetricsConfig struct {
|
||||
Host string `yaml:"host"`
|
||||
Port int `yaml:"port"`
|
||||
}
|
||||
|
||||
func (mc *MetricsConfig) Validate() error {
|
||||
if mc.Host == "" {
|
||||
return nil
|
||||
}
|
||||
if mc.Port < 1 || mc.Port > 65535 {
|
||||
return fmt.Errorf("port value is invalid")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type LoggingLevel string
|
||||
|
||||
var (
|
||||
DebugLoggingLevel LoggingLevel = "DEBUG"
|
||||
InfoLoggingLevel LoggingLevel = "INFO"
|
||||
WarningLoggingLevel LoggingLevel = "WARNING"
|
||||
ErrorLoggingLevel LoggingLevel = "ERROR"
|
||||
PanicLoggingLevel LoggingLevel = "PANIC"
|
||||
FatalLoggingLevel LoggingLevel = "FATAL"
|
||||
)
|
||||
|
||||
func (l LoggingLevel) ToZapLevel() (zapcore.Level, error) {
|
||||
if l == "" {
|
||||
return zapcore.InvalidLevel, ErrLoggingDisabled
|
||||
}
|
||||
levelString := strings.ToLower(string(l))
|
||||
mapping := map[string]zapcore.Level{
|
||||
"debug": zapcore.DebugLevel,
|
||||
"info": zapcore.InfoLevel,
|
||||
"warn": zapcore.WarnLevel,
|
||||
"warning": zapcore.WarnLevel,
|
||||
"error": zapcore.ErrorLevel,
|
||||
"panic": zapcore.PanicLevel,
|
||||
"fatal": zapcore.FatalLevel,
|
||||
}
|
||||
if level, ok := mapping[levelString]; ok {
|
||||
return level, nil
|
||||
}
|
||||
return zapcore.InvalidLevel, fmt.Errorf("invalid logging level value: %v", l)
|
||||
}
|
||||
21
internal/config/ratelimit.go
Normal file
21
internal/config/ratelimit.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RateLimitConfig struct {
|
||||
MaxFailures int `yaml:"max_failures"`
|
||||
Window time.Duration `yaml:"window"`
|
||||
}
|
||||
|
||||
func (rc *RateLimitConfig) Validate() error {
|
||||
if rc.MaxFailures <= 0 {
|
||||
return fmt.Errorf("max_failures must be positive")
|
||||
}
|
||||
if rc.Window <= 0 {
|
||||
return fmt.Errorf("window must be positive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
37
internal/config/server.go
Normal file
37
internal/config/server.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/netip"
|
||||
|
||||
"go.uber.org/multierr"
|
||||
)
|
||||
|
||||
type ServerConfig struct {
|
||||
Address string `yaml:"address"`
|
||||
|
||||
// TrustedProxies lists IPs/CIDRs (e.g. of a fronting nginx) allowed to set
|
||||
// X-Forwarded-For/X-Real-IP when gin resolves the client IP. Empty means no
|
||||
// forwarded header is trusted, and the raw remote address is used instead.
|
||||
TrustedProxies []string `yaml:"trusted_proxies"`
|
||||
}
|
||||
|
||||
func (sc *ServerConfig) Validate() error {
|
||||
var err error
|
||||
|
||||
if sc.Address == "" {
|
||||
err = multierr.Append(err, fmt.Errorf("address is missing"))
|
||||
}
|
||||
|
||||
for _, proxy := range sc.TrustedProxies {
|
||||
if _, parseErr := netip.ParsePrefix(proxy); parseErr == nil {
|
||||
continue
|
||||
}
|
||||
if _, parseErr := netip.ParseAddr(proxy); parseErr == nil {
|
||||
continue
|
||||
}
|
||||
err = multierr.Append(err, fmt.Errorf("invalid trusted_proxies entry %q: must be an IP or CIDR", proxy))
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user