linting #475

Merged
tech merged 1 commits from dis-474 into main 2026-02-12 11:48:37 +00:00
8 changed files with 271 additions and 24 deletions

195
api/discovery/.golangci.yml Normal file
View File

@@ -0,0 +1,195 @@
# See the dedicated "version" documentation section.
version: "2"
linters:
# Default set of linters.
# The value can be:
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
# - `all`: enables all linters by default.
# - `none`: disables all linters by default.
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
# Default: standard
default: all
# Enable specific linter.
enable:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- embeddedstructfieldcheck
- err113
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wrapcheck
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gomoddirectives
- wsl
# All available settings of specific linters.
# See the dedicated "linters.settings" documentation section.
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: false
branch-max-lines: 2
# Defines a set of rules to ignore issues.
# It does not skip the analysis, and so does not ignore "typecheck" errors.
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: strict
generated: lax
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
# Predefined exclusion rules.
# Default: []
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
# Excluding configuration per-path, per-linter, per-text and per-source.
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gocyclo
- errcheck
- dupl
- gosec
# Run some linter only for test files by excluding its issues for everything else.
- path-except: _test\.go
linters:
- forbidigo
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments.
# `/` will be replaced by the current OS file path separator to properly work on Windows.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some `staticcheck` messages.
- linters:
- staticcheck
text: "SA9003:"
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
# "/" will be replaced by the current OS file path separator to properly work on Windows.
# Default: []
paths: []
# Which file paths to not exclude.
# Default: []
paths-except: []

View File

@@ -14,8 +14,8 @@ var (
BuildDate string BuildDate string
) )
func Create() version.Printer { func Create() version.Printer { //nolint:ireturn // factory returns interface by design
vi := version.Info{ info := version.Info{
Program: "Sendico Discovery Service", Program: "Sendico Discovery Service",
Revision: Revision, Revision: Revision,
Branch: Branch, Branch: Branch,
@@ -23,5 +23,6 @@ func Create() version.Printer {
BuildDate: BuildDate, BuildDate: BuildDate,
Version: Version, Version: Version,
} }
return vf.Create(&vi)
return vf.Create(&info)
} }

View File

@@ -10,7 +10,10 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
const defaultMetricsAddress = ":9405" const (
defaultMetricsAddress = ":9405"
defaultShutdownTimeoutSeconds = 15
)
type config struct { type config struct {
Runtime *grpcapp.RuntimeConfig `yaml:"runtime"` Runtime *grpcapp.RuntimeConfig `yaml:"runtime"`
@@ -24,24 +27,28 @@ type metricsConfig struct {
} }
type registryConfig struct { type registryConfig struct {
KVTTLSeconds *int `yaml:"kv_ttl_seconds"` KVTTLSeconds *int `yaml:"kv_ttl_seconds"` //nolint:tagliatelle // matches config file format
} }
func (i *Imp) loadConfig() (*config, error) { func (i *Imp) loadConfig() (*config, error) {
data, err := os.ReadFile(i.file) data, err := os.ReadFile(i.file)
if err != nil { if err != nil {
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
return nil, err
return nil, err //nolint:wrapcheck
} }
cfg := &config{} cfg := &config{}
if err := yaml.Unmarshal(data, cfg); err != nil {
err = yaml.Unmarshal(data, cfg)
if err != nil {
i.logger.Error("Failed to parse configuration", zap.Error(err)) i.logger.Error("Failed to parse configuration", zap.Error(err))
return nil, err
return nil, err //nolint:wrapcheck
} }
if cfg.Runtime == nil { if cfg.Runtime == nil {
cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15} cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: defaultShutdownTimeoutSeconds}
} }
if cfg.Metrics != nil && strings.TrimSpace(cfg.Metrics.Address) == "" { if cfg.Metrics != nil && strings.TrimSpace(cfg.Metrics.Address) == "" {

View File

@@ -14,30 +14,37 @@ import (
func (i *Imp) startDiscovery(cfg *config) error { func (i *Imp) startDiscovery(cfg *config) error {
if cfg == nil || cfg.Messaging == nil || cfg.Messaging.Driver == "" { if cfg == nil || cfg.Messaging == nil || cfg.Messaging.Driver == "" {
//nolint:wrapcheck
return merrors.InvalidArgument("discovery service: messaging configuration is required", "messaging") return merrors.InvalidArgument("discovery service: messaging configuration is required", "messaging")
} }
broker, err := msg.CreateMessagingBroker(i.logger.Named("discovery_bus"), cfg.Messaging) broker, err := msg.CreateMessagingBroker(i.logger.Named("discovery_bus"), cfg.Messaging)
if err != nil { if err != nil {
return err return err //nolint:wrapcheck
} }
i.logger.Info("Discovery messaging broker ready", zap.String("messaging_driver", string(cfg.Messaging.Driver))) i.logger.Info("Discovery messaging broker ready", zap.String("messaging_driver", string(cfg.Messaging.Driver)))
producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker) producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker)
registry := discovery.NewRegistry() registry := discovery.NewRegistry()
var registryOpts []discovery.RegistryOption var registryOpts []discovery.RegistryOption
if cfg.Registry != nil && cfg.Registry.KVTTLSeconds != nil { if cfg.Registry != nil && cfg.Registry.KVTTLSeconds != nil {
ttlSeconds := *cfg.Registry.KVTTLSeconds ttlSeconds := *cfg.Registry.KVTTLSeconds
if ttlSeconds < 0 { if ttlSeconds < 0 {
i.logger.Warn("Discovery registry TTL is negative, disabling TTL", zap.Int("ttl_seconds", ttlSeconds)) i.logger.Warn("Discovery registry TTL is negative, disabling TTL", zap.Int("ttl_seconds", ttlSeconds))
ttlSeconds = 0 ttlSeconds = 0
} }
registryOpts = append(registryOpts, discovery.WithRegistryKVTTL(time.Duration(ttlSeconds)*time.Second)) registryOpts = append(registryOpts, discovery.WithRegistryKVTTL(time.Duration(ttlSeconds)*time.Second))
} }
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, string(mservice.Discovery), registryOpts...)
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, mservice.Discovery, registryOpts...)
if err != nil { if err != nil {
return err return err //nolint:wrapcheck
} }
svc.Start() svc.Start()
i.registrySvc = svc i.registrySvc = svc
@@ -47,10 +54,11 @@ func (i *Imp) startDiscovery(cfg *config) error {
Operations: []string{"discovery.lookup"}, Operations: []string{"discovery.lookup"},
Version: appversion.Create().Short(), Version: appversion.Create().Short(),
} }
i.announcer = discovery.NewAnnouncer(i.logger, producer, string(mservice.Discovery), announce) i.announcer = discovery.NewAnnouncer(i.logger, producer, mservice.Discovery, announce)
i.announcer.Start() i.announcer.Start()
i.logger.Info("Discovery registry service started", zap.String("messaging_driver", string(cfg.Messaging.Driver))) i.logger.Info("Discovery registry service started", zap.String("messaging_driver", string(cfg.Messaging.Driver)))
return nil return nil
} }
@@ -58,10 +66,12 @@ func (i *Imp) stopDiscovery() {
if i == nil { if i == nil {
return return
} }
if i.announcer != nil { if i.announcer != nil {
i.announcer.Stop() i.announcer.Stop()
i.announcer = nil i.announcer = nil
} }
if i.registrySvc != nil { if i.registrySvc != nil {
i.registrySvc.Stop() i.registrySvc.Stop()
i.registrySvc = nil i.registrySvc = nil

View File

@@ -15,22 +15,30 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
const readHeaderTimeout = 5 * time.Second
func (i *Imp) startMetrics(cfg *metricsConfig) { func (i *Imp) startMetrics(cfg *metricsConfig) {
if i == nil { if i == nil {
return return
} }
address := "" address := ""
if cfg != nil { if cfg != nil {
address = strings.TrimSpace(cfg.Address) address = strings.TrimSpace(cfg.Address)
} }
if address == "" { if address == "" {
i.logger.Info("Metrics endpoint disabled") i.logger.Info("Metrics endpoint disabled")
return return
} }
listener, err := net.Listen("tcp", address) lc := net.ListenConfig{}
listener, err := lc.Listen(context.Background(), "tcp", address)
if err != nil { if err != nil {
i.logger.Error("Failed to bind metrics listener", zap.String("address", address), zap.Error(err)) i.logger.Error("Failed to bind metrics listener", zap.String("address", address), zap.Error(err))
return return
} }
@@ -38,7 +46,9 @@ func (i *Imp) startMetrics(cfg *metricsConfig) {
router.Handle("/metrics", promhttp.Handler()) router.Handle("/metrics", promhttp.Handler())
var healthRouter routers.Health var healthRouter routers.Health
if hr, err := routers.NewHealthRouter(i.logger.Named("metrics"), router, ""); err != nil {
hr, err := routers.NewHealthRouter(i.logger.Named("metrics"), router, "")
if err != nil {
i.logger.Warn("Failed to initialise health router", zap.Error(err)) i.logger.Warn("Failed to initialise health router", zap.Error(err))
} else { } else {
hr.SetStatus(health.SSStarting) hr.SetStatus(health.SSStarting)
@@ -49,13 +59,16 @@ func (i *Imp) startMetrics(cfg *metricsConfig) {
i.metricsSrv = &http.Server{ i.metricsSrv = &http.Server{
Addr: address, Addr: address,
Handler: router, Handler: router,
ReadHeaderTimeout: 5 * time.Second, ReadHeaderTimeout: readHeaderTimeout,
} }
go func() { go func() {
i.logger.Info("Prometheus endpoint listening", zap.String("address", address)) i.logger.Info("Prometheus endpoint listening", zap.String("address", address))
if err := i.metricsSrv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
i.logger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(err)) serveErr := i.metricsSrv.Serve(listener)
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
i.logger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(serveErr))
if healthRouter != nil { if healthRouter != nil {
healthRouter.SetStatus(health.SSTerminating) healthRouter.SetStatus(health.SSTerminating)
} }
@@ -69,14 +82,18 @@ func (i *Imp) shutdownMetrics(ctx context.Context) {
i.metricsHealth.Finish() i.metricsHealth.Finish()
i.metricsHealth = nil i.metricsHealth = nil
} }
if i.metricsSrv == nil { if i.metricsSrv == nil {
return return
} }
if err := i.metricsSrv.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
err := i.metricsSrv.Shutdown(ctx)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
i.logger.Warn("Failed to stop metrics server", zap.Error(err)) i.logger.Warn("Failed to stop metrics server", zap.Error(err))
} else { } else {
i.logger.Info("Metrics server stopped") i.logger.Info("Metrics server stopped")
} }
i.metricsSrv = nil i.metricsSrv = nil
} }
@@ -84,5 +101,6 @@ func (i *Imp) setMetricsStatus(status health.ServiceStatus) {
if i == nil || i.metricsHealth == nil { if i == nil || i.metricsHealth == nil {
return return
} }
i.metricsHealth.SetStatus(status) i.metricsHealth.SetStatus(status)
} }

View File

@@ -10,6 +10,8 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
const defaultShutdownTimeout = 15 * time.Second
func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
return &Imp{ return &Imp{
logger: logger.Named("server"), logger: logger.Named("server"),
@@ -28,29 +30,37 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.config = cfg i.config = cfg
messagingDriver := "none" messagingDriver := "none"
if cfg.Messaging != nil { if cfg.Messaging != nil {
messagingDriver = string(cfg.Messaging.Driver) messagingDriver = string(cfg.Messaging.Driver)
} }
metricsAddress := "" metricsAddress := ""
if cfg.Metrics != nil { if cfg.Metrics != nil {
metricsAddress = strings.TrimSpace(cfg.Metrics.Address) metricsAddress = strings.TrimSpace(cfg.Metrics.Address)
} }
if metricsAddress == "" { if metricsAddress == "" {
metricsAddress = "disabled" metricsAddress = "disabled"
} }
i.logger.Info("Discovery config loaded", zap.String("messaging_driver", messagingDriver), zap.String("metrics_address", metricsAddress))
i.logger.Info("Discovery config loaded",
zap.String("messaging_driver", messagingDriver),
zap.String("metrics_address", metricsAddress))
i.startMetrics(cfg.Metrics) i.startMetrics(cfg.Metrics)
if err := i.startDiscovery(cfg); err != nil { err = i.startDiscovery(cfg)
if err != nil {
i.stopDiscovery() i.stopDiscovery()
i.setMetricsStatus(health.SSTerminating) i.setMetricsStatus(health.SSTerminating)
ctx, cancel := context.WithTimeout(context.Background(), i.shutdownTimeout()) ctx, cancel := context.WithTimeout(context.Background(), i.shutdownTimeout())
i.shutdownMetrics(ctx) i.shutdownMetrics(ctx)
cancel() cancel()
return err return err
} }
@@ -59,6 +69,7 @@ func (i *Imp) Start() error {
<-i.stopCh <-i.stopCh
i.logger.Info("Discovery service stop signal received") i.logger.Info("Discovery service stop signal received")
return nil return nil
} }
@@ -72,6 +83,7 @@ func (i *Imp) Shutdown() {
if i.doneCh != nil { if i.doneCh != nil {
<-i.doneCh <-i.doneCh
} }
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
i.shutdownMetrics(ctx) i.shutdownMetrics(ctx)
cancel() cancel()
@@ -83,6 +95,7 @@ func (i *Imp) initStopChannels() {
if i.stopCh == nil { if i.stopCh == nil {
i.stopCh = make(chan struct{}) i.stopCh = make(chan struct{})
} }
if i.doneCh == nil { if i.doneCh == nil {
i.doneCh = make(chan struct{}) i.doneCh = make(chan struct{})
} }
@@ -108,5 +121,6 @@ func (i *Imp) shutdownTimeout() time.Duration {
if i.config != nil && i.config.Runtime != nil { if i.config != nil && i.config.Runtime != nil {
return i.config.Runtime.ShutdownTimeout() return i.config.Runtime.ShutdownTimeout()
} }
return 15 * time.Second
return defaultShutdownTimeout
} }

View File

@@ -6,6 +6,7 @@ import (
"github.com/tech/sendico/pkg/server" "github.com/tech/sendico/pkg/server"
) )
//nolint:ireturn // factory returns interface by design
func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
return serverimp.Create(logger, file, debug) return serverimp.Create(logger, file, debug) //nolint:wrapcheck
} }

View File

@@ -8,8 +8,9 @@ import (
smain "github.com/tech/sendico/pkg/server/main" smain "github.com/tech/sendico/pkg/server/main"
) )
//nolint:ireturn // factory returns interface by design
func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) { func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
return si.Create(logger, file, debug) return si.Create(logger, file, debug) //nolint:wrapcheck
} }
func main() { func main() {