AGENTS.md: no direct commits to main, all changes via feature branches
This commit is contained in:
@@ -23,8 +23,8 @@ No commit lands on main with lint errors, test failures, or formatting issues.
|
|||||||
|
|
||||||
## Git Workflow
|
## Git Workflow
|
||||||
|
|
||||||
- Never commit directly to main — always feature branches + PRs
|
- **Never commit directly to main** — all changes go on feature branches
|
||||||
- Exception: initial scaffolding and AGENTS.md/CONVENTIONS.md updates
|
- Merge to main only when ready and passing all checks
|
||||||
- PR titles: `Description (closes #N)` when closing an issue
|
- PR titles: `Description (closes #N)` when closing an issue
|
||||||
- One logical change per commit
|
- One logical change per commit
|
||||||
- Commit messages: imperative mood, concise
|
- Commit messages: imperative mood, concise
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
// Package config provides application configuration via environment and files.
|
||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/chat/internal/globals"
|
"git.eeqj.de/sneak/chat/internal/globals"
|
||||||
@@ -9,15 +9,18 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload" // loads .env file
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ConfigParams defines the dependencies for creating a Config.
|
||||||
type ConfigParams struct {
|
type ConfigParams struct {
|
||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
Globals *globals.Globals
|
Globals *globals.Globals
|
||||||
Logger *logger.Logger
|
Logger *logger.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Config holds all application configuration values.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
DBURL string
|
DBURL string
|
||||||
Debug bool
|
Debug bool
|
||||||
@@ -36,14 +39,15 @@ type Config struct {
|
|||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
// New creates a new Config by reading from files and environment variables.
|
||||||
|
func New(_ fx.Lifecycle, params ConfigParams) (*Config, error) {
|
||||||
log := params.Logger.Get()
|
log := params.Logger.Get()
|
||||||
name := params.Globals.Appname
|
name := params.Globals.Appname
|
||||||
|
|
||||||
viper.SetConfigName(name)
|
viper.SetConfigName(name)
|
||||||
viper.SetConfigType("yaml")
|
viper.SetConfigType("yaml")
|
||||||
viper.AddConfigPath(fmt.Sprintf("/etc/%s", name))
|
viper.AddConfigPath("/etc/" + name)
|
||||||
viper.AddConfigPath(fmt.Sprintf("$HOME/.config/%s", name))
|
viper.AddConfigPath("$HOME/.config/" + name)
|
||||||
viper.AutomaticEnv()
|
viper.AutomaticEnv()
|
||||||
|
|
||||||
viper.SetDefault("DEBUG", "false")
|
viper.SetDefault("DEBUG", "false")
|
||||||
@@ -60,10 +64,9 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
|||||||
viper.SetDefault("SERVER_NAME", "")
|
viper.SetDefault("SERVER_NAME", "")
|
||||||
viper.SetDefault("FEDERATION_KEY", "")
|
viper.SetDefault("FEDERATION_KEY", "")
|
||||||
|
|
||||||
if err := viper.ReadInConfig(); err != nil {
|
err := viper.ReadInConfig()
|
||||||
if _, ok := err.(viper.ConfigFileNotFoundError); ok {
|
if err != nil {
|
||||||
// Config file not found is OK
|
if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
|
||||||
} else {
|
|
||||||
log.Error("config file malformed", "error", err)
|
log.Error("config file malformed", "error", err)
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package db provides database access and migration management.
|
||||||
package db
|
package db
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -5,38 +6,47 @@ import (
|
|||||||
"database/sql"
|
"database/sql"
|
||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/chat/internal/config"
|
"git.eeqj.de/sneak/chat/internal/config"
|
||||||
"git.eeqj.de/sneak/chat/internal/logger"
|
"git.eeqj.de/sneak/chat/internal/logger"
|
||||||
"git.eeqj.de/sneak/chat/internal/models"
|
"git.eeqj.de/sneak/chat/internal/models"
|
||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload" // loads .env file
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite" // SQLite driver
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed schema/*.sql
|
const (
|
||||||
var SchemaFiles embed.FS
|
minMigrationParts = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
// SchemaFiles contains embedded SQL migration files.
|
||||||
|
//
|
||||||
|
//go:embed schema/*.sql
|
||||||
|
var SchemaFiles embed.FS //nolint:gochecknoglobals
|
||||||
|
|
||||||
|
// DatabaseParams defines the dependencies for creating a Database.
|
||||||
type DatabaseParams struct {
|
type DatabaseParams struct {
|
||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
Logger *logger.Logger
|
Logger *logger.Logger
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Database manages the SQLite database connection and migrations.
|
||||||
type Database struct {
|
type Database struct {
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
params *DatabaseParams
|
params *DatabaseParams
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDB implements models.db so Database can be embedded in model structs.
|
// GetDB returns the underlying sql.DB connection.
|
||||||
func (s *Database) GetDB() *sql.DB {
|
func (s *Database) GetDB() *sql.DB {
|
||||||
return s.db
|
return s.db
|
||||||
}
|
}
|
||||||
@@ -52,9 +62,11 @@ func (s *Database) NewChannel(id int64, name, topic, modes string, createdAt, up
|
|||||||
UpdatedAt: updatedAt,
|
UpdatedAt: updatedAt,
|
||||||
}
|
}
|
||||||
c.SetDB(s)
|
c.SetDB(s)
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new Database instance and registers lifecycle hooks.
|
||||||
func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
|
func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
|
||||||
s := new(Database)
|
s := new(Database)
|
||||||
s.params = ¶ms
|
s.params = ¶ms
|
||||||
@@ -65,16 +77,20 @@ func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
|
|||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
OnStart: func(ctx context.Context) error {
|
OnStart: func(ctx context.Context) error {
|
||||||
s.log.Info("Database OnStart Hook")
|
s.log.Info("Database OnStart Hook")
|
||||||
|
|
||||||
return s.connect(ctx)
|
return s.connect(ctx)
|
||||||
},
|
},
|
||||||
OnStop: func(ctx context.Context) error {
|
OnStop: func(ctx context.Context) error {
|
||||||
s.log.Info("Database OnStop Hook")
|
s.log.Info("Database OnStop Hook")
|
||||||
|
|
||||||
if s.db != nil {
|
if s.db != nil {
|
||||||
return s.db.Close()
|
return s.db.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,11 +105,14 @@ func (s *Database) connect(ctx context.Context) error {
|
|||||||
d, err := sql.Open("sqlite", dbURL)
|
d, err := sql.Open("sqlite", dbURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.log.Error("failed to open database", "error", err)
|
s.log.Error("failed to open database", "error", err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := d.PingContext(ctx); err != nil {
|
err = d.PingContext(ctx)
|
||||||
|
if err != nil {
|
||||||
s.log.Error("failed to ping database", "error", err)
|
s.log.Error("failed to ping database", "error", err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +129,27 @@ type migration struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Database) runMigrations(ctx context.Context) error {
|
func (s *Database) runMigrations(ctx context.Context) error {
|
||||||
// Bootstrap: create schema_migrations table directly (migration 001 also does this,
|
err := s.bootstrapMigrationsTable(ctx)
|
||||||
// but we need it to exist before we can check which migrations have run)
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations, err := s.loadMigrations()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.applyMigrations(ctx, migrations)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.log.Info("database migrations complete")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) bootstrapMigrationsTable(ctx context.Context) error {
|
||||||
_, err := s.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
|
_, err := s.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_migrations (
|
||||||
version INTEGER PRIMARY KEY,
|
version INTEGER PRIMARY KEY,
|
||||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
@@ -120,23 +158,27 @@ func (s *Database) runMigrations(ctx context.Context) error {
|
|||||||
return fmt.Errorf("failed to create schema_migrations table: %w", err)
|
return fmt.Errorf("failed to create schema_migrations table: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read all migration files
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) loadMigrations() ([]migration, error) {
|
||||||
entries, err := fs.ReadDir(SchemaFiles, "schema")
|
entries, err := fs.ReadDir(SchemaFiles, "schema")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read schema dir: %w", err)
|
return nil, fmt.Errorf("failed to read schema dir: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var migrations []migration
|
var migrations []migration
|
||||||
|
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
|
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".sql") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse version from filename (e.g. "001_initial.sql" -> 1)
|
parts := strings.SplitN(entry.Name(), "_", minMigrationParts)
|
||||||
parts := strings.SplitN(entry.Name(), "_", 2)
|
if len(parts) < minMigrationParts {
|
||||||
if len(parts) < 2 {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
version, err := strconv.Atoi(parts[0])
|
version, err := strconv.Atoi(parts[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
continue
|
continue
|
||||||
@@ -144,7 +186,7 @@ func (s *Database) runMigrations(ctx context.Context) error {
|
|||||||
|
|
||||||
content, err := SchemaFiles.ReadFile("schema/" + entry.Name())
|
content, err := SchemaFiles.ReadFile("schema/" + entry.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read migration %s: %w", entry.Name(), err)
|
return nil, fmt.Errorf("failed to read migration %s: %w", entry.Name(), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
migrations = append(migrations, migration{
|
migrations = append(migrations, migration{
|
||||||
@@ -158,26 +200,34 @@ func (s *Database) runMigrations(ctx context.Context) error {
|
|||||||
return migrations[i].version < migrations[j].version
|
return migrations[i].version < migrations[j].version
|
||||||
})
|
})
|
||||||
|
|
||||||
|
return migrations, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Database) applyMigrations(ctx context.Context, migrations []migration) error {
|
||||||
for _, m := range migrations {
|
for _, m := range migrations {
|
||||||
var exists int
|
var exists int
|
||||||
|
|
||||||
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM schema_migrations WHERE version = ?", m.version).Scan(&exists)
|
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM schema_migrations WHERE version = ?", m.version).Scan(&exists)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to check migration %d: %w", m.version, err)
|
return fmt.Errorf("failed to check migration %d: %w", m.version, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if exists > 0 {
|
if exists > 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.Info("applying migration", "version", m.version, "name", m.name)
|
s.log.Info("applying migration", "version", m.version, "name", m.name)
|
||||||
if _, err := s.db.ExecContext(ctx, m.sql); err != nil {
|
|
||||||
|
_, err = s.db.ExecContext(ctx, m.sql)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("failed to apply migration %d (%s): %w", m.version, m.name, err)
|
return fmt.Errorf("failed to apply migration %d (%s): %w", m.version, m.name, err)
|
||||||
}
|
}
|
||||||
if _, err := s.db.ExecContext(ctx, "INSERT INTO schema_migrations (version) VALUES (?)", m.version); err != nil {
|
|
||||||
|
_, err = s.db.ExecContext(ctx, "INSERT INTO schema_migrations (version) VALUES (?)", m.version)
|
||||||
|
if err != nil {
|
||||||
return fmt.Errorf("failed to record migration %d: %w", m.version, err)
|
return fmt.Errorf("failed to record migration %d: %w", m.version, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.Info("database migrations complete")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package globals provides shared global state for the application.
|
||||||
package globals
|
package globals
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -5,19 +6,25 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
Appname string
|
// Appname is the global application name.
|
||||||
Version string
|
Appname string //nolint:gochecknoglobals
|
||||||
|
|
||||||
|
// Version is the global application version.
|
||||||
|
Version string //nolint:gochecknoglobals
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Globals holds application-wide metadata.
|
||||||
type Globals struct {
|
type Globals struct {
|
||||||
Appname string
|
Appname string
|
||||||
Version string
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(lc fx.Lifecycle) (*Globals, error) {
|
// New creates a new Globals instance from the global state.
|
||||||
|
func New(_ fx.Lifecycle) (*Globals, error) {
|
||||||
n := &Globals{
|
n := &Globals{
|
||||||
Appname: Appname,
|
Appname: Appname,
|
||||||
Version: Version,
|
Version: Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package handlers provides HTTP request handlers for the chat server.
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -13,36 +14,43 @@ import (
|
|||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HandlersParams defines the dependencies for creating Handlers.
|
||||||
type HandlersParams struct {
|
type HandlersParams struct {
|
||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
Logger *logger.Logger
|
Logger *logger.Logger
|
||||||
Globals *globals.Globals
|
Globals *globals.Globals
|
||||||
Database *db.Database
|
Database *db.Database
|
||||||
Healthcheck *healthcheck.Healthcheck
|
Healthcheck *healthcheck.Healthcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handlers manages HTTP request handling.
|
||||||
type Handlers struct {
|
type Handlers struct {
|
||||||
params *HandlersParams
|
params *HandlersParams
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
hc *healthcheck.Healthcheck
|
hc *healthcheck.Healthcheck
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new Handlers instance.
|
||||||
func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
|
func New(lc fx.Lifecycle, params HandlersParams) (*Handlers, error) {
|
||||||
s := new(Handlers)
|
s := new(Handlers)
|
||||||
s.params = ¶ms
|
s.params = ¶ms
|
||||||
s.log = params.Logger.Get()
|
s.log = params.Logger.Get()
|
||||||
s.hc = params.Healthcheck
|
s.hc = params.Healthcheck
|
||||||
|
|
||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
OnStart: func(ctx context.Context) error {
|
OnStart: func(_ context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Handlers) respondJSON(w http.ResponseWriter, r *http.Request, data interface{}, status int) {
|
func (s *Handlers) respondJSON(w http.ResponseWriter, _ *http.Request, data interface{}, status int) {
|
||||||
w.WriteHeader(status)
|
w.WriteHeader(status)
|
||||||
w.Header().Set("Content-Type", "application/json")
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
|
||||||
if data != nil {
|
if data != nil {
|
||||||
err := json.NewEncoder(w).Encode(data)
|
err := json.NewEncoder(w).Encode(data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -51,6 +59,6 @@ func (s *Handlers) respondJSON(w http.ResponseWriter, r *http.Request, data inte
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Handlers) decodeJSON(w http.ResponseWriter, r *http.Request, v interface{}) error {
|
func (s *Handlers) decodeJSON(_ http.ResponseWriter, r *http.Request, v interface{}) error {
|
||||||
return json.NewDecoder(r.Body).Decode(v)
|
return json.NewDecoder(r.Body).Decode(v)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const httpStatusOK = 200
|
||||||
|
|
||||||
|
// HandleHealthCheck returns an HTTP handler for the health check endpoint.
|
||||||
func (s *Handlers) HandleHealthCheck() http.HandlerFunc {
|
func (s *Handlers) HandleHealthCheck() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, req *http.Request) {
|
return func(w http.ResponseWriter, req *http.Request) {
|
||||||
resp := s.hc.Healthcheck()
|
resp := s.hc.Healthcheck()
|
||||||
s.respondJSON(w, req, resp, 200)
|
s.respondJSON(w, req, resp, httpStatusOK)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package healthcheck provides health status reporting for the server.
|
||||||
package healthcheck
|
package healthcheck
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,51 +13,57 @@ import (
|
|||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// HealthcheckParams defines the dependencies for creating a Healthcheck.
|
||||||
type HealthcheckParams struct {
|
type HealthcheckParams struct {
|
||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
Globals *globals.Globals
|
Globals *globals.Globals
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
Logger *logger.Logger
|
Logger *logger.Logger
|
||||||
Database *db.Database
|
Database *db.Database
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Healthcheck tracks server uptime and provides health status.
|
||||||
type Healthcheck struct {
|
type Healthcheck struct {
|
||||||
|
// StartupTime records when the server started.
|
||||||
StartupTime time.Time
|
StartupTime time.Time
|
||||||
|
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
params *HealthcheckParams
|
params *HealthcheckParams
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new Healthcheck instance.
|
||||||
func New(lc fx.Lifecycle, params HealthcheckParams) (*Healthcheck, error) {
|
func New(lc fx.Lifecycle, params HealthcheckParams) (*Healthcheck, error) {
|
||||||
s := new(Healthcheck)
|
s := new(Healthcheck)
|
||||||
s.params = ¶ms
|
s.params = ¶ms
|
||||||
s.log = params.Logger.Get()
|
s.log = params.Logger.Get()
|
||||||
|
|
||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
OnStart: func(ctx context.Context) error {
|
OnStart: func(_ context.Context) error {
|
||||||
s.StartupTime = time.Now()
|
s.StartupTime = time.Now()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
OnStop: func(ctx context.Context) error {
|
OnStop: func(_ context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HealthcheckResponse is the JSON response returned by the health endpoint.
|
||||||
type HealthcheckResponse struct {
|
type HealthcheckResponse struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Now string `json:"now"`
|
Now string `json:"now"`
|
||||||
UptimeSeconds int64 `json:"uptime_seconds"`
|
UptimeSeconds int64 `json:"uptimeSeconds"`
|
||||||
UptimeHuman string `json:"uptime_human"`
|
UptimeHuman string `json:"uptimeHuman"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Appname string `json:"appname"`
|
Appname string `json:"appname"`
|
||||||
Maintenance bool `json:"maintenance_mode"`
|
Maintenance bool `json:"maintenanceMode"`
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Healthcheck) uptime() time.Duration {
|
|
||||||
return time.Since(s.StartupTime)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Healthcheck returns the current health status of the server.
|
||||||
func (s *Healthcheck) Healthcheck() *HealthcheckResponse {
|
func (s *Healthcheck) Healthcheck() *HealthcheckResponse {
|
||||||
resp := &HealthcheckResponse{
|
resp := &HealthcheckResponse{
|
||||||
Status: "ok",
|
Status: "ok",
|
||||||
@@ -66,5 +73,10 @@ func (s *Healthcheck) Healthcheck() *HealthcheckResponse {
|
|||||||
Appname: s.params.Globals.Appname,
|
Appname: s.params.Globals.Appname,
|
||||||
Version: s.params.Globals.Version,
|
Version: s.params.Globals.Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp
|
return resp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Healthcheck) uptime() time.Duration {
|
||||||
|
return time.Since(s.StartupTime)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package logger provides structured logging for the application.
|
||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -8,18 +9,22 @@ import (
|
|||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// LoggerParams defines the dependencies for creating a Logger.
|
||||||
type LoggerParams struct {
|
type LoggerParams struct {
|
||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
Globals *globals.Globals
|
Globals *globals.Globals
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logger wraps slog with application-specific configuration.
|
||||||
type Logger struct {
|
type Logger struct {
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
level *slog.LevelVar
|
level *slog.LevelVar
|
||||||
params LoggerParams
|
params LoggerParams
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(lc fx.Lifecycle, params LoggerParams) (*Logger, error) {
|
// New creates a new Logger with appropriate handler based on terminal detection.
|
||||||
|
func New(_ fx.Lifecycle, params LoggerParams) (*Logger, error) {
|
||||||
l := new(Logger)
|
l := new(Logger)
|
||||||
l.level = new(slog.LevelVar)
|
l.level = new(slog.LevelVar)
|
||||||
l.level.Set(slog.LevelInfo)
|
l.level.Set(slog.LevelInfo)
|
||||||
@@ -48,15 +53,18 @@ func New(lc fx.Lifecycle, params LoggerParams) (*Logger, error) {
|
|||||||
return l, nil
|
return l, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// EnableDebugLogging switches the log level to debug.
|
||||||
func (l *Logger) EnableDebugLogging() {
|
func (l *Logger) EnableDebugLogging() {
|
||||||
l.level.Set(slog.LevelDebug)
|
l.level.Set(slog.LevelDebug)
|
||||||
l.log.Debug("debug logging enabled", "debug", true)
|
l.log.Debug("debug logging enabled", "debug", true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get returns the underlying slog.Logger.
|
||||||
func (l *Logger) Get() *slog.Logger {
|
func (l *Logger) Get() *slog.Logger {
|
||||||
return l.log
|
return l.log
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Identify logs the application name and version at startup.
|
||||||
func (l *Logger) Identify() {
|
func (l *Logger) Identify() {
|
||||||
l.log.Info("starting",
|
l.log.Info("starting",
|
||||||
"appname", l.params.Globals.Appname,
|
"appname", l.params.Globals.Appname,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package middleware provides HTTP middleware for the chat server.
|
||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -19,22 +20,29 @@ import (
|
|||||||
"go.uber.org/fx"
|
"go.uber.org/fx"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const corsMaxAge = 300
|
||||||
|
|
||||||
|
// MiddlewareParams defines the dependencies for creating Middleware.
|
||||||
type MiddlewareParams struct {
|
type MiddlewareParams struct {
|
||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
Logger *logger.Logger
|
Logger *logger.Logger
|
||||||
Globals *globals.Globals
|
Globals *globals.Globals
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Middleware provides HTTP middleware handlers.
|
||||||
type Middleware struct {
|
type Middleware struct {
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
params *MiddlewareParams
|
params *MiddlewareParams
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(lc fx.Lifecycle, params MiddlewareParams) (*Middleware, error) {
|
// New creates a new Middleware instance.
|
||||||
|
func New(_ fx.Lifecycle, params MiddlewareParams) (*Middleware, error) {
|
||||||
s := new(Middleware)
|
s := new(Middleware)
|
||||||
s.params = ¶ms
|
s.params = ¶ms
|
||||||
s.log = params.Logger.Get()
|
s.log = params.Logger.Get()
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,17 +51,21 @@ func ipFromHostPort(hp string) string {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(h) > 0 && h[0] == '[' {
|
if len(h) > 0 && h[0] == '[' {
|
||||||
return h[1 : len(h)-1]
|
return h[1 : len(h)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
type loggingResponseWriter struct {
|
type loggingResponseWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
|
|
||||||
statusCode int
|
statusCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewLoggingResponseWriter wraps a ResponseWriter to capture the status code.
|
||||||
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
|
func NewLoggingResponseWriter(w http.ResponseWriter) *loggingResponseWriter {
|
||||||
return &loggingResponseWriter{w, http.StatusOK}
|
return &loggingResponseWriter{w, http.StatusOK}
|
||||||
}
|
}
|
||||||
@@ -63,20 +75,25 @@ func (lrw *loggingResponseWriter) WriteHeader(code int) {
|
|||||||
lrw.ResponseWriter.WriteHeader(code)
|
lrw.ResponseWriter.WriteHeader(code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Logging returns middleware that logs each HTTP request.
|
||||||
func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
lrw := NewLoggingResponseWriter(w)
|
lrw := NewLoggingResponseWriter(w)
|
||||||
ctx := r.Context()
|
ctx := r.Context()
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
latency := time.Since(start)
|
latency := time.Since(start)
|
||||||
|
|
||||||
|
reqID, _ := ctx.Value(middleware.RequestIDKey).(string)
|
||||||
|
|
||||||
s.log.InfoContext(ctx, "request",
|
s.log.InfoContext(ctx, "request",
|
||||||
"request_start", start,
|
"request_start", start,
|
||||||
"method", r.Method,
|
"method", r.Method,
|
||||||
"url", r.URL.String(),
|
"url", r.URL.String(),
|
||||||
"useragent", r.UserAgent(),
|
"useragent", r.UserAgent(),
|
||||||
"request_id", ctx.Value(middleware.RequestIDKey).(string),
|
"request_id", reqID,
|
||||||
"referer", r.Referer(),
|
"referer", r.Referer(),
|
||||||
"proto", r.Proto,
|
"proto", r.Proto,
|
||||||
"remoteIP", ipFromHostPort(r.RemoteAddr),
|
"remoteIP", ipFromHostPort(r.RemoteAddr),
|
||||||
@@ -90,6 +107,7 @@ func (s *Middleware) Logging() func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CORS returns middleware that handles Cross-Origin Resource Sharing.
|
||||||
func (s *Middleware) CORS() func(http.Handler) http.Handler {
|
func (s *Middleware) CORS() func(http.Handler) http.Handler {
|
||||||
return cors.Handler(cors.Options{
|
return cors.Handler(cors.Options{
|
||||||
AllowedOrigins: []string{"*"},
|
AllowedOrigins: []string{"*"},
|
||||||
@@ -97,10 +115,11 @@ func (s *Middleware) CORS() func(http.Handler) http.Handler {
|
|||||||
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||||
ExposedHeaders: []string{"Link"},
|
ExposedHeaders: []string{"Link"},
|
||||||
AllowCredentials: false,
|
AllowCredentials: false,
|
||||||
MaxAge: 300,
|
MaxAge: corsMaxAge,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auth returns middleware that performs authentication.
|
||||||
func (s *Middleware) Auth() func(http.Handler) http.Handler {
|
func (s *Middleware) Auth() func(http.Handler) http.Handler {
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -110,15 +129,18 @@ func (s *Middleware) Auth() func(http.Handler) http.Handler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metrics returns middleware that records HTTP metrics.
|
||||||
func (s *Middleware) Metrics() func(http.Handler) http.Handler {
|
func (s *Middleware) Metrics() func(http.Handler) http.Handler {
|
||||||
mdlw := ghmm.New(ghmm.Config{
|
mdlw := ghmm.New(ghmm.Config{
|
||||||
Recorder: metrics.NewRecorder(metrics.Config{}),
|
Recorder: metrics.NewRecorder(metrics.Config{}),
|
||||||
})
|
})
|
||||||
|
|
||||||
return func(next http.Handler) http.Handler {
|
return func(next http.Handler) http.Handler {
|
||||||
return std.Handler("", mdlw, next)
|
return std.Handler("", mdlw, next)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MetricsAuth returns middleware that protects metrics with basic auth.
|
||||||
func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler {
|
func (s *Middleware) MetricsAuth() func(http.Handler) http.Handler {
|
||||||
return basicauth.New(
|
return basicauth.New(
|
||||||
"metrics",
|
"metrics",
|
||||||
|
|||||||
@@ -7,15 +7,11 @@ import (
|
|||||||
// Channel represents a chat channel.
|
// Channel represents a chat channel.
|
||||||
type Channel struct {
|
type Channel struct {
|
||||||
Base
|
Base
|
||||||
|
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Modes string `json:"modes"`
|
Modes string `json:"modes"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example relation method — will be fleshed out when we add channel_members:
|
|
||||||
// func (c *Channel) Members(ctx context.Context) ([]*User, error) {
|
|
||||||
// return c.DB().GetChannelMembers(ctx, c.ID)
|
|
||||||
// }
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package models defines the data models used by the chat application.
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import "database/sql"
|
import "database/sql"
|
||||||
@@ -13,6 +14,7 @@ type Base struct {
|
|||||||
db DB
|
db DB
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDB injects the database reference into a model.
|
||||||
func (b *Base) SetDB(d DB) {
|
func (b *Base) SetDB(d DB) {
|
||||||
b.db = d
|
b.db = d
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,9 @@
|
|||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import "time"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
const (
|
||||||
"time"
|
httpReadTimeout = 10 * time.Second
|
||||||
|
httpWriteTimeout = 10 * time.Second
|
||||||
|
maxHeaderBytes = 1 << 20
|
||||||
)
|
)
|
||||||
|
|
||||||
func (s *Server) serveUntilShutdown() {
|
|
||||||
listenAddr := fmt.Sprintf(":%d", s.params.Config.Port)
|
|
||||||
s.httpServer = &http.Server{
|
|
||||||
Addr: listenAddr,
|
|
||||||
ReadTimeout: 10 * time.Second,
|
|
||||||
WriteTimeout: 10 * time.Second,
|
|
||||||
MaxHeaderBytes: 1 << 20,
|
|
||||||
Handler: s,
|
|
||||||
}
|
|
||||||
|
|
||||||
s.SetupRoutes()
|
|
||||||
|
|
||||||
s.log.Info("http begin listen", "listenaddr", listenAddr)
|
|
||||||
if err := s.httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
|
||||||
s.log.Error("listen error", "error", err)
|
|
||||||
if s.cancelFunc != nil {
|
|
||||||
s.cancelFunc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
s.router.ServeHTTP(w, r)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ import (
|
|||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const routeTimeout = 60 * time.Second
|
||||||
|
|
||||||
|
// SetupRoutes configures the HTTP routes and middleware chain.
|
||||||
func (s *Server) SetupRoutes() {
|
func (s *Server) SetupRoutes() {
|
||||||
s.router = chi.NewRouter()
|
s.router = chi.NewRouter()
|
||||||
|
|
||||||
@@ -23,7 +26,7 @@ func (s *Server) SetupRoutes() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
s.router.Use(s.mw.CORS())
|
s.router.Use(s.mw.CORS())
|
||||||
s.router.Use(middleware.Timeout(60 * time.Second))
|
s.router.Use(middleware.Timeout(routeTimeout))
|
||||||
|
|
||||||
if s.sentryEnabled {
|
if s.sentryEnabled {
|
||||||
sentryHandler := sentryhttp.New(sentryhttp.Options{
|
sentryHandler := sentryhttp.New(sentryhttp.Options{
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
|
// Package server implements the main HTTP server for the chat application.
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -20,11 +22,18 @@ import (
|
|||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/go-chi/chi"
|
"github.com/go-chi/chi"
|
||||||
|
|
||||||
_ "github.com/joho/godotenv/autoload"
|
_ "github.com/joho/godotenv/autoload" // loads .env file
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
shutdownTimeout = 5 * time.Second
|
||||||
|
sentryFlushTime = 2 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServerParams defines the dependencies for creating a Server.
|
||||||
type ServerParams struct {
|
type ServerParams struct {
|
||||||
fx.In
|
fx.In
|
||||||
|
|
||||||
Logger *logger.Logger
|
Logger *logger.Logger
|
||||||
Globals *globals.Globals
|
Globals *globals.Globals
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
@@ -32,12 +41,14 @@ type ServerParams struct {
|
|||||||
Handlers *handlers.Handlers
|
Handlers *handlers.Handlers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Server is the main HTTP server. It manages routing, middleware, and lifecycle.
|
||||||
|
// Server is the main HTTP server. It manages routing, middleware, and lifecycle.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
startupTime time.Time
|
startupTime time.Time
|
||||||
exitCode int
|
exitCode int
|
||||||
sentryEnabled bool
|
sentryEnabled bool
|
||||||
log *slog.Logger
|
log *slog.Logger
|
||||||
ctx context.Context
|
ctx context.Context //nolint:containedctx // signal handling pattern
|
||||||
cancelFunc context.CancelFunc
|
cancelFunc context.CancelFunc
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
router *chi.Mux
|
router *chi.Mux
|
||||||
@@ -46,6 +57,7 @@ type Server struct {
|
|||||||
h *handlers.Handlers
|
h *handlers.Handlers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New creates a new Server and registers its lifecycle hooks.
|
||||||
func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
|
func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
|
||||||
s := new(Server)
|
s := new(Server)
|
||||||
s.params = params
|
s.params = params
|
||||||
@@ -54,24 +66,37 @@ func New(lc fx.Lifecycle, params ServerParams) (*Server, error) {
|
|||||||
s.log = params.Logger.Get()
|
s.log = params.Logger.Get()
|
||||||
|
|
||||||
lc.Append(fx.Hook{
|
lc.Append(fx.Hook{
|
||||||
OnStart: func(ctx context.Context) error {
|
OnStart: func(_ context.Context) error {
|
||||||
s.startupTime = time.Now()
|
s.startupTime = time.Now()
|
||||||
go s.Run()
|
go s.Run() //nolint:contextcheck
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
OnStop: func(ctx context.Context) error {
|
OnStop: func(_ context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run starts the server configuration, Sentry, and begins serving.
|
||||||
func (s *Server) Run() {
|
func (s *Server) Run() {
|
||||||
s.configure()
|
s.configure()
|
||||||
s.enableSentry()
|
s.enableSentry()
|
||||||
s.serve()
|
s.serve()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ServeHTTP delegates to the chi router.
|
||||||
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
s.router.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MaintenanceMode reports whether the server is in maintenance mode.
|
||||||
|
func (s *Server) MaintenanceMode() bool {
|
||||||
|
return s.params.Config.MaintenanceMode
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) enableSentry() {
|
func (s *Server) enableSentry() {
|
||||||
s.sentryEnabled = false
|
s.sentryEnabled = false
|
||||||
|
|
||||||
@@ -87,6 +112,7 @@ func (s *Server) enableSentry() {
|
|||||||
s.log.Error("sentry init failure", "error", err)
|
s.log.Error("sentry init failure", "error", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.log.Info("sentry error reporting activated")
|
s.log.Info("sentry error reporting activated")
|
||||||
s.sentryEnabled = true
|
s.sentryEnabled = true
|
||||||
}
|
}
|
||||||
@@ -96,10 +122,12 @@ func (s *Server) serve() int {
|
|||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
c := make(chan os.Signal, 1)
|
c := make(chan os.Signal, 1)
|
||||||
|
|
||||||
signal.Ignore(syscall.SIGPIPE)
|
signal.Ignore(syscall.SIGPIPE)
|
||||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
sig := <-c
|
sig := <-c
|
||||||
s.log.Info("signal received", "signal", sig)
|
s.log.Info("signal received", "signal", sig)
|
||||||
|
|
||||||
if s.cancelFunc != nil {
|
if s.cancelFunc != nil {
|
||||||
s.cancelFunc()
|
s.cancelFunc()
|
||||||
}
|
}
|
||||||
@@ -108,8 +136,11 @@ func (s *Server) serve() int {
|
|||||||
go s.serveUntilShutdown()
|
go s.serveUntilShutdown()
|
||||||
|
|
||||||
for range s.ctx.Done() {
|
for range s.ctx.Done() {
|
||||||
|
// wait for context cancellation
|
||||||
}
|
}
|
||||||
|
|
||||||
s.cleanShutdown()
|
s.cleanShutdown()
|
||||||
|
|
||||||
return s.exitCode
|
return s.exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,10 +150,14 @@ func (s *Server) cleanupForExit() {
|
|||||||
|
|
||||||
func (s *Server) cleanShutdown() {
|
func (s *Server) cleanShutdown() {
|
||||||
s.exitCode = 0
|
s.exitCode = 0
|
||||||
ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
||||||
if err := s.httpServer.Shutdown(ctxShutdown); err != nil {
|
ctxShutdown, shutdownCancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
||||||
|
|
||||||
|
err := s.httpServer.Shutdown(ctxShutdown)
|
||||||
|
if err != nil {
|
||||||
s.log.Error("server clean shutdown failed", "error", err)
|
s.log.Error("server clean shutdown failed", "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if shutdownCancel != nil {
|
if shutdownCancel != nil {
|
||||||
shutdownCancel()
|
shutdownCancel()
|
||||||
}
|
}
|
||||||
@@ -130,13 +165,34 @@ func (s *Server) cleanShutdown() {
|
|||||||
s.cleanupForExit()
|
s.cleanupForExit()
|
||||||
|
|
||||||
if s.sentryEnabled {
|
if s.sentryEnabled {
|
||||||
sentry.Flush(2 * time.Second)
|
sentry.Flush(sentryFlushTime)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) MaintenanceMode() bool {
|
func (s *Server) configure() {
|
||||||
return s.params.Config.MaintenanceMode
|
// server configuration placeholder
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) configure() {
|
func (s *Server) serveUntilShutdown() {
|
||||||
|
listenAddr := fmt.Sprintf(":%d", s.params.Config.Port)
|
||||||
|
s.httpServer = &http.Server{
|
||||||
|
Addr: listenAddr,
|
||||||
|
ReadTimeout: httpReadTimeout,
|
||||||
|
WriteTimeout: httpWriteTimeout,
|
||||||
|
MaxHeaderBytes: maxHeaderBytes,
|
||||||
|
Handler: s,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.SetupRoutes()
|
||||||
|
|
||||||
|
s.log.Info("http begin listen", "listenaddr", listenAddr)
|
||||||
|
|
||||||
|
err := s.httpServer.ListenAndServe()
|
||||||
|
if err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||||
|
s.log.Error("listen error", "error", err)
|
||||||
|
|
||||||
|
if s.cancelFunc != nil {
|
||||||
|
s.cancelFunc()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user