feat: implement Epic 0 - Project Setup & Foundation

Implemented all 5 stories from Epic 0:

Story 0.1: Project Initialization
- Initialize Go module with path git.dcentral.systems/toolz/goplt
- Create complete directory structure (cmd/, internal/, pkg/, modules/, config/, etc.)
- Add comprehensive .gitignore for Go projects
- Create README.md with project overview and setup instructions

Story 0.2: Configuration Management System
- Define ConfigProvider interface in pkg/config
- Implement Viper-based configuration in internal/config
- Create configuration loader with environment support
- Add default, development, and production YAML config files

Story 0.3: Structured Logging System
- Define Logger interface in pkg/logger
- Implement Zap-based logger in internal/logger
- Add request ID middleware for Gin
- Create global logger export with convenience functions
- Support context-aware logging with request/user ID extraction

Story 0.4: CI/CD Pipeline
- Create GitHub Actions workflow for CI (test, lint, build, fmt)
- Add comprehensive Makefile with development commands
- Configure golangci-lint with reasonable defaults

Story 0.5: Dependency Injection and Bootstrap
- Create FX-based DI container in internal/di
- Implement provider functions for Config and Logger
- Create application entry point in cmd/platform/main.go
- Add lifecycle management with graceful shutdown

All acceptance criteria met:
- go build ./cmd/platform succeeds
- go test ./... runs successfully
- go mod verify passes
- Config loads from config/default.yaml
- Logger can be injected and used
- Application starts and shuts down gracefully
This commit is contained in:
2025-11-05 12:21:15 +01:00
parent 3f90262860
commit 4724a2efb5
21 changed files with 1442 additions and 17 deletions

66
internal/di/container.go Normal file
View File

@@ -0,0 +1,66 @@
package di
import (
"context"
"os"
"os/signal"
"syscall"
"time"
"go.uber.org/fx"
)
// Container wraps the FX application and provides lifecycle management.
type Container struct {
app *fx.App
}
// NewContainer creates a new DI container with the provided options.
func NewContainer(opts ...fx.Option) *Container {
// Add core module
allOpts := []fx.Option{CoreModule()}
allOpts = append(allOpts, opts...)
app := fx.New(allOpts...)
return &Container{
app: app,
}
}
// Start starts the container and blocks until shutdown.
func (c *Container) Start(ctx context.Context) error {
// Start the FX app
if err := c.app.Start(ctx); err != nil {
return err
}
// Wait for interrupt signal
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
// Block until signal received
<-sigChan
// Create shutdown context
shutdownCtx, cancel := context.WithTimeout(context.Background(), c.getShutdownTimeout())
defer cancel()
// Stop the FX app
if err := c.app.Stop(shutdownCtx); err != nil {
return err
}
return nil
}
// Stop stops the container gracefully.
func (c *Container) Stop(ctx context.Context) error {
return c.app.Stop(ctx)
}
// getShutdownTimeout returns the shutdown timeout duration.
// Can be made configurable in the future.
func (c *Container) getShutdownTimeout() time.Duration {
return 30 * time.Second
}

83
internal/di/providers.go Normal file
View File

@@ -0,0 +1,83 @@
package di
import (
"context"
"fmt"
"os"
"go.uber.org/fx"
configimpl "git.dcentral.systems/toolz/goplt/internal/config"
loggerimpl "git.dcentral.systems/toolz/goplt/internal/logger"
"git.dcentral.systems/toolz/goplt/pkg/config"
"git.dcentral.systems/toolz/goplt/pkg/logger"
)
// ProvideConfig creates an FX option that provides ConfigProvider.
func ProvideConfig() fx.Option {
return fx.Provide(func() (config.ConfigProvider, error) {
// Determine environment from environment variable or default to "development"
env := os.Getenv("ENVIRONMENT")
if env == "" {
env = "development"
}
cfg, err := configimpl.LoadConfig(env)
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
return cfg, nil
})
}
// ProvideLogger creates an FX option that provides Logger.
func ProvideLogger() fx.Option {
return fx.Provide(func(cfg config.ConfigProvider) (logger.Logger, error) {
level := cfg.GetString("logging.level")
if level == "" {
level = "info"
}
format := cfg.GetString("logging.format")
if format == "" {
format = "json"
}
log, err := loggerimpl.NewZapLogger(level, format)
if err != nil {
return nil, fmt.Errorf("failed to create logger: %w", err)
}
// Set as global logger
logger.SetGlobalLogger(log)
return log, nil
})
}
// CoreModule returns an FX option that provides all core services.
// This includes configuration and logging.
func CoreModule() fx.Option {
return fx.Options(
ProvideConfig(),
ProvideLogger(),
)
}
// RegisterLifecycleHooks registers lifecycle hooks for logging.
func RegisterLifecycleHooks(lc fx.Lifecycle, l logger.Logger) {
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
l.Info("Application starting",
logger.String("component", "bootstrap"),
)
return nil
},
OnStop: func(ctx context.Context) error {
l.Info("Application shutting down",
logger.String("component", "bootstrap"),
)
return nil
},
})
}