chore: add go config and logging layer
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Gemeinsames Go-Modul `platform/` mit ENV-Konfiguration und strukturiertem JSON-Logging angelegt.
|
||||
- Forgejo-Actions-CI mit Go/Frontend-Verifikation und Gitleaks-Secret-Scan angelegt.
|
||||
- E0-T01 begonnen: Monorepo-Grundstruktur, Root-Dokumentation, Env-Beispiel und minimale Service-Startpunkte angelegt.
|
||||
- Annahme dokumentiert: vorlaeufiger Modulpfad ist `proxui`.
|
||||
|
||||
@@ -20,10 +20,10 @@ migrate:
|
||||
@echo "No migrations configured yet. E2-T01 will wire golang-migrate."
|
||||
|
||||
test:
|
||||
env GOCACHE="$(GOCACHE)" go test ./backend/... ./worker/... ./console-proxy/...
|
||||
env GOCACHE="$(GOCACHE)" go test ./platform/... ./backend/... ./worker/... ./console-proxy/...
|
||||
|
||||
lint:
|
||||
env GOCACHE="$(GOCACHE)" go vet ./backend/... ./worker/... ./console-proxy/...
|
||||
env GOCACHE="$(GOCACHE)" go vet ./platform/... ./backend/... ./worker/... ./console-proxy/...
|
||||
npm run lint --prefix frontend
|
||||
|
||||
build:
|
||||
|
||||
@@ -7,6 +7,7 @@ Multi-Tenant-Konsole fuer Proxmox auf Basis von Go, Supabase Postgres/Auth, Redi
|
||||
- `backend/`: HTTP-API, Auth/JWT-Validierung, RBAC, DB-Zugriff und Proxmox-Orchestrierung
|
||||
- `worker/`: asynchrone Jobs fuer Proxmox-Tasks, Reconciliation und Cleanup
|
||||
- `console-proxy/`: Websocket-Proxy fuer noVNC/xterm.js ohne direkte Proxmox-Verbindung im Browser
|
||||
- `platform/`: gemeinsame Go-Bausteine fuer Konfiguration, Logging und spaetere Querschnittsfunktionen
|
||||
- `frontend/`: React/TypeScript/Vite-App fuer Kunden- und Admin-Workflows
|
||||
- `migrations/`: versionierte SQL-Migrationen
|
||||
- `deploy/`: lokale und spaetere Deployment-Artefakte
|
||||
|
||||
@@ -21,10 +21,14 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda
|
||||
- [x] Forgejo-Actions-Workflow fuer Pushes auf `main` und Pull Requests angelegt
|
||||
- [x] Go- und Frontend-Verifikation ueber `make verify` eingebunden
|
||||
- [x] Gitleaks-Secret-Scan mit enger Allowlist fuer offizielle Supabase-Beispieldateien eingebunden
|
||||
- [x] E0-T04: Konfigurations- und Logging-Layer fuer Go-Dienste
|
||||
- [x] Gemeinsames Go-Modul `platform/` angelegt
|
||||
- [x] ENV-basierte Konfiguration mit Defaults und Validierung eingebunden
|
||||
- [x] Einheitliches strukturiertes JSON-Logging mit Service- und Env-Feldern eingebunden
|
||||
- [x] Backend, Worker und Console-Proxy auf den gemeinsamen Layer umgestellt
|
||||
|
||||
## Naechste Aufgaben
|
||||
|
||||
- [ ] E0-T04: Konfigurations- und Logging-Layer fuer Go-Dienste
|
||||
- [ ] E2-T01: Migrations-Setup mit `golang-migrate`
|
||||
|
||||
## MVP-Backlog
|
||||
@@ -63,3 +67,4 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda
|
||||
- 2026-06-09: `GET /auth/v1/admin/users` ueber Kong mit Service-Role-Key erfolgreich: leere User-Liste statt Studio-Fehler.
|
||||
- 2026-06-10: CI-Workflow fuer Forgejo Actions angelegt; lokales `make verify` erfolgreich.
|
||||
- 2026-06-10: Gitleaks-Secret-Scan lokal mit `zricethezav/gitleaks:v8.28.0` erfolgreich; keine Leaks gefunden.
|
||||
- 2026-06-10: Gemeinsamer Go-Config-/Logging-Layer angelegt; Tests fuer Config-Defaults und Validierung ergaenzt.
|
||||
|
||||
+13
-12
@@ -3,17 +3,25 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/config"
|
||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/logging"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := getenv("BACKEND_ADDR", ":8080")
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
logger := logging.New("backend", "unknown", 0)
|
||||
logger.Error("failed to load config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger := logging.New("backend", cfg.AppEnv, cfg.LogLevel)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -22,7 +30,7 @@ func main() {
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Addr: cfg.BackendAddr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
@@ -31,7 +39,7 @@ func main() {
|
||||
defer stop()
|
||||
|
||||
go func() {
|
||||
logger.Info("backend listening", "addr", addr)
|
||||
logger.Info("backend listening", "addr", cfg.BackendAddr)
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
logger.Error("backend failed", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -47,10 +55,3 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -3,17 +3,25 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/config"
|
||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/logging"
|
||||
)
|
||||
|
||||
func main() {
|
||||
addr := getenv("CONSOLE_PROXY_ADDR", ":8081")
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
logger := logging.New("console-proxy", "unknown", 0)
|
||||
logger.Error("failed to load config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger := logging.New("console-proxy", cfg.AppEnv, cfg.LogLevel)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -22,7 +30,7 @@ func main() {
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: addr,
|
||||
Addr: cfg.ConsoleProxyAddr,
|
||||
Handler: mux,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
}
|
||||
@@ -31,7 +39,7 @@ func main() {
|
||||
defer stop()
|
||||
|
||||
go func() {
|
||||
logger.Info("console proxy listening", "addr", addr)
|
||||
logger.Info("console proxy listening", "addr", cfg.ConsoleProxyAddr)
|
||||
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
logger.Error("console proxy failed", "error", err)
|
||||
os.Exit(1)
|
||||
@@ -47,10 +55,3 @@ func main() {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
AppEnv string
|
||||
LogLevel slog.Level
|
||||
BackendAddr string
|
||||
ConsoleProxyAddr string
|
||||
WorkerConcurrency int
|
||||
DatabaseURL string
|
||||
DatabaseDirectURL string
|
||||
SupabaseURL string
|
||||
SupabaseJWKSURL string
|
||||
SupabaseIssuer string
|
||||
RedisAddr string
|
||||
}
|
||||
|
||||
func Load() (Config, error) {
|
||||
logLevel, err := parseLogLevel(getenv("LOG_LEVEL", "info"))
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
workerConcurrency, err := intFromEnv("WORKER_CONCURRENCY", 5)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
if workerConcurrency < 1 {
|
||||
return Config{}, fmt.Errorf("WORKER_CONCURRENCY must be greater than 0")
|
||||
}
|
||||
|
||||
return Config{
|
||||
AppEnv: getenv("APP_ENV", "development"),
|
||||
LogLevel: logLevel,
|
||||
BackendAddr: getenv("BACKEND_ADDR", ":8080"),
|
||||
ConsoleProxyAddr: getenv("CONSOLE_PROXY_ADDR", ":8081"),
|
||||
WorkerConcurrency: workerConcurrency,
|
||||
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||
DatabaseDirectURL: os.Getenv("DATABASE_DIRECT_URL"),
|
||||
SupabaseURL: os.Getenv("SUPABASE_URL"),
|
||||
SupabaseJWKSURL: os.Getenv("SUPABASE_JWKS_URL"),
|
||||
SupabaseIssuer: os.Getenv("SUPABASE_ISSUER"),
|
||||
RedisAddr: getenv("REDIS_ADDR", "localhost:6379"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func getenv(key, fallback string) string {
|
||||
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
||||
return value
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func intFromEnv(key string, fallback int) (int, error) {
|
||||
raw := strings.TrimSpace(os.Getenv(key))
|
||||
if raw == "" {
|
||||
return fallback, nil
|
||||
}
|
||||
|
||||
value, err := strconv.Atoi(raw)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("%s must be an integer: %w", key, err)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
func parseLogLevel(raw string) (slog.Level, error) {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "debug":
|
||||
return slog.LevelDebug, nil
|
||||
case "info":
|
||||
return slog.LevelInfo, nil
|
||||
case "warn", "warning":
|
||||
return slog.LevelWarn, nil
|
||||
case "error":
|
||||
return slog.LevelError, nil
|
||||
default:
|
||||
return slog.LevelInfo, fmt.Errorf("LOG_LEVEL must be one of debug, info, warn, error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLoadDefaults(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.AppEnv != "development" {
|
||||
t.Fatalf("AppEnv = %q, want development", cfg.AppEnv)
|
||||
}
|
||||
if cfg.LogLevel != slog.LevelInfo {
|
||||
t.Fatalf("LogLevel = %v, want info", cfg.LogLevel)
|
||||
}
|
||||
if cfg.BackendAddr != ":8080" {
|
||||
t.Fatalf("BackendAddr = %q, want :8080", cfg.BackendAddr)
|
||||
}
|
||||
if cfg.ConsoleProxyAddr != ":8081" {
|
||||
t.Fatalf("ConsoleProxyAddr = %q, want :8081", cfg.ConsoleProxyAddr)
|
||||
}
|
||||
if cfg.WorkerConcurrency != 5 {
|
||||
t.Fatalf("WorkerConcurrency = %d, want 5", cfg.WorkerConcurrency)
|
||||
}
|
||||
if cfg.RedisAddr != "localhost:6379" {
|
||||
t.Fatalf("RedisAddr = %q, want localhost:6379", cfg.RedisAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOverrides(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
|
||||
t.Setenv("APP_ENV", "test")
|
||||
t.Setenv("LOG_LEVEL", "debug")
|
||||
t.Setenv("BACKEND_ADDR", ":9000")
|
||||
t.Setenv("CONSOLE_PROXY_ADDR", ":9001")
|
||||
t.Setenv("WORKER_CONCURRENCY", "12")
|
||||
t.Setenv("REDIS_ADDR", "redis:6379")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.AppEnv != "test" {
|
||||
t.Fatalf("AppEnv = %q, want test", cfg.AppEnv)
|
||||
}
|
||||
if cfg.LogLevel != slog.LevelDebug {
|
||||
t.Fatalf("LogLevel = %v, want debug", cfg.LogLevel)
|
||||
}
|
||||
if cfg.BackendAddr != ":9000" {
|
||||
t.Fatalf("BackendAddr = %q, want :9000", cfg.BackendAddr)
|
||||
}
|
||||
if cfg.ConsoleProxyAddr != ":9001" {
|
||||
t.Fatalf("ConsoleProxyAddr = %q, want :9001", cfg.ConsoleProxyAddr)
|
||||
}
|
||||
if cfg.WorkerConcurrency != 12 {
|
||||
t.Fatalf("WorkerConcurrency = %d, want 12", cfg.WorkerConcurrency)
|
||||
}
|
||||
if cfg.RedisAddr != "redis:6379" {
|
||||
t.Fatalf("RedisAddr = %q, want redis:6379", cfg.RedisAddr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidLogLevel(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
|
||||
t.Setenv("LOG_LEVEL", "trace")
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("Load() error = nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadRejectsInvalidWorkerConcurrency(t *testing.T) {
|
||||
clearConfigEnv(t)
|
||||
|
||||
t.Setenv("WORKER_CONCURRENCY", "0")
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("Load() error = nil, want error")
|
||||
}
|
||||
}
|
||||
|
||||
func clearConfigEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
for _, key := range []string{
|
||||
"APP_ENV",
|
||||
"LOG_LEVEL",
|
||||
"BACKEND_ADDR",
|
||||
"CONSOLE_PROXY_ADDR",
|
||||
"WORKER_CONCURRENCY",
|
||||
"DATABASE_URL",
|
||||
"DATABASE_DIRECT_URL",
|
||||
"SUPABASE_URL",
|
||||
"SUPABASE_JWKS_URL",
|
||||
"SUPABASE_ISSUER",
|
||||
"REDIS_ADDR",
|
||||
} {
|
||||
t.Setenv(key, "")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module forgejo.digital-droplets.de/philschlo/proxui/platform
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,17 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"os"
|
||||
)
|
||||
|
||||
func New(service string, appEnv string, level slog.Level) *slog.Logger {
|
||||
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: level,
|
||||
})
|
||||
|
||||
return slog.New(handler).With(
|
||||
"service", service,
|
||||
"env", appEnv,
|
||||
)
|
||||
}
|
||||
@@ -2,18 +2,27 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/config"
|
||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/logging"
|
||||
)
|
||||
|
||||
func main() {
|
||||
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
logger := logging.New("worker", "unknown", 0)
|
||||
logger.Error("failed to load config", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger := logging.New("worker", cfg.AppEnv, cfg.LogLevel)
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
|
||||
defer stop()
|
||||
|
||||
logger.Info("worker started")
|
||||
logger.Info("worker started", "concurrency", cfg.WorkerConcurrency)
|
||||
<-ctx.Done()
|
||||
logger.Info("worker stopped")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user