diff --git a/cmd/lokstra/main.go b/cmd/lokstra/main.go index 969e98d7..8827fdee 100644 --- a/cmd/lokstra/main.go +++ b/cmd/lokstra/main.go @@ -8,15 +8,15 @@ import ( "slices" "github.com/primadi/lokstra" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/annotation" - "github.com/primadi/lokstra/core/deploy" ) const version = "1.0.2" func main() { - deploy.SetLogLevel(deploy.LogLevelInfo) + logger.SetLogLevel(logger.LogLevelInfo) // for debugging purpose if lokstra.DetectRunMode() != lokstra.RunModeProd { diff --git a/cmd/lokstra/migration.go b/cmd/lokstra/migration.go index a7cbd3c9..2c22b8f9 100644 --- a/cmd/lokstra/migration.go +++ b/cmd/lokstra/migration.go @@ -35,7 +35,7 @@ func migrationCmd() { migrationFlags := flag.NewFlagSet("migration", flag.ExitOnError) configFileFlag := migrationFlags.String("config", "config.yaml", "Lokstra config file") migDirFlag := migrationFlags.String("dir", "migrations", "Migrations directory") - dbFlag := migrationFlags.String("db", "global-db", "Database pool name") + dbFlag := migrationFlags.String("db", "db_main", "Database pool name") stepsFlag := migrationFlags.Int("steps", 1, "Number of migrations to rollback") // Handle create command separately (doesn't need DB connection) @@ -134,9 +134,9 @@ func executeMigration(subCmd, configFile, migrationDir, dbPoolName string, steps return fmt.Errorf("database pool '%s' not found in registry", dbPoolName) } - pool, ok := poolAny.(serviceapi.DbPoolWithSchema) + pool, ok := poolAny.(serviceapi.DbPool) if !ok { - return fmt.Errorf("database pool '%s' does not implement DbPoolWithSchema interface", dbPoolName) + return fmt.Errorf("database pool '%s' does not implement DbPool interface", dbPoolName) } // Create migration runner diff --git a/common/logger/logger.go b/common/logger/logger.go new file mode 100644 index 00000000..bff03d82 --- /dev/null +++ b/common/logger/logger.go @@ -0,0 +1,78 @@ +package logger + +import ( + "os" + "strings" +) + +// LogLevel represents the logging level +type LogLevel int + +const ( + LogLevelSilent LogLevel = iota + LogLevelError + LogLevelWarn + LogLevelInfo + LogLevelDebug + LogLevelFromEnvi +) + +var ( + activeBackend LoggerBackend = NewSlogBackend() // default slog +) + +// LoggerBackend is the interface for logging backends. +// This allows replacing slog with zap, zerolog, etc in the future. +type LoggerBackend interface { + Debug(msg string, args ...any) + Info(msg string, args ...any) + Warn(msg string, args ...any) + Error(msg string, args ...any) + Panic(msg string, args ...any) + SetLogLevel(level LogLevel) + GetLogLevel() LogLevel +} + +// SetBackend replaces the active logger backend +func SetBackend(backend LoggerBackend) { + activeBackend = backend +} + +// SetLogLevel sets the global log level +func SetLogLevel(level LogLevel) { + if level == LogLevelFromEnvi { + SetLogLevelFromEnv() + return + } + activeBackend.SetLogLevel(level) +} + +// GetLogLevel returns the current log level +func GetLogLevel() LogLevel { + return activeBackend.GetLogLevel() +} + +// SetLogLevelFromEnv sets log level from env var: LOKSTRA_LOG_LEVEL +func SetLogLevelFromEnv() { + envLevel := strings.ToLower(os.Getenv("LOKSTRA_LOG_LEVEL")) + switch envLevel { + case "silent": + SetLogLevel(LogLevelSilent) + case "error": + SetLogLevel(LogLevelError) + case "warn", "warning": + SetLogLevel(LogLevelWarn) + case "info": + SetLogLevel(LogLevelInfo) + case "debug": + SetLogLevel(LogLevelDebug) + } +} + +// Public wrapper functions (unchanged API) +func LogDebug(format string, args ...any) { activeBackend.Debug(format, args...) } +func LogInfo(format string, args ...any) { activeBackend.Info(format, args...) } +func LogWarn(format string, args ...any) { activeBackend.Warn(format, args...) } +func LogWarning(format string, args ...any) { activeBackend.Warn(format, args...) } +func LogError(format string, args ...any) { activeBackend.Error(format, args...) } +func LogPanic(format string, args ...any) { activeBackend.Panic(format, args...) } diff --git a/common/logger/readable_handler.go b/common/logger/readable_handler.go new file mode 100644 index 00000000..0e234a54 --- /dev/null +++ b/common/logger/readable_handler.go @@ -0,0 +1,54 @@ +package logger + +import ( + "context" + "fmt" + "log/slog" + "os" + "strings" + "time" +) + +type ReadableHandler struct { + Level LogLevel + Out *os.File +} + +func (h *ReadableHandler) Enabled(_ context.Context, level slog.Level) bool { + switch h.Level { + case LogLevelSilent: + return false + case LogLevelError: + return level >= slog.LevelError + case LogLevelWarn: + return level >= slog.LevelWarn + case LogLevelInfo: + return level >= slog.LevelInfo + case LogLevelDebug: + return level >= slog.LevelDebug + } + return true +} + +func (h *ReadableHandler) Handle(_ context.Context, r slog.Record) error { + timestamp := time.Now().Format("2006/01/02 15:04:05") + + level := "[" + strings.ToUpper(r.Level.String()) + "]" + + // message + line := fmt.Sprintf("%s %s %s", timestamp, level, r.Message) + + // attributes → optional, currently ignored for simplicity + // You may add key=value printing here if needed. + + _, err := fmt.Fprintln(h.Out, line) + return err +} + +func (h *ReadableHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return h +} + +func (h *ReadableHandler) WithGroup(name string) slog.Handler { + return h +} diff --git a/common/logger/slog_backend.go b/common/logger/slog_backend.go new file mode 100644 index 00000000..624f7ef3 --- /dev/null +++ b/common/logger/slog_backend.go @@ -0,0 +1,73 @@ +package logger + +import ( + "fmt" + "log/slog" + "os" +) + +type SlogBackend struct { + logger *slog.Logger + level LogLevel +} + +func NewSlogBackend() *SlogBackend { + b := &SlogBackend{level: LogLevelInfo} + b.rebuildLogger() + return b +} + +func (b *SlogBackend) SetLogLevel(level LogLevel) { + // If user passes LogLevelFromEnvi directly → load env level + if level == LogLevelFromEnvi { + // Call global logic to resolve env variable + SetLogLevelFromEnv() + return + } + + b.level = level + b.rebuildLogger() +} + +func (b *SlogBackend) GetLogLevel() LogLevel { + return b.level +} + +func (b *SlogBackend) Debug(format string, args ...any) { + if b.level >= LogLevelDebug { + b.logger.Debug(fmt.Sprintf(format, args...)) + } +} + +func (b *SlogBackend) Info(format string, args ...any) { + if b.level >= LogLevelInfo { + b.logger.Info(fmt.Sprintf(format, args...)) + } +} + +func (b *SlogBackend) Warn(format string, args ...any) { + if b.level >= LogLevelWarn { + b.logger.Warn(fmt.Sprintf(format, args...)) + } +} + +func (b *SlogBackend) Error(format string, args ...any) { + if b.level >= LogLevelError { + b.logger.Error(fmt.Sprintf(format, args...)) + } +} + +func (b *SlogBackend) Panic(format string, args ...any) { + msg := fmt.Sprintf(format, args...) + b.logger.Error(msg) + panic(msg) +} + +func (b *SlogBackend) rebuildLogger() { + handler := &ReadableHandler{ + Level: b.level, + Out: os.Stdout, + } + + b.logger = slog.New(handler) +} diff --git a/common/utils/maps.go b/common/utils/maps.go index 86000554..5f0b55c5 100644 --- a/common/utils/maps.go +++ b/common/utils/maps.go @@ -1,9 +1,10 @@ package utils import ( - "fmt" "maps" "time" + + "github.com/primadi/lokstra/common/logger" ) func GetValueFromMap[T any](settings map[string]any, key string, defaultValue T) T { @@ -31,7 +32,7 @@ func GetDurationFromMap(settings map[string]any, key string, defaultValue any) t if d, err := time.ParseDuration(v); err == nil { return d } else { - fmt.Printf("Invalid duration string for key %q: %v\n", key, err) + logger.LogInfo("Invalid duration string for key %q: %v\n", key, err) } case float64: // jika YAML sudah diparse jadi angka (ms, s, dst) return time.Duration(v) * time.Second @@ -42,7 +43,7 @@ func GetDurationFromMap(settings map[string]any, key string, defaultValue any) t case time.Duration: return v default: - fmt.Printf("Unsupported duration type for key %q: %T\n", key, val) + logger.LogInfo("Unsupported duration type for key %q: %T\n", key, val) } } @@ -51,7 +52,7 @@ func GetDurationFromMap(settings map[string]any, key string, defaultValue any) t if d, err := time.ParseDuration(v); err == nil { return d } else { - fmt.Printf("Invalid duration string for key %q: %v\n", key, err) + logger.LogInfo("Invalid duration string for key %q: %v\n", key, err) } case float64: // jika YAML sudah diparse jadi angka (ms, s, dst) return time.Duration(v) * time.Second @@ -62,7 +63,7 @@ func GetDurationFromMap(settings map[string]any, key string, defaultValue any) t case time.Duration: return v default: - fmt.Printf("Unsupported default duration type for key %q: %T\n", + logger.LogInfo("Unsupported default duration type for key %q: %T\n", key, defaultValue) } diff --git a/core/annotation/complex_processor.go b/core/annotation/complex_processor.go index 664112d4..2a47b413 100644 --- a/core/annotation/complex_processor.go +++ b/core/annotation/complex_processor.go @@ -15,6 +15,7 @@ import ( "sync" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/annotation/internal" ) @@ -131,7 +132,7 @@ func ProcessComplexAnnotations(rootPath []string, maxWorkers int, if len(packagesWithServices) > 0 && len(rootPath) > 0 { if err := generateImportFile(rootPath[0], packagesWithServices); err != nil { // Log warning but don't fail the whole operation - fmt.Printf("⚠️ Warning: Failed to generate import file: %v\n", err) + logger.LogWarn("⚠️ Warning: Failed to generate import file: %v\n", err) } } @@ -793,6 +794,6 @@ func generateImportFile(startPath string, packages []string) error { return fmt.Errorf("failed to write %s: %w", importFilePath, err) } - fmt.Printf("✅ Generated: %s\n", importFilePath) + logger.LogInfo("✅ Generated: %s\n", importFilePath) return nil } diff --git a/core/annotation/examples/annotation_parsing/main.go b/core/annotation/examples/annotation_parsing/main.go index 14a1c88e..2f0ca0f5 100644 --- a/core/annotation/examples/annotation_parsing/main.go +++ b/core/annotation/examples/annotation_parsing/main.go @@ -2,9 +2,9 @@ package main import ( "fmt" - "log" "strings" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/annotation" ) @@ -16,7 +16,7 @@ func main() { annotations, err := annotation.ParseFileAnnotations(filePath) if err != nil { - log.Fatalf("Error parsing file: %v", err) + logger.LogPanic("Error parsing file: %v", err) } fmt.Printf("\nFound %d annotations:\n\n", len(annotations)) diff --git a/core/app/app.go b/core/app/app.go index f68bfa60..f4a3d93e 100644 --- a/core/app/app.go +++ b/core/app/app.go @@ -7,6 +7,7 @@ import ( "syscall" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/app/listener" "github.com/primadi/lokstra/core/router" "github.com/primadi/lokstra/lokstra_handler" @@ -101,7 +102,7 @@ func (a *App) AddReverseProxies(proxies []*ReverseProxyConfig) { return } - fmt.Printf("📦 [%s] Adding %d reverse proxy(ies)...\n", a.name, len(proxies)) + logger.LogInfo("📦 [%s] Adding %d reverse proxy(ies)...\n", a.name, len(proxies)) // Create a dedicated router for reverse proxies proxyRouter := router.New(a.name + "-reverse-proxy") @@ -119,10 +120,10 @@ func (a *App) AddReverseProxies(proxies []*ReverseProxyConfig) { From: proxy.Rewrite.From, To: proxy.Rewrite.To, } - fmt.Printf(" 🔄 %s -> %s (strip: %v, rewrite: %s -> %s)\n", + logger.LogInfo(" 🔄 %s -> %s (strip: %v, rewrite: %s -> %s)", proxy.Prefix, proxy.Target, proxy.StripPrefix, proxy.Rewrite.From, proxy.Rewrite.To) } else { - fmt.Printf(" 🔄 %s -> %s (strip: %v)\n", + logger.LogInfo(" 🔄 %s -> %s (strip: %v)", proxy.Prefix, proxy.Target, proxy.StripPrefix) } @@ -143,7 +144,7 @@ func (a *App) AddReverseProxies(proxies []*ReverseProxyConfig) { a.mainRouter = proxyRouter } - fmt.Printf("✅ [%s] Reverse proxies added successfully\n", a.name) + logger.LogInfo("✅ [%s] Reverse proxies added successfully\n", a.name) } func (a *App) numRouters() int { @@ -163,16 +164,12 @@ func (a *App) numRouters() int { // Print app start information, including the number of routers and their routes func (a *App) PrintStartInfo() { - if a.mainRouter == nil { - // App may have no routers (e.g., static file server only) - fmt.Println("Starting [" + a.name + "] with 0 router(s) on address " + - a.listenerConfig["addr"].(string)) - return - } + logger.LogInfo("Starting [%s] with %d router(s) on address %s", + a.name, a.numRouters(), a.listenerConfig["addr"]) - fmt.Println("Starting ["+a.name+"] with", a.numRouters(), "router(s) on address", - a.listenerConfig["addr"]) - a.mainRouter.PrintRoutes() + if a.mainRouter != nil { + a.mainRouter.PrintRoutes() + } } // Start the app. It blocks until the app stops or returns an error. @@ -207,7 +204,7 @@ func (a *App) Run(timeout time.Duration) error { select { case sig := <-stop: - fmt.Println("Received shutdown signal:", sig) + logger.LogInfo("Received shutdown signal: %v", sig) if err := a.Shutdown(timeout); err != nil { return fmt.Errorf("shutdown error: %w", err) } diff --git a/core/app/listener/fasthttp/fasthttp.go b/core/app/listener/fasthttp/fasthttp.go index e6a930e2..4fbff98a 100644 --- a/core/app/listener/fasthttp/fasthttp.go +++ b/core/app/listener/fasthttp/fasthttp.go @@ -12,6 +12,7 @@ import ( "sync/atomic" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/app/listener" listener_utils "github.com/primadi/lokstra/core/app/listener/utils" @@ -70,14 +71,14 @@ func (s *FastHttp) ListenAndServe() error { if err != nil { return fmt.Errorf("failed to listen on unix socket: %w", err) } - // fmt.Printf("[FastHttp] Starting server on Unix socket %s\n", socketPath) + logger.LogInfo("[FastHttp] Starting server on Unix socket %s\n", socketPath) } else { var err error listener, err = net.Listen("tcp", s.addr) if err != nil { return listener_utils.WrapListenError(s.addr, err) } - // fmt.Printf("[FastHttp] Starting server on TCP %s\n", addr) + logger.LogInfo("[FastHttp] Starting server on TCP %s\n", s.addr) } if s.secure { @@ -105,9 +106,9 @@ func (s *FastHttp) Shutdown(timeout time.Duration) error { defer cancel() if s.secure { - fmt.Printf("[FastHttp] Initiating graceful shutdown for secure app at %s\n", s.addr) + logger.LogInfo("[FastHttp] Initiating graceful shutdown for secure app at %s\n", s.addr) } else { - fmt.Printf("[FastHttp] Initiating graceful shutdown for app at %s\n", s.addr) + logger.LogInfo("[FastHttp] Initiating graceful shutdown for app at %s\n", s.addr) } shutdownErr := s.server.ShutdownWithContext(ctx) diff --git a/core/app/listener/https3/http3.go b/core/app/listener/https3/http3.go index f7a68530..eedb708d 100644 --- a/core/app/listener/https3/http3.go +++ b/core/app/listener/https3/http3.go @@ -8,6 +8,7 @@ import ( "sync/atomic" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/app/listener" listener_utils "github.com/primadi/lokstra/core/app/listener/utils" @@ -68,7 +69,7 @@ func (s *Http3) Shutdown(timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - fmt.Printf("[HTTP3] Initiating graceful shutdown for app at %s\n", s.server.Addr) + logger.LogInfo("[HTTP3] Initiating graceful shutdown for app at %s\n", s.server.Addr) shutdownErr := s.server.Shutdown(ctx) done := make(chan struct{}) diff --git a/core/app/listener/net_http.go b/core/app/listener/net_http.go index 22b1b4a1..448653d2 100644 --- a/core/app/listener/net_http.go +++ b/core/app/listener/net_http.go @@ -12,6 +12,7 @@ import ( "sync/atomic" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" listener_utils "github.com/primadi/lokstra/core/app/listener/utils" ) @@ -77,14 +78,14 @@ func (s *NetHttp) ListenAndServe() error { if err != nil { return fmt.Errorf("failed to listen on unix socket: %w", err) } - // fmt.Printf("[NETHTTP] Starting server on Unix socket %s\n", socketPath) + logger.LogInfo("[NETHTTP] Starting server on Unix socket %s\n", socketPath) } else { var err error listener, err = net.Listen("tcp", s.server.Addr) if err != nil { return listener_utils.WrapListenError(s.server.Addr, err) } - // fmt.Printf("[NETHTTP] Starting server on TCP %s\n", addr) + logger.LogInfo("[NETHTTP] Starting server on TCP %s\n", s.server.Addr) } if s.secure { @@ -112,9 +113,9 @@ func (s *NetHttp) Shutdown(timeout time.Duration) error { defer cancel() if s.secure { - fmt.Printf("[NETHTTP] Initiating graceful shutdown for secure app at %s\n", s.server.Addr) + logger.LogInfo("[NETHTTP] Initiating graceful shutdown for secure app at %s\n", s.server.Addr) } else { - fmt.Printf("[NETHTTP] Initiating graceful shutdown for app at %s\n", s.server.Addr) + logger.LogInfo("[NETHTTP] Initiating graceful shutdown for app at %s\n", s.server.Addr) } shutdownErr := s.server.Shutdown(ctx) diff --git a/core/deploy/internal/test/registry_lazy_test.go b/core/deploy/internal/test/registry_lazy_test.go index 798ee615..ff695b61 100644 --- a/core/deploy/internal/test/registry_lazy_test.go +++ b/core/deploy/internal/test/registry_lazy_test.go @@ -143,7 +143,7 @@ func TestRegisterLazyService_MultipleInstances(t *testing.T) { g := deploy.NewGlobalRegistry() // Register multiple DB instances with different DSN - g.RegisterLazyService("db-main", func(cfg map[string]any) any { + g.RegisterLazyService("db_main", func(cfg map[string]any) any { return "Connection to " + cfg["dsn"].(string) }, map[string]any{"dsn": "main-db"}) @@ -156,9 +156,9 @@ func TestRegisterLazyService_MultipleInstances(t *testing.T) { }, map[string]any{"dsn": "cache-db"}) // Access each instance - main, _ := g.GetServiceAny("db-main") + main, _ := g.GetServiceAny("db_main") if main != "Connection to main-db" { - t.Errorf("unexpected db-main: %v", main) + t.Errorf("unexpected db_main: %v", main) } analytics, _ := g.GetServiceAny("db-analytics") diff --git a/core/deploy/loader/builder.go b/core/deploy/loader/builder.go index e22d011e..646196fd 100644 --- a/core/deploy/loader/builder.go +++ b/core/deploy/loader/builder.go @@ -2,16 +2,15 @@ package loader import ( "fmt" - "log" "os" "path/filepath" "strings" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/deploy/schema" "github.com/primadi/lokstra/core/router" - "github.com/primadi/lokstra/internal" "github.com/primadi/lokstra/serviceapi" ) @@ -572,7 +571,7 @@ func RegisterDefinitionsForRuntime(registry *deploy.GlobalRegistry, config *sche // Check if service already registered with inline factory (e.g., from annotations) if existingEntry := registry.GetLazyServiceEntry(serviceName); existingEntry != nil && existingEntry.IsResolved() { // Service already has inline factory - skip config-based resolution - log.Printf("⏭️ Skipping config resolution for '%s': already resolved with inline factory", serviceName) + logger.LogDebug("⏭️ Skipping config resolution for '%s': already resolved with inline factory", serviceName) continue } @@ -648,9 +647,9 @@ func RegisterDefinitionsForRuntime(registry *deploy.GlobalRegistry, config *sche for serviceName := range publishedServicesMap { _, ok := registry.GetServiceAny(serviceName) if !ok { - log.Printf("⚠️ Warning: Published service '%s' failed to instantiate (dependencies may be missing)", serviceName) + logger.LogWarn("⚠️ Warning: Published service '%s' failed to instantiate (dependencies may be missing)", serviceName) } else { - log.Printf("✅ Instantiated published service: %s", serviceName) + logger.LogInfo("✅ Instantiated published service: %s", serviceName) } } @@ -788,7 +787,7 @@ func RegisterDefinitionsForRuntime(registry *deploy.GlobalRegistry, config *sche serviceInstance, ok = registry.GetServiceAny(serviceName) if !ok || serviceInstance == nil { // Service failed to instantiate - log detailed error - log.Printf("⚠️ Warning: Service '%s' failed to instantiate (dependencies may not be ready), creating lazy router factory instead", serviceName) + logger.LogWarn("⚠️ Warning: Service '%s' failed to instantiate (dependencies may not be ready), creating lazy router factory instead", serviceName) // Create a lazy router factory that will try again when GetRouter is called registry.RegisterRouterFactory(routerName, func() router.Router { @@ -821,7 +820,7 @@ func RegisterDefinitionsForRuntime(registry *deploy.GlobalRegistry, config *sche return router.NewFromService(svcInst, opts) }) - log.Printf("🔧 Registered lazy router factory for '%s' (will instantiate service on-demand)", routerName) + logger.LogDebug("🔧 Registered lazy router factory for '%s' (will instantiate service on-demand)", routerName) continue } @@ -854,7 +853,7 @@ func RegisterDefinitionsForRuntime(registry *deploy.GlobalRegistry, config *sche // Register router instance registry.RegisterRouter(routerName, r) - log.Printf("🔧 Auto-created router '%s' from service '%s' (type: %s, prefix: %s)", routerName, serviceName, serviceDef.Type, finalPrefix) + logger.LogDebug("🔧 Auto-created router '%s' from service '%s' (type: %s, prefix: %s)", routerName, serviceName, serviceDef.Type, finalPrefix) } return nil @@ -982,21 +981,18 @@ func LoadAndBuild(configPaths []string) error { return nil } -// SetupNamedDbPools auto-discovers and sets up named DB pools from config +// LoadNamedDbPoolsFromConfig auto-discovers and sets up named DB pools from config // Requires dbpool-manager service to be already registered -func SetupNamedDbPools() error { +func LoadNamedDbPoolsFromConfig() error { registry := deploy.Global() config := registry.GetDeployConfig() // Check if named-db-pools section exists if len(config.NamedDbPools) == 0 { - internal.AutoCreateDbPoolManager() // No named-db-pools section, skip return nil } - internal.AutoCreateDbPoolManager() - dpm, ok := deploy.Global().GetServiceAny("dbpool-manager") if !ok || dpm == nil { return fmt.Errorf("dbpool-manager service not found in registry") @@ -1088,10 +1084,10 @@ func SetupNamedDbPools() error { } // Set DSN and Schema for poolName - dbPoolManager.SetNamedDsn(poolName, dsn, schema) + dbPoolManager.SetNamedDbPool(poolName, dsn, schema, "") // Create the pool - dbPool, err := dbPoolManager.GetNamedPool(poolName) + dbPool, err := dbPoolManager.GetNamedDbPool(poolName) if err != nil { return fmt.Errorf("failed to create pool '%s': %w", poolName, err) } @@ -1099,7 +1095,7 @@ func SetupNamedDbPools() error { // Register pool as a service registry.RegisterService(poolName, dbPool) - deploy.LogDebug("✅ Registered DB pool: %s (schema: %s)", poolName, schema) + logger.LogDebug("✅ Registered DB pool: %s (schema: %s)", poolName, schema) } return nil diff --git a/core/deploy/loader/resolver/provider_resolver.go b/core/deploy/loader/resolver/provider_resolver.go index 490d0650..88a5c825 100644 --- a/core/deploy/loader/resolver/provider_resolver.go +++ b/core/deploy/loader/resolver/provider_resolver.go @@ -3,6 +3,8 @@ package resolver import ( "fmt" "strings" + + "github.com/primadi/lokstra/common/logger" ) // ResolveSingleValue resolves a single value (not YAML content) @@ -180,7 +182,7 @@ func getNestedConfig(configs map[string]any, key string) any { if !found { // Debug: show what keys are available if i == 0 { - fmt.Printf(" Available keys at root: %v\n", getMapKeys(m)) + logger.LogDebug(" Available keys at root: %v\n", getMapKeys(m)) } return nil } diff --git a/core/deploy/logger.go b/core/deploy/logger.go deleted file mode 100644 index fa66e747..00000000 --- a/core/deploy/logger.go +++ /dev/null @@ -1,78 +0,0 @@ -package deploy - -import ( - "fmt" - "os" - "strings" -) - -// LogLevel represents the logging level -type LogLevel int - -const ( - LogLevelSilent LogLevel = iota - LogLevelError - LogLevelWarn - LogLevelInfo - LogLevelDebug -) - -var ( - currentLogLevel = LogLevelInfo // Default log level -) - -// SetLogLevel sets the global log level for the deploy package -func SetLogLevel(level LogLevel) { - currentLogLevel = level -} - -// GetLogLevel returns the current log level -func GetLogLevel() LogLevel { - return currentLogLevel -} - -// SetLogLevelFromEnv sets log level from environment variable LOKSTRA_LOG_LEVEL -// Supported values: silent, error, warn, info, debug -func SetLogLevelFromEnv() { - envLevel := strings.ToLower(os.Getenv("LOKSTRA_LOG_LEVEL")) - switch envLevel { - case "silent": - currentLogLevel = LogLevelSilent - case "error": - currentLogLevel = LogLevelError - case "warn", "warning": - currentLogLevel = LogLevelWarn - case "info": - currentLogLevel = LogLevelInfo - case "debug": - currentLogLevel = LogLevelDebug - } -} - -// LogDebug prints debug messages if log level is Debug or higher -func LogDebug(format string, args ...any) { - if currentLogLevel >= LogLevelDebug { - fmt.Printf("🐛 "+format+"\n", args...) - } -} - -// LogInfo prints info messages if log level is Info or higher -func LogInfo(format string, args ...any) { - if currentLogLevel >= LogLevelInfo { - fmt.Printf("ℹ️ "+format+"\n", args...) - } -} - -// LogWarn prints warning messages if log level is Warn or higher -func LogWarn(format string, args ...any) { - if currentLogLevel >= LogLevelWarn { - fmt.Printf("⚠️ "+format+"\n", args...) - } -} - -// LogError prints error messages if log level is Error or higher -func LogError(format string, args ...any) { - if currentLogLevel >= LogLevelError { - fmt.Printf("❌ "+format+"\n", args...) - } -} diff --git a/core/deploy/registry.go b/core/deploy/registry.go index fdbbbe88..960d145a 100644 --- a/core/deploy/registry.go +++ b/core/deploy/registry.go @@ -7,6 +7,7 @@ import ( "strings" "sync" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/deploy/schema" "github.com/primadi/lokstra/core/proxy" @@ -215,7 +216,7 @@ func ResetGlobalRegistryForTesting() { // - config: Optional routing configuration (path prefix, middlewares, route overrides) func (g *GlobalRegistry) RegisterRouterServiceType(serviceType string, local, remote any, config *ServiceTypeConfig) { - LogDebug("[RegisterRouterServiceType CALLED] serviceType=%s", serviceType) + logger.LogDebug("[RegisterRouterServiceType CALLED] serviceType=%s", serviceType) g.mu.Lock() defer g.mu.Unlock() @@ -259,7 +260,7 @@ func (g *GlobalRegistry) RegisterRouterServiceType(serviceType string, local, re } // Debug: log metadata before filtering - LogDebug("[RegisterServiceType] %s (before filter): PathPrefix='%s', RouteOverrides=%d", + logger.LogDebug("[RegisterServiceType] %s (before filter): PathPrefix='%s', RouteOverrides=%d", serviceType, metadata.PathPrefix, len(metadata.RouteOverrides)) // Store metadata if any meaningful configuration is provided @@ -272,13 +273,13 @@ func (g *GlobalRegistry) RegisterRouterServiceType(serviceType string, local, re if hasConfig { metadataPtr = metadata // Debug log - LogDebug("[RegisterServiceType] %s: STORED - PathPrefix=%s, RouteOverrides count=%d", + logger.LogDebug("[RegisterServiceType] %s: STORED - PathPrefix=%s, RouteOverrides count=%d", serviceType, metadata.PathPrefix, len(metadata.RouteOverrides)) for methodName, route := range metadata.RouteOverrides { - LogDebug(" - %s: method=%s, path=%s", methodName, route.Method, route.Path) + logger.LogDebug(" - %s: method=%s, path=%s", methodName, route.Method, route.Path) } } else { - LogDebug("[RegisterServiceType] %s: NOT STORED (no meaningful config)", serviceType) + logger.LogDebug("[RegisterServiceType] %s: NOT STORED (no meaningful config)", serviceType) } // Normalize local and remote factories @@ -298,7 +299,7 @@ func (g *GlobalRegistry) RegisterRouterServiceType(serviceType string, local, re Metadata: metadataPtr, } - LogDebug("[RegisterRouterServiceType] %s: registered (local=%v, remote=%v)", + logger.LogDebug("[RegisterRouterServiceType] %s: registered (local=%v, remote=%v)", serviceType, localFactory != nil, remoteFactory != nil) } @@ -420,15 +421,15 @@ func (g *GlobalRegistry) GetServiceMetadata(serviceType string) *ServiceMetadata entry, ok := g.serviceFactories[serviceType] if !ok { - LogDebug("[GetServiceMetadata] serviceType '%s' NOT FOUND", serviceType) + logger.LogDebug("[GetServiceMetadata] serviceType '%s' NOT FOUND", serviceType) return nil } if entry.Metadata != nil { - LogDebug("[GetServiceMetadata] serviceType '%s' FOUND: PathPrefix=%s, RouteOverrides=%d", + logger.LogDebug("[GetServiceMetadata] serviceType '%s' FOUND: PathPrefix=%s, RouteOverrides=%d", serviceType, entry.Metadata.PathPrefix, len(entry.Metadata.RouteOverrides)) } else { - LogDebug("[GetServiceMetadata] serviceType '%s' FOUND but Metadata=nil", serviceType) + logger.LogDebug("[GetServiceMetadata] serviceType '%s' FOUND but Metadata=nil", serviceType) } return entry.Metadata @@ -514,7 +515,7 @@ func (g *GlobalRegistry) flattenAndStoreNested(prefix string, values map[string] } // GetConfig returns a config value -// Supports both flat access ("global-db.dsn") and nested access ("global-db" returns map) +// Supports both flat access ("db_main.dsn") and nested access ("db_main" returns map) // Key lookup is case-insensitive func (g *GlobalRegistry) GetConfig(name string) (any, bool) { g.mu.RLock() @@ -538,7 +539,7 @@ func (g *GlobalRegistry) GetConfig(name string) (any, bool) { // Remove prefix and reconstruct nested structure subKey := after - // Handle further nesting (e.g., "global-db.connection.pool.size") + // Handle further nesting (e.g., "db_main.connection.pool.size") if strings.Contains(subKey, ".") { setNestedValue(nested, subKey, value) } else { @@ -644,7 +645,7 @@ func (g *GlobalRegistry) RegisterRouter(name string, r router.Router) { if routerDef := g.GetRouterDef(name); routerDef != nil { if routerDef.PathPrefix != "" { // Apply PathPrefix from RouterDef (YAML router-definitions) - LogDebug("🔧 Applying PathPrefix '%s' to router '%s' from router-definitions", routerDef.PathPrefix, name) + logger.LogDebug("🔧 Applying PathPrefix '%s' to router '%s' from router-definitions", routerDef.PathPrefix, name) r = r.SetPathPrefix(routerDef.PathPrefix) } @@ -658,7 +659,7 @@ func (g *GlobalRegistry) RegisterRouter(name string, r router.Router) { } } - LogDebug("🔧 RegisterRouter: storing router '%s' at %p (type=%T)", name, r, r) + logger.LogDebug("🔧 RegisterRouter: storing router '%s' at %p (type=%T)", name, r, r) g.routerInstances.Store(name, r) } @@ -673,13 +674,13 @@ func (g *GlobalRegistry) RegisterRouter(name string, r router.Router) { // return emailService.GetRouter() // }) func (g *GlobalRegistry) RegisterRouterFactory(name string, factory func() router.Router) { - LogDebug("🔧 RegisterRouterFactory: registering lazy router '%s'", name) + logger.LogDebug("🔧 RegisterRouterFactory: registering lazy router '%s'", name) g.lazyRouterFactories.Store(name, factory) } // instantiateLazyRouters creates router instances from registered factories // func (g *GlobalRegistry) InstantiateLazyRouters() { -// LogDebug("🔧 InstantiateLazyRouters: starting lazy router instantiation") +// logger.LogDebug("🔧 InstantiateLazyRouters: starting lazy router instantiation") // count := 0 // g.lazyRouterFactories.Range(func(nameAny, factoryAny any) bool { // name := nameAny.(string) @@ -687,18 +688,18 @@ func (g *GlobalRegistry) RegisterRouterFactory(name string, factory func() route // // Skip if already instantiated // if _, exists := g.routerInstances.Load(name); exists { -// LogDebug("🔧 Lazy router '%s': already instantiated, skipping", name) +// logger.LogDebug("🔧 Lazy router '%s': already instantiated, skipping", name) // return true // } -// LogDebug("🔧 Instantiating lazy router: '%s'", name) +// logger.LogDebug("🔧 Instantiating lazy router: '%s'", name) // r := factory() -// LogDebug("🔧 Lazy router '%s': factory returned %T, registering", name, r) +// logger.LogDebug("🔧 Lazy router '%s': factory returned %T, registering", name, r) // g.RegisterRouter(name, r) // count++ // return true // }) -// LogDebug("🔧 InstantiateLazyRouters: completed, instantiated %d routers", count) +// logger.LogDebug("🔧 InstantiateLazyRouters: completed, instantiated %d routers", count) // } // GetRouter retrieves a router instance by name @@ -707,16 +708,16 @@ func (g *GlobalRegistry) GetRouter(name string) router.Router { // Check if already instantiated if v, ok := g.routerInstances.Load(name); ok { r := v.(router.Router) - LogDebug("🔍 GetRouter('%s'): found router %p (type=%T)", name, r, r) + logger.LogDebug("🔍 GetRouter('%s'): found router %p (type=%T)", name, r, r) return r } // Check lazy router factories and instantiate if found if factoryAny, ok := g.lazyRouterFactories.Load(name); ok { factory := factoryAny.(func() router.Router) - LogDebug("🔍 GetRouter('%s'): found lazy factory, instantiating...", name) + logger.LogDebug("🔍 GetRouter('%s'): found lazy factory, instantiating...", name) r := factory() - LogDebug("🔍 GetRouter('%s'): lazy factory returned %T, registering", name, r) + logger.LogDebug("🔍 GetRouter('%s'): lazy factory returned %T, registering", name, r) g.RegisterRouter(name, r) return r } @@ -729,48 +730,48 @@ func (g *GlobalRegistry) GetRouter(name string) router.Router { // Check if service exists (lazy or instance) if g.HasService(serviceName) { // Try to instantiate service (this will trigger router creation in the service factory) - LogDebug("🔍 GetRouter('%s'): service '%s' exists, attempting to instantiate...", name, serviceName) + logger.LogDebug("🔍 GetRouter('%s'): service '%s' exists, attempting to instantiate...", name, serviceName) // Get service instance (this will instantiate if lazy) serviceInstance, ok := g.GetServiceAny(serviceName) - LogDebug("🔍 GetRouter('%s'): GetServiceAny returned ok=%v, instance=%v", name, ok, serviceInstance != nil) + logger.LogDebug("🔍 GetRouter('%s'): GetServiceAny returned ok=%v, instance=%v", name, ok, serviceInstance != nil) if ok && serviceInstance != nil { - LogDebug("🔍 GetRouter('%s'): service '%s' instantiated successfully, checking router again...", name, serviceName) + logger.LogDebug("🔍 GetRouter('%s'): service '%s' instantiated successfully, checking router again...", name, serviceName) // Check if router was created during service instantiation if v, ok := g.routerInstances.Load(name); ok { r := v.(router.Router) - LogDebug("🔍 GetRouter('%s'): router created during service instantiation", name) + logger.LogDebug("🔍 GetRouter('%s'): router created during service instantiation", name) return r } // Service instantiated but router not found - may need to create manually - LogDebug("🔍 GetRouter('%s'): service instantiated but router not auto-created, checking metadata...", name) + logger.LogDebug("🔍 GetRouter('%s'): service instantiated but router not auto-created, checking metadata...", name) // Get service definition serviceDef := g.GetDeferredServiceDef(serviceName) if serviceDef == nil { - LogDebug("🔍 GetRouter('%s'): service definition not found", name) + logger.LogDebug("🔍 GetRouter('%s'): service definition not found", name) return nil } // Get service metadata metadata := g.GetServiceMetadata(serviceDef.Type) if metadata == nil { - LogDebug("🔍 GetRouter('%s'): service metadata not found", name) + logger.LogDebug("🔍 GetRouter('%s'): service metadata not found", name) return nil } // Check if service has router config hasRouterConfig := len(metadata.RouteOverrides) > 0 || metadata.PathPrefix != "" if !hasRouterConfig { - LogDebug("🔍 GetRouter('%s'): service has no router configuration", name) + logger.LogDebug("🔍 GetRouter('%s'): service has no router configuration", name) return nil } // Create router from service using autogen - LogDebug("🔍 GetRouter('%s'): creating router from service instance", name) + logger.LogDebug("🔍 GetRouter('%s'): creating router from service instance", name) // Get RouterDef if exists routerDef := g.GetRouterDef(name) @@ -807,13 +808,13 @@ func (g *GlobalRegistry) GetRouter(name string) router.Router { // Create router using NewFromService r := router.NewFromService(serviceInstance, opts) g.RegisterRouter(name, r) - LogDebug("🔍 GetRouter('%s'): router created and registered", name) + logger.LogDebug("🔍 GetRouter('%s'): router created and registered", name) return r } } } - LogDebug("🔍 GetRouter('%s'): NOT FOUND", name) + logger.LogDebug("🔍 GetRouter('%s'): NOT FOUND", name) return nil } @@ -833,10 +834,7 @@ func (g *GlobalRegistry) RegisterService(name string, service any) { panic(fmt.Sprintf("service %s already registered", name)) } g.serviceInstances.Store(name, service) - - if GetLogLevel() >= LogLevelInfo { - LogDebug("ℹ️ Registered service instance: '%s'\n", name) - } + logger.LogDebug("ℹ️ Registered service instance: '%s'\n", name) } // RegisterLazyService registers a lazy service factory that will be instantiated on first access. @@ -852,7 +850,7 @@ func (g *GlobalRegistry) RegisterService(name string, service any) { // Example with config: // // // Multiple DB instances with different DSN -// lokstra_registry.RegisterLazyService("db-main", func(cfg map[string]any) any { +// lokstra_registry.RegisterLazyService("db_main", func(cfg map[string]any) any { // return NewDB(cfg["dsn"].(string)) // }, map[string]any{"dsn": "main-dsn"}) // @@ -931,7 +929,7 @@ func (g *GlobalRegistry) RegisterLazyService(name string, factory any, config ma deps[dep] = dep } } - LogDebug("📦 RegisterLazyService '%s': extracted %d dependencies from config: %v", name, len(deps), dependsOn) + logger.LogDebug("📦 RegisterLazyService '%s': extracted %d dependencies from config: %v", name, len(deps), dependsOn) } } // Delegate to RegisterLazyServiceWithDeps g.RegisterLazyServiceWithDeps(name, factory, deps, config) @@ -1131,7 +1129,7 @@ func (g *GlobalRegistry) RegisterLazyServiceWithDeps(name string, factory any, d resolved: true, // Already has Factory function } - LogDebug("📦 RegisterLazyServiceWithDeps '%s': stored with %d dependencies: %v", name, len(deps), deps) + logger.LogDebug("📦 RegisterLazyServiceWithDeps '%s': stored with %d dependencies: %v", name, len(deps), deps) g.lazyServiceFactories.Store(name, entry) g.lazyServiceOnce.Store(name, &sync.Once{}) } @@ -1173,27 +1171,27 @@ func (g *GlobalRegistry) GetServiceAny(name string) (any, bool) { // getServiceAnyWithStack is internal version with circular dependency detection func (g *GlobalRegistry) getServiceAnyWithStack(name string, resolutionStack []string) (any, bool) { - LogDebug("🔍 GetServiceAny('%s'): starting resolution, stack=%v", name, resolutionStack) + logger.LogDebug("🔍 GetServiceAny('%s'): starting resolution, stack=%v", name, resolutionStack) // Handle @ prefix - resolve actual service name from config // Example: "@store.order-repository" reads config key "store.order-repository" // and gets the actual service name to inject if after, ok := strings.CutPrefix(name, "@"); ok { - LogDebug("🔍 GetServiceAny('%s'): has @ prefix, resolving from config key '%s'", name, after) + logger.LogDebug("🔍 GetServiceAny('%s'): has @ prefix, resolving from config key '%s'", name, after) configKey := after configValue, ok := g.GetConfig(configKey) if !ok { - LogDebug("🔍 GetServiceAny('%s'): config key '%s' NOT FOUND", name, configKey) + logger.LogDebug("🔍 GetServiceAny('%s'): config key '%s' NOT FOUND", name, configKey) return nil, false } actualServiceName, ok := configValue.(string) if !ok || actualServiceName == "" { - LogDebug("🔍 GetServiceAny('%s'): config value is not string or empty: %v", name, configValue) + logger.LogDebug("🔍 GetServiceAny('%s'): config value is not string or empty: %v", name, configValue) return nil, false } - LogDebug("🔍 GetServiceAny('%s'): resolved to actual service '%s'", name, actualServiceName) + logger.LogDebug("🔍 GetServiceAny('%s'): resolved to actual service '%s'", name, actualServiceName) // Recursively resolve the actual service (add to stack to detect circular deps) return g.getServiceAnyWithStack(actualServiceName, append(resolutionStack, name)) } @@ -1209,7 +1207,7 @@ func (g *GlobalRegistry) getServiceAnyWithStack(name string, resolutionStack []s // Check eager registry first if svc, ok := g.serviceInstances.Load(name); ok { - LogDebug("🔍 GetServiceAny('%s'): found in eager registry (already instantiated)", name) + logger.LogDebug("🔍 GetServiceAny('%s'): found in eager registry (already instantiated)", name) return svc, true } @@ -1219,20 +1217,20 @@ func (g *GlobalRegistry) getServiceAnyWithStack(name string, resolutionStack []s // Check lazy registry and create if needed onceAny, hasOnce := g.lazyServiceOnce.Load(name) if !hasOnce { - LogDebug("🔍 GetServiceAny('%s'): NOT in lazyServiceOnce, checking lazyServiceFactories...", name) + logger.LogDebug("🔍 GetServiceAny('%s'): NOT in lazyServiceOnce, checking lazyServiceFactories...", name) // Not in lazy registry - check if in lazyServiceFactories with unresolved entry if entryAny, exists := g.lazyServiceFactories.Load(name); exists { - LogDebug("🔍 GetServiceAny('%s'): found in lazyServiceFactories", name) + logger.LogDebug("🔍 GetServiceAny('%s'): found in lazyServiceFactories", name) entry := entryAny.(*LazyServiceEntry) // If unresolved (Phase 1 - from registerDeferredService), resolve it now if !entry.resolved { - LogDebug("🔍 GetServiceAny('%s'): entry UNRESOLVED, resolving factory type '%s'...", name, entry.FactoryType) + logger.LogDebug("🔍 GetServiceAny('%s'): entry UNRESOLVED, resolving factory type '%s'...", name, entry.FactoryType) // Get factory for the service type factory := g.GetServiceFactory(entry.FactoryType, true) // true = local factory if factory == nil { - LogDebug("🔍 GetServiceAny('%s'): factory '%s' NOT FOUND!", name, entry.FactoryType) + logger.LogDebug("🔍 GetServiceAny('%s'): factory '%s' NOT FOUND!", name, entry.FactoryType) panic(fmt.Sprintf("service factory '%s' not registered for service '%s'", entry.FactoryType, name)) } @@ -1243,7 +1241,7 @@ func (g *GlobalRegistry) getServiceAnyWithStack(name string, resolutionStack []s // Create sync.Once if not exists (handles case where entry was resolved externally) if _, hasOnceAlready := g.lazyServiceOnce.Load(name); !hasOnceAlready { - LogDebug("🔍 GetServiceAny('%s'): creating sync.Once (entry was resolved=%v)", name, entry.resolved) + logger.LogDebug("🔍 GetServiceAny('%s'): creating sync.Once (entry was resolved=%v)", name, entry.resolved) g.lazyServiceOnce.Store(name, &sync.Once{}) } @@ -1258,7 +1256,7 @@ func (g *GlobalRegistry) getServiceAnyWithStack(name string, resolutionStack []s if hasFactory && factoryEntry.Local != nil { // Auto-register as lazy service with default config - LogDebug("🔧 Auto-registering service '%s' from factory type '%s' (default config)", name, name) + logger.LogDebug("🔧 Auto-registering service '%s' from factory type '%s' (default config)", name, name) entry := &LazyServiceEntry{ FactoryType: name, Factory: factoryEntry.Local, @@ -1275,11 +1273,11 @@ func (g *GlobalRegistry) getServiceAnyWithStack(name string, resolutionStack []s } if !hasOnce { - LogDebug("🔍 GetServiceAny('%s'): NOT FOUND in any registry, returning false", name) + logger.LogDebug("🔍 GetServiceAny('%s'): NOT FOUND in any registry, returning false", name) return nil, false } } else { - LogDebug("🔍 GetServiceAny('%s'): found in lazyServiceOnce, will instantiate", name) + logger.LogDebug("🔍 GetServiceAny('%s'): found in lazyServiceOnce, will instantiate", name) } once := onceAny.(*sync.Once) @@ -1308,34 +1306,34 @@ func (g *GlobalRegistry) getServiceAnyWithStack(name string, resolutionStack []s // Resolve dependencies if specified var resolvedDeps map[string]any if len(entry.Deps) > 0 { - LogDebug("📦 Service '%s': resolving %d dependencies: %v", name, len(entry.Deps), entry.Deps) + logger.LogDebug("📦 Service '%s': resolving %d dependencies: %v", name, len(entry.Deps), entry.Deps) resolvedDeps = make(map[string]any, len(entry.Deps)) for factoryKey, serviceName := range entry.Deps { // Recursively resolve dependency with circular detection // @ prefix is handled automatically by getServiceAnyWithStack - LogDebug("📦 Service '%s': resolving dependency '%s' -> '%s'", name, factoryKey, serviceName) + logger.LogDebug("📦 Service '%s': resolving dependency '%s' -> '%s'", name, factoryKey, serviceName) depSvc, ok := g.getServiceAnyWithStack(serviceName, newStack) if !ok { panic(fmt.Sprintf("lazy service %s: dependency %s not found", name, serviceName)) } - LogDebug("📦 Service '%s': dependency '%s' resolved to: %T", name, factoryKey, depSvc) + logger.LogDebug("📦 Service '%s': dependency '%s' resolved to: %T", name, factoryKey, depSvc) // Use factoryKey (may include @ prefix) as key for factory lookup resolvedDeps[factoryKey] = depSvc } - LogDebug("📦 Service '%s': all dependencies resolved, calling factory", name) + logger.LogDebug("📦 Service '%s': all dependencies resolved, calling factory", name) } else { - LogDebug("📦 Service '%s': no dependencies, calling factory directly", name) + logger.LogDebug("📦 Service '%s': no dependencies, calling factory directly", name) } // Call factory with resolved deps or nil // Check if this is a remote service (has "remote" in config) if _, isRemote := entry.Config["remote"]; isRemote { - LogDebug("📦 Creating remote service wrapper: '%s'", name) + logger.LogDebug("📦 Creating remote service wrapper: '%s'", name) } else { - LogDebug("📦 Creating service instance: '%s'", name) + logger.LogDebug("📦 Creating service instance: '%s'", name) } instance := entry.Factory(resolvedDeps, entry.Config) - LogDebug("📦 Service '%s' created: instance=%p, type=%T", name, instance, instance) + logger.LogDebug("📦 Service '%s' created: instance=%p, type=%T", name, instance, instance) g.serviceInstances.Store(name, instance) }) @@ -1377,7 +1375,7 @@ func (g *GlobalRegistry) MergeRegistryServicesToConfig(config *schema.DeployConf // Resolved entries don't need to be merged to config because they already have // the factory function ready to instantiate - no factory type lookup needed if entry.resolved { - LogDebug("⏭️ Skipping merge for '%s': already resolved with inline factory", serviceName) + logger.LogDebug("⏭️ Skipping merge for '%s': already resolved with inline factory", serviceName) return true // continue iteration } @@ -1403,10 +1401,10 @@ func (g *GlobalRegistry) MergeRegistryServicesToConfig(config *schema.DeployConf // Returns the definition if found in lazyServiceFactories, or nil if not found. // This is primarily used by wrapper functions that need access to service metadata. func (g *GlobalRegistry) GetDeferredServiceDef(name string) *schema.ServiceDef { - LogDebug("[GetDeferredServiceDef] looking for '%s'", name) + logger.LogDebug("[GetDeferredServiceDef] looking for '%s'", name) if entryAny, ok := g.lazyServiceFactories.Load(name); ok { entry := entryAny.(*LazyServiceEntry) - LogDebug("[GetDeferredServiceDef] FOUND '%s': Type=%s", name, entry.FactoryType) + logger.LogDebug("[GetDeferredServiceDef] FOUND '%s': Type=%s", name, entry.FactoryType) // Convert Deps map to DependsOn slice dependsOn := make([]string, 0, len(entry.Deps)) @@ -1421,7 +1419,7 @@ func (g *GlobalRegistry) GetDeferredServiceDef(name string) *schema.ServiceDef { Config: entry.Config, } } - LogDebug("[GetDeferredServiceDef] NOT FOUND '%s'", name) + logger.LogDebug("[GetDeferredServiceDef] NOT FOUND '%s'", name) return nil } @@ -1431,10 +1429,10 @@ func (g *GlobalRegistry) GetDeferredServiceDef(name string) *schema.ServiceDef { // func (g *GlobalRegistry) autoRegisterLazyService(name string, def *schema.ServiceDef) { // // Get current deployment context // currentKey := g.GetCurrentCompositeKey() -// LogDebug("[autoRegisterLazyService] service '%s', currentKey='%s'", name, currentKey) +// logger.LogDebug("[autoRegisterLazyService] service '%s', currentKey='%s'", name, currentKey) // if currentKey == "" { // // No current context - default to LOCAL -// LogDebug("[autoRegisterLazyService] No currentKey - registering '%s' as LOCAL", name) +// logger.LogDebug("[autoRegisterLazyService] No currentKey - registering '%s' as LOCAL", name) // g.autoRegisterLocalService(name, def) // return // } @@ -1443,23 +1441,23 @@ func (g *GlobalRegistry) GetDeferredServiceDef(name string) *schema.ServiceDef { // currentServerTopo, ok := g.GetServerTopology(currentKey) // if !ok { // // No topology found - default to LOCAL -// LogDebug("[autoRegisterLazyService] No topology found for '%s' - registering '%s' as LOCAL", currentKey, name) +// logger.LogDebug("[autoRegisterLazyService] No topology found for '%s' - registering '%s' as LOCAL", currentKey, name) // g.autoRegisterLocalService(name, def) // return // } // // Check if service is published on another server (REMOTE) // remoteBaseURL, isRemote := currentServerTopo.RemoteServices[name] -// LogDebug("[autoRegisterLazyService] service '%s': isRemote=%v, remoteBaseURL='%s'", name, isRemote, remoteBaseURL) +// logger.LogDebug("[autoRegisterLazyService] service '%s': isRemote=%v, remoteBaseURL='%s'", name, isRemote, remoteBaseURL) // if isRemote { // // Register as REMOTE service (HTTP proxy) -// LogDebug("[autoRegisterLazyService] Registering '%s' as REMOTE -> %s", name, remoteBaseURL) +// logger.LogDebug("[autoRegisterLazyService] Registering '%s' as REMOTE -> %s", name, remoteBaseURL) // g.AutoRegisterRemoteService(name, def, remoteBaseURL) // return // } // // Not remote - register as LOCAL -// LogDebug("[autoRegisterLazyService] Registering '%s' as LOCAL", name) +// logger.LogDebug("[autoRegisterLazyService] Registering '%s' as LOCAL", name) // g.autoRegisterLocalService(name, def) // } @@ -1500,14 +1498,14 @@ func (g *GlobalRegistry) GetDeferredServiceDef(name string) *schema.ServiceDef { // } // // Call original factory -// LogDebug("📦 Creating service instance: '%s' (type: %s)", name, def.Type) +// logger.LogDebug("📦 Creating service instance: '%s' (type: %s)", name, def.Type) // return factory(lazyDeps, cfg) // }, deps, def.Config) // } // AutoRegisterRemoteService registers a service as REMOTE (HTTP proxy) func (g *GlobalRegistry) AutoRegisterRemoteService(name string, def *schema.ServiceDef, remoteBaseURL string) { - LogDebug("🌐 Creating remote service proxy: '%s' -> %s", name, remoteBaseURL) + logger.LogDebug("🌐 Creating remote service proxy: '%s' -> %s", name, remoteBaseURL) // Get remote factory factory := g.GetServiceFactory(def.Type, false) // false = remote factory @@ -1545,7 +1543,7 @@ func (g *GlobalRegistry) AutoRegisterRemoteService(name string, def *schema.Serv } else { // No metadata - service must have explicit route mappings // Create empty proxy (routes must be added manually) - LogDebug("⚠️ Remote service '%s' has no route metadata - proxy created with empty routes", name) + logger.LogDebug("⚠️ Remote service '%s' has no route metadata - proxy created with empty routes", name) proxyService = proxy.NewService(remoteBaseURL, make(map[string]proxy.RouteMapping)) } @@ -1927,11 +1925,11 @@ func (g *GlobalRegistry) ShutdownServices() { item := snapshot[i] if shutdownable, ok := item.svc.(Shutdownable); ok { if err := shutdownable.Shutdown(); err != nil { - fmt.Printf("[ShutdownServices] Failed to shutdown service %s: %v\n", item.name, err) + logger.LogInfo("[ShutdownServices] Failed to shutdown service %s: %v\n", item.name, err) } else { - fmt.Printf("[ShutdownServices] Successfully shutdown service: %s\n", item.name) + logger.LogInfo("[ShutdownServices] Successfully shutdown service: %s\n", item.name) } } } - fmt.Println("[ShutdownServices] Gracefully shutdown all services.") + logger.LogInfo("[ShutdownServices] Gracefully shutdown all services.") } diff --git a/core/proxy/service.go b/core/proxy/service.go index b745962a..bf60244e 100644 --- a/core/proxy/service.go +++ b/core/proxy/service.go @@ -2,12 +2,12 @@ package proxy import ( "fmt" - "log" "reflect" "strings" "time" "github.com/primadi/lokstra/api_client" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/request" ) @@ -41,7 +41,7 @@ func NewService(baseURL string, routeMap map[string]RouteMapping) *Service { Timeout: 30 * time.Second, } - log.Printf("🌐 Created remote service proxy: %s with %d routes", baseURL, len(routeMap)) + logger.LogDebug("🌐 Created remote service proxy: %s with %d routes", baseURL, len(routeMap)) return &Service{ client: client, @@ -89,7 +89,7 @@ func Call(s *Service, methodName string, params ...any) error { // Replace path parameters from context path := s.replacePathParameters(pathTemplate, ctx, structParam) - log.Printf("🌐 proxy.Call: %s → %s %s", methodName, httpMethod, s.baseURL+path) + logger.LogDebug("🌐 proxy.Call: %s → %s %s", methodName, httpMethod, s.baseURL+path) // Build request options opts := s.buildRequestOptions(httpMethod, structParam, ctx) @@ -97,11 +97,11 @@ func Call(s *Service, methodName string, params ...any) error { // Make HTTP call - use empty response type for error-only handlers _, err = api_client.FetchAndCast[any](s.client, path, opts...) if err != nil { - log.Printf("❌ proxy.Call error: %v", err) + logger.LogError("❌ proxy.Call error: %v", err) return err } - log.Printf("✅ proxy.Call success") + logger.LogDebug("✅ proxy.Call success") return nil } @@ -138,7 +138,7 @@ func CallWithData[T any](s *Service, methodName string, params ...any) (T, error // Replace path parameters from context path := s.replacePathParameters(pathTemplate, ctx, structParam) - log.Printf("🌐 proxy.CallWithData: %s → %s %s", methodName, httpMethod, s.baseURL+path) + logger.LogDebug("🌐 proxy.CallWithData: %s → %s %s", methodName, httpMethod, s.baseURL+path) // Build request options opts := s.buildRequestOptions(httpMethod, structParam, ctx) @@ -146,11 +146,11 @@ func CallWithData[T any](s *Service, methodName string, params ...any) (T, error // Make HTTP call and get typed response data, err := api_client.FetchAndCast[T](s.client, path, opts...) if err != nil { - log.Printf("❌ proxy.CallWithData error: %v", err) + logger.LogError("❌ proxy.CallWithData error: %v", err) return zero, err } - log.Printf("✅ proxy.CallWithData success: %T", data) + logger.LogDebug("✅ proxy.CallWithData success: %T", data) return data, nil } diff --git a/core/request/context.go b/core/request/context.go index 760d8e1f..43452b00 100644 --- a/core/request/context.go +++ b/core/request/context.go @@ -114,3 +114,22 @@ func (c *Context) GetContextValue(key string) any { } return c.Context.Value(contextKey(key)) } + +// StatusCode returns the HTTP status code from the response +// It checks multiple sources in order of priority: +// 1. Writer's status code (if manually written) +// 2. Response helper's status code (if set via Api/Resp) +// 3. Default 200 OK +func (c *Context) StatusCode() int { + // First check writer's status code (manual writes) + ret := c.W.StatusCode() + if ret == 0 { + // Then check response helper's status code + ret = c.Resp.RespStatusCode + } + if ret == 0 { + // Default to 200 OK + ret = 200 + } + return ret +} diff --git a/core/router/router_impl.go b/core/router/router_impl.go index f972df6b..0daa81ea 100644 --- a/core/router/router_impl.go +++ b/core/router/router_impl.go @@ -7,6 +7,7 @@ import ( "strings" "sync" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/core/route" "github.com/primadi/lokstra/core/router/engine" @@ -448,7 +449,7 @@ func (r *routerImpl) PrintRoutes() { if routerNameDisplay == "" { routerNameDisplay = r.name } - fmt.Printf("[%s] %s %s -> %s%s\n", routerNameDisplay, rt.Method, rt.FullPath, rt.Name, mwDescr) + logger.LogInfo("[%s] %s %s -> %s%s", routerNameDisplay, rt.Method, rt.FullPath, rt.Name, mwDescr) }) } diff --git a/core/server/server.go b/core/server/server.go index 3518ef5b..77d1e70e 100644 --- a/core/server/server.go +++ b/core/server/server.go @@ -9,6 +9,7 @@ import ( "syscall" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/app" ) @@ -45,16 +46,16 @@ func New(name string, apps ...*app.App) *Server { // Print server start information, including each app's details func (s *Server) PrintStartInfo() { s.build() - fmt.Printf("Server '%s' starting with %d app(s):\n", s.Name, len(s.Apps)) + logger.LogInfo("Server '%s' starting with %d app(s):\n", s.Name, len(s.Apps)) for _, a := range s.Apps { a.PrintStartInfo() } - fmt.Println("Press CTRL+C to stop the server...") + logger.LogInfo("Press CTRL+C to stop the server...") } func (s *Server) AddApp(a *app.App) { if s.built { - panic("Cannot add app after server is built") + logger.LogPanic("Cannot add app after server is built") } s.Apps = append(s.Apps, a) } @@ -147,10 +148,10 @@ func (s *Server) shutdown(timeout time.Duration) error { go func(a *app.App) { defer wg.Done() if err := a.Shutdown(timeout); err != nil { - fmt.Printf("Failed to shutdown app '%s': %v\n", a.GetName(), err) + logger.LogError("Failed to shutdown app '%s': %v\n", a.GetName(), err) errCh <- fmt.Errorf("app '%s': %w", a.GetName(), err) } else { - fmt.Printf("App '%s' has been gracefully shutdown.\n", a.GetName()) + logger.LogInfo("App '%s' has been gracefully shutdown.\n", a.GetName()) } }(ap) } @@ -192,7 +193,7 @@ func (s *Server) Run(timeout time.Duration) error { select { case sig := <-stop: - fmt.Println("Received shutdown signal:", sig) + logger.LogInfo("Received shutdown signal:", sig) if err := s.shutdown(timeout); err != nil { return fmt.Errorf("shutdown error: %w", err) } diff --git a/core/service/lazy_load.go b/core/service/lazy_load.go index 0e4cbcb2..836d694c 100644 --- a/core/service/lazy_load.go +++ b/core/service/lazy_load.go @@ -1,9 +1,9 @@ package service import ( - "log" "sync" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/proxy" "github.com/primadi/lokstra/internal/registry" @@ -77,7 +77,7 @@ func (l *Cached[T]) Get() T { // Log when service is loaded if l.serviceName != "" && !utils.IsNil(l.cache) { - log.Printf("🔧 Lazy loaded service: '%s'", l.serviceName) + logger.LogDebug("🔧 Lazy loaded service: '%s'", l.serviceName) } } else { // No loader provided - return zero value diff --git a/docs/00-introduction/examples/annotations/README.md b/docs/00-introduction/examples/annotations/README.md index 1e87fbfd..55bc10a1 100644 --- a/docs/00-introduction/examples/annotations/README.md +++ b/docs/00-introduction/examples/annotations/README.md @@ -259,7 +259,7 @@ func (s *MyService) Init() error { } // Pre-load data, setup connections, etc. - log.Println("Service initialized") + logger.LogInfo("Service initialized") return nil } ``` diff --git a/docs/00-introduction/examples/annotations/init_example.go b/docs/00-introduction/examples/annotations/init_example.go index faf26838..32d6f7ed 100644 --- a/docs/00-introduction/examples/annotations/init_example.go +++ b/docs/00-introduction/examples/annotations/init_example.go @@ -2,7 +2,8 @@ package application import ( "fmt" - "log" + + "github.com/primadi/lokstra/common/logger" ) // Example with Init() method @@ -33,7 +34,7 @@ func (c *CacheManager) Init() error { return fmt.Errorf("cache TTL must be positive, got %d", c.TTLSeconds) } - log.Printf("✅ CacheManager initialized: max_size=%d, ttl=%ds", c.MaxSize, c.TTLSeconds) + logger.LogInfo("✅ CacheManager initialized: max_size=%d, ttl=%ds", c.MaxSize, c.TTLSeconds) return nil } diff --git a/docs/00-introduction/examples/annotations/router_with_init_example.go b/docs/00-introduction/examples/annotations/router_with_init_example.go index 68b69bdf..3ba8a393 100644 --- a/docs/00-introduction/examples/annotations/router_with_init_example.go +++ b/docs/00-introduction/examples/annotations/router_with_init_example.go @@ -2,7 +2,8 @@ package application import ( "fmt" - "log" + + "github.com/primadi/lokstra/common/logger" ) // Example RouterService with Init() method @@ -30,7 +31,7 @@ func (s *ProductAPIService) Init() error { } // Pre-load data if needed - log.Printf("✅ ProductAPIService initialized: max_items=%d", s.MaxItems) + logger.LogInfo("✅ ProductAPIService initialized: max_items=%d", s.MaxItems) return nil } diff --git a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/config/deployment.yaml b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/config/deployment.yaml index 3a37edeb..1fe7395c 100644 --- a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/config/deployment.yaml +++ b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/config/deployment.yaml @@ -3,7 +3,7 @@ configs: server: ${SERVER:} db_dsn: ${DB_DSN:postgres://localhost/enterprise_router_service} - global-db: + db_main: dsn: ${@cfg:db_dsn} schema: "public" diff --git a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/main.go b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/main.go index 75d40482..87bcccb1 100644 --- a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/main.go +++ b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/main.go @@ -1,41 +1,39 @@ package main import ( - "fmt" - "github.com/primadi/lokstra" - "github.com/primadi/lokstra/core/deploy" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/lokstra_registry" ) func main() { lokstra.Bootstrap() - fmt.Println("") - fmt.Println("╔═══════════════════════════════════════════════╗") - fmt.Println("║ LOKSTRA ENTERPRISE MODULAR TEMPLATE ║") - fmt.Println("║ Domain-Driven Design with Bounded Contexts ║") - fmt.Println("╚═══════════════════════════════════════════════╝") - fmt.Println("") + logger.LogInfo("") + logger.LogInfo("╔═══════════════════════════════════════════════╗") + logger.LogInfo("║ LOKSTRA ENTERPRISE MODULAR TEMPLATE ║") + logger.LogInfo("║ Domain-Driven Design with Bounded Contexts ║") + logger.LogInfo("╚═══════════════════════════════════════════════╝") + logger.LogInfo("") - deploy.SetLogLevelFromEnv() + logger.SetLogLevelFromEnv() lokstra.LoadConfigFromFolder("config") - dsn := lokstra_registry.GetConfig("global-db.dsn", "") - schema := lokstra_registry.GetConfig("global-db.schema", "public") + dsn := lokstra_registry.GetConfig("db_main.dsn", "") + schema := lokstra_registry.GetConfig("db_main.schema", "public") // Just to show that we can access nested config values - fmt.Printf("Using Global DB DSN: %s, Schema: %s\n", dsn, schema) + logger.LogInfo("Using Global DB DSN: %s, Schema: %s", dsn, schema) type dbConfig struct { DSN string `json:"dsn"` Schema string `json:"schema"` } - fullDBConfig := lokstra_registry.GetConfig("global-db", dbConfig{}) + fullDBConfig := lokstra_registry.GetConfig("db_main", dbConfig{}) // Print full nested config struct - fmt.Printf("Full Global DB Config: %+v\n", fullDBConfig) + logger.LogInfo("Full Global DB Config: %+v", fullDBConfig) // 1. Register service types from all modules registerServiceTypes() @@ -44,7 +42,7 @@ func main() { registerMiddlewareTypes() // 3. Run server from config folder - if err := lokstra.InitAndRunServer(); err != nil { + if err := lokstra.RunConfiguredServer(); err != nil { panic(err) } } diff --git a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/register.go b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/register.go index faa3db8d..0f8fbebc 100644 --- a/docs/00-introduction/examples/full-framework/01_enterprise_router_service/register.go +++ b/docs/00-introduction/examples/full-framework/01_enterprise_router_service/register.go @@ -1,9 +1,8 @@ package main import ( - "log" - "github.com/google/uuid" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/middleware/recovery" @@ -30,7 +29,7 @@ func requestLoggerFactory() request.HandlerFunc { return func(ctx *request.Context) error { // Before request reqID := uuid.New().String() - log.Printf("→ [%s] %s %s", reqID, ctx.R.Method, ctx.R.URL.Path) + logger.LogInfo("→ [%s] %s %s", reqID, ctx.R.Method, ctx.R.URL.Path) ctx.Set("request_id", reqID) // Process request @@ -38,9 +37,9 @@ func requestLoggerFactory() request.HandlerFunc { // After request if err != nil { - log.Printf("← [%s] ERROR: %v", reqID, err) + logger.LogError("← [%s] ERROR: %v", reqID, err) } else { - log.Printf("← [%s] SUCCESS (status: %d)", reqID, ctx.Resp.RespStatusCode) + logger.LogInfo("← [%s] SUCCESS (status: %d)", reqID, ctx.Resp.RespStatusCode) } return err diff --git a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/main.go b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/main.go index bf404452..6f08779c 100644 --- a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/main.go +++ b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/main.go @@ -1,9 +1,8 @@ package main import ( - "fmt" - - "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra" + "github.com/primadi/lokstra/common/logger" ) func main() { @@ -12,12 +11,11 @@ func main() { // "Server to run (monolith.api-server or microservice.user-server, microservice.user-server, or microservice.order-server)") // flag.Parse() - fmt.Println("") - fmt.Println("╔═════════════════════════════════════════════╗") - fmt.Println("║ LOKSTRA MULTI-DEPLOYMENT DEMO ║") - fmt.Println("╚═════════════════════════════════════════════╝") - fmt.Println("") - + logger.LogInfo("") + logger.LogInfo("╔═════════════════════════════════════════════╗") + logger.LogInfo("║ LOKSTRA MULTI-DEPLOYMENT DEMO ║") + logger.LogInfo("╚═════════════════════════════════════════════╝") + logger.LogInfo("") // 1. Register service types registerServiceTypes() @@ -25,5 +23,11 @@ func main() { registerMiddlewareTypes() // 3. RunServerFromConfig - lokstra_registry.RunServerFromConfig() + if err := lokstra.LoadConfig(); err != nil { + logger.LogPanic("❌ Failed to load config:", err) + } + + if err := lokstra.RunConfiguredServer(); err != nil { + logger.LogPanic("❌ Failed to run server:", err) + } } diff --git a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/register.go b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/register.go index 704ba87b..1bfc9eb7 100644 --- a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/register.go +++ b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/register.go @@ -1,9 +1,8 @@ package main import ( - "log" - "github.com/google/uuid" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/repository" @@ -53,7 +52,7 @@ func beforeAfterLoggerFactory() request.HandlerFunc { return func(ctx *request.Context) error { // 1. Before request reqId := uuid.New().String() - log.Printf("Starting request id=%s %s %s\n", reqId, ctx.R.Method, ctx.R.URL.Path) + logger.LogInfo("Starting request id=%s %s %s", reqId, ctx.R.Method, ctx.R.URL.Path) ctx.Set("request_id", reqId) // 2. Proceed to next middleware/handler @@ -61,9 +60,9 @@ func beforeAfterLoggerFactory() request.HandlerFunc { // 3. After request if err != nil { - log.Printf("Request id=%s failed: %v\n", reqId, err) + logger.LogError("Request id=%s failed: %v", reqId, err) } else { - log.Printf("Request id=%s completed successfully\n", reqId) + logger.LogInfo("Request id=%s completed successfully", reqId) } return err } diff --git a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/repository/order_repository.go b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/repository/order_repository.go index cadb578a..efaec373 100644 --- a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/repository/order_repository.go +++ b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/repository/order_repository.go @@ -2,8 +2,8 @@ package repository import ( "fmt" - "log" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/model" ) @@ -33,7 +33,7 @@ var _ OrderRepository = (*OrderRepositoryMemory)(nil) // NewOrderRepositoryMemory creates a new in-memory order repository with seed data func NewOrderRepositoryMemory(config map[string]any) *OrderRepositoryMemory { dsn := utils.GetValueFromMap(config, "dsn", "") - log.Printf("⚙️ Initializing OrderRepositoryMemory with DSN: %s\n", dsn) + logger.LogInfo("⚙️ Initializing OrderRepositoryMemory with DSN: %s\n", dsn) repo := &OrderRepositoryMemory{ orders: make(map[int]*model.Order), diff --git a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/repository/user_repository.go b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/repository/user_repository.go index 177491aa..b2f76623 100644 --- a/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/repository/user_repository.go +++ b/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/repository/user_repository.go @@ -1,9 +1,8 @@ package repository import ( - "log" - "github.com/primadi/lokstra/api_client" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/02-multi-deployment-yaml/model" ) @@ -33,7 +32,7 @@ var _ UserRepository = (*UserRepositoryMemory)(nil) // NewUserRepositoryMemory creates a new in-memory user repository with seed data func NewUserRepositoryMemory(config map[string]any) *UserRepositoryMemory { dsn := utils.GetValueFromMap(config, "dsn", "") - log.Printf("⚙️ Initializing UserRepositoryMemory with DSN: %s", dsn) + logger.LogInfo("⚙️ Initializing UserRepositoryMemory with DSN: %s", dsn) repo := &UserRepositoryMemory{ users: make(map[int]*model.User), diff --git a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/load_config.go b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/load_config.go index aa8c8d3a..95d76a88 100644 --- a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/load_config.go +++ b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/load_config.go @@ -1,8 +1,7 @@ package main import ( - "log" - + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/lokstra_registry" ) @@ -73,7 +72,7 @@ func loadconfigFromCode() { }, }) if err != nil { - log.Fatal("❌ Failed to register monolith deployment:", err) + logger.LogInfo("❌ Failed to register monolith deployment:", err) } // Microservice: Each service in its own process @@ -105,6 +104,6 @@ func loadconfigFromCode() { }, }) if err != nil { - log.Fatal("❌ Failed to register microservice deployment:", err) + logger.LogPanic("❌ Failed to register microservice deployment:", err) } } diff --git a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/main.go b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/main.go index 296e5902..db6d709e 100644 --- a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/main.go +++ b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/main.go @@ -2,10 +2,9 @@ package main import ( "flag" - "fmt" - "log" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/lokstra_registry" ) @@ -15,12 +14,11 @@ func main() { "Server to run (monolith.api-server or microservice.user-server, microservice.user-server, or microservice.order-server)") flag.Parse() - fmt.Println("") - fmt.Println("╔═════════════════════════════════════════════╗") - fmt.Println("║ LOKSTRA MULTI-DEPLOYMENT DEMO ║") - fmt.Println("╚═════════════════════════════════════════════╝") - fmt.Println("") - + logger.LogInfo("") + logger.LogInfo("╔═════════════════════════════════════════════╗") + logger.LogInfo("║ LOKSTRA MULTI-DEPLOYMENT DEMO ║") + logger.LogInfo("╚═════════════════════════════════════════════╝") + logger.LogInfo("") // 1. Register service types registerServiceTypes() @@ -33,6 +31,6 @@ func main() { // 3. Run server (no more YAML needed!) if err := lokstra_registry.RunServer(*server, 30*time.Second); err != nil { - log.Fatal("❌ Failed to run server:", err) + logger.LogPanic("❌ Failed to run server:", err) } } diff --git a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/register.go b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/register.go index e8ae659b..79991138 100644 --- a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/register.go +++ b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/register.go @@ -1,9 +1,8 @@ package main import ( - "log" - "github.com/google/uuid" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/repository" @@ -56,7 +55,7 @@ func beforeAfterLoggerFactory() request.HandlerFunc { return func(ctx *request.Context) error { // 1. Before request reqId := uuid.New().String() - log.Printf("Starting request id=%s %s %s\n", reqId, ctx.R.Method, ctx.R.URL.Path) + logger.LogInfo("Starting request id=%s %s %s\n", reqId, ctx.R.Method, ctx.R.URL.Path) ctx.Set("request_id", reqId) // 2. Proceed to next middleware/handler @@ -64,9 +63,9 @@ func beforeAfterLoggerFactory() request.HandlerFunc { // 3. After request if err != nil { - log.Printf("Request id=%s failed: %v\n", reqId, err) + logger.LogError("Request id=%s failed: %v\n", reqId, err) } else { - log.Printf("Request id=%s completed successfully\n", reqId) + logger.LogInfo("Request id=%s completed successfully\n", reqId) } return err } diff --git a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/repository/order_repository.go b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/repository/order_repository.go index 3dc07595..ff432900 100644 --- a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/repository/order_repository.go +++ b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/repository/order_repository.go @@ -2,8 +2,8 @@ package repository import ( "fmt" - "log" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/model" ) @@ -33,7 +33,7 @@ var _ OrderRepository = (*OrderRepositoryMemory)(nil) // NewOrderRepositoryMemory creates a new in-memory order repository with seed data func NewOrderRepositoryMemory(config map[string]any) *OrderRepositoryMemory { dsn := utils.GetValueFromMap(config, "dsn", "") - log.Printf("⚙️ Initializing OrderRepositoryMemory with DSN: %s\n", dsn) + logger.LogInfo("⚙️ Initializing OrderRepositoryMemory with DSN: %s\n", dsn) repo := &OrderRepositoryMemory{ orders: make(map[int]*model.Order), } diff --git a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/repository/user_repository.go b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/repository/user_repository.go index e7b696ac..545c5890 100644 --- a/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/repository/user_repository.go +++ b/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/repository/user_repository.go @@ -1,9 +1,8 @@ package repository import ( - "log" - "github.com/primadi/lokstra/api_client" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/03-multi-deployment-pure-code/model" ) @@ -33,7 +32,7 @@ var _ UserRepository = (*UserRepositoryMemory)(nil) // NewUserRepositoryMemory creates a new in-memory user repository with seed data func NewUserRepositoryMemory(config map[string]any) *UserRepositoryMemory { dsn := utils.GetValueFromMap(config, "dsn", "") - log.Printf("⚙️ Initializing UserRepositoryMemory with DSN: %s", dsn) + logger.LogInfo("⚙️ Initializing UserRepositoryMemory with DSN: %s", dsn) repo := &UserRepositoryMemory{ users: make(map[int]*model.User), } diff --git a/docs/00-introduction/examples/full-framework/04-external-services/main.go b/docs/00-introduction/examples/full-framework/04-external-services/main.go index df7c1fdb..ee653d67 100644 --- a/docs/00-introduction/examples/full-framework/04-external-services/main.go +++ b/docs/00-introduction/examples/full-framework/04-external-services/main.go @@ -1,9 +1,10 @@ package main import ( - "fmt" "strings" + "github.com/primadi/lokstra" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/deploy" svc "github.com/primadi/lokstra/docs/00-introduction/examples/full-framework/04-external-services/service" "github.com/primadi/lokstra/lokstra_registry" @@ -33,60 +34,67 @@ func main() { printStartInfo() - lokstra_registry.RunServerFromConfig() + if err := lokstra.LoadConfig(); err != nil { + logger.LogPanic("❌ Failed to load config:", err) + } + + if err := lokstra.RunConfiguredServer(); err != nil { + logger.LogPanic("❌ Failed to run server:", err) + } + } func printStartInfo() { - fmt.Println() - fmt.Println(strings.Repeat("=", 70)) - fmt.Println("🌐 Example 06 - External Services Integration") - fmt.Println(strings.Repeat("=", 70)) - fmt.Println() - fmt.Println("This example demonstrates:") - fmt.Println(" ✅ External service integration (mock payment gateway)") - fmt.Println(" ✅ proxy.Service for remote calls") - fmt.Println(" ✅ Route override for non-standard endpoints") - fmt.Println(" ✅ external-service-definitions in config") - fmt.Println() - fmt.Println(strings.Repeat("=", 70)) - fmt.Println() - fmt.Println("📋 Prerequisites:") - fmt.Println(" 1. Start mock payment gateway first:") - fmt.Println(" cd mock-payment-gateway && go run main.go") - fmt.Println(" (Runs on http://localhost:9000)") - fmt.Println() - fmt.Println(" 2. Then start this server:") - fmt.Println(" go run main.go") - fmt.Println() - fmt.Println(strings.Repeat("=", 70)) - fmt.Println() - fmt.Println("🔗 API Endpoints:") - fmt.Println() - fmt.Println(" Order Management:") - fmt.Println(" POST http://localhost:3000/orders - Create order (processes payment)") - fmt.Println(" GET http://localhost:3000/orders/{id} - Get order details") - fmt.Println(" POST http://localhost:3000/orders/{id}/refund - Refund order") - fmt.Println() - fmt.Println("💡 How it works:") - fmt.Println(" 1. CreateOrder calls external payment gateway via proxy.Service") - fmt.Println(" 2. Payment gateway returns payment ID") - fmt.Println(" 3. Order is marked as 'paid' with payment ID") - fmt.Println(" 4. Refund also goes through external gateway") - fmt.Println() - fmt.Println("📝 Test:") - fmt.Println(" Use test.http file or:") - fmt.Println() - fmt.Println(" # Create order (processes payment)") - fmt.Println(` curl -X POST http://localhost:3000/orders \`) - fmt.Println(` -H "Content-Type: application/json" \`) - fmt.Println(` -d '{"user_id": 1, "items": ["Book", "Pen"], "total_amount": 25.50, "currency": "USD"}'`) - fmt.Println() - fmt.Println(" # Get order") - fmt.Println(` curl http://localhost:3000/orders/order_1`) - fmt.Println() - fmt.Println(" # Refund order") - fmt.Println(` curl -X POST http://localhost:3000/orders/order_1/refund`) - fmt.Println() - fmt.Println(strings.Repeat("=", 70)) - fmt.Println() + logger.LogInfo("") + logger.LogInfo(strings.Repeat("=", 70)) + logger.LogInfo("🌐 Example 06 - External Services Integration") + logger.LogInfo(strings.Repeat("=", 70)) + logger.LogInfo("") + logger.LogInfo("This example demonstrates:") + logger.LogInfo(" ✅ External service integration (mock payment gateway)") + logger.LogInfo(" ✅ proxy.Service for remote calls") + logger.LogInfo(" ✅ Route override for non-standard endpoints") + logger.LogInfo(" ✅ external-service-definitions in config") + logger.LogInfo("") + logger.LogInfo(strings.Repeat("=", 70)) + logger.LogInfo("") + logger.LogInfo("📋 Prerequisites:") + logger.LogInfo(" 1. Start mock payment gateway first:") + logger.LogInfo(" cd mock-payment-gateway && go run main.go") + logger.LogInfo(" (Runs on http://localhost:9000)") + logger.LogInfo("") + logger.LogInfo(" 2. Then start this server:") + logger.LogInfo(" go run main.go") + logger.LogInfo("") + logger.LogInfo(strings.Repeat("=", 70)) + logger.LogInfo("") + logger.LogInfo("🔗 API Endpoints:") + logger.LogInfo("") + logger.LogInfo(" Order Management:") + logger.LogInfo(" POST http://localhost:3000/orders - Create order (processes payment)") + logger.LogInfo(" GET http://localhost:3000/orders/{id} - Get order details") + logger.LogInfo(" POST http://localhost:3000/orders/{id}/refund - Refund order") + logger.LogInfo("") + logger.LogInfo("💡 How it works:") + logger.LogInfo(" 1. CreateOrder calls external payment gateway via proxy.Service") + logger.LogInfo(" 2. Payment gateway returns payment ID") + logger.LogInfo(" 3. Order is marked as 'paid' with payment ID") + logger.LogInfo(" 4. Refund also goes through external gateway") + logger.LogInfo("") + logger.LogInfo("📝 Test:") + logger.LogInfo(" Use test.http file or:") + logger.LogInfo("") + logger.LogInfo(" # Create order (processes payment)") + logger.LogInfo(` curl -X POST http://localhost:3000/orders \`) + logger.LogInfo(` -H "Content-Type: application/json" \`) + logger.LogInfo(` -d '{"user_id": 1, "items": ["Book", "Pen"], "total_amount": 25.50, "currency": "USD"}'`) + logger.LogInfo("") + logger.LogInfo(" # Get order") + logger.LogInfo(` curl http://localhost:3000/orders/order_1`) + logger.LogInfo("") + logger.LogInfo(" # Refund order") + logger.LogInfo(` curl -X POST http://localhost:3000/orders/order_1/refund`) + logger.LogInfo("") + logger.LogInfo(strings.Repeat("=", 70)) + logger.LogInfo("") } diff --git a/docs/00-introduction/examples/full-framework/06-inline-definitions-example/main.go b/docs/00-introduction/examples/full-framework/06-inline-definitions-example/main.go index 6840e076..41e21b1e 100644 --- a/docs/00-introduction/examples/full-framework/06-inline-definitions-example/main.go +++ b/docs/00-introduction/examples/full-framework/06-inline-definitions-example/main.go @@ -1,7 +1,8 @@ package main import ( - "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra" + "github.com/primadi/lokstra/common/logger" ) func main() { @@ -11,7 +12,13 @@ func main() { // Get config path // configPath := filepath.Join("docs", "00-introduction", "examples", "full-framework", "06-inline-definitions-example", "config.yaml") - lokstra_registry.RunServerFromConfig() + if err := lokstra.LoadConfig(); err != nil { + logger.LogPanic("❌ Failed to load config:", err) + } + + if err := lokstra.RunConfiguredServer(); err != nil { + logger.LogPanic("❌ Failed to run server:", err) + } // Load config and run server // if err := lokstra_registry.LoadAndBuild([]string{"config.yaml"}); err != nil { diff --git a/docs/01-router-guide/02-service/examples/03-service-dependencies/index.md b/docs/01-router-guide/02-service/examples/03-service-dependencies/index.md index 2a20574e..579057fc 100644 --- a/docs/01-router-guide/02-service/examples/03-service-dependencies/index.md +++ b/docs/01-router-guide/02-service/examples/03-service-dependencies/index.md @@ -50,7 +50,7 @@ lokstra_registry.RegisterService("order-service", orderService) // 3. func() any - no params (simplest!) // Multiple DB instances with different DSN (config only - mode 2) -lokstra_registry.RegisterLazyService("db-main", func(cfg map[string]any) any { +lokstra_registry.RegisterLazyService("db_main", func(cfg map[string]any) any { return db.NewConnection(cfg["dsn"].(string)) }, map[string]any{ "dsn": "postgresql://localhost/main", @@ -64,7 +64,7 @@ lokstra_registry.RegisterLazyService("db-analytics", func(cfg map[string]any) an // Services without params - simplest! (mode 3) lokstra_registry.RegisterLazyService("user-repo", func() any { - db := lokstra_registry.MustGetService[*DB]("db-main") + db := lokstra_registry.MustGetService[*DB]("db_main") return NewUserRepository(db) }, nil) @@ -205,7 +205,7 @@ func main() { // Can register in any order! // Multiple DB instances with different config - lokstra_registry.RegisterLazyService("db-main", func(cfg map[string]any) any { + lokstra_registry.RegisterLazyService("db_main", func(cfg map[string]any) any { return db.NewConnection(cfg["dsn"].(string)) }, map[string]any{ "dsn": "postgresql://localhost/main", @@ -231,12 +231,12 @@ func main() { }, nil) lokstra_registry.RegisterLazyService("order-repo", func(cfg map[string]any) any { - db := lokstra_registry.MustGetService[*DB]("db-main") + db := lokstra_registry.MustGetService[*DB]("db_main") return repository.NewOrderRepository(db) }, nil) lokstra_registry.RegisterLazyService("user-repo", func(cfg map[string]any) any { - db := lokstra_registry.MustGetService[*DB]("db-main") + db := lokstra_registry.MustGetService[*DB]("db_main") return repository.NewUserRepository(db) }, nil) @@ -248,7 +248,7 @@ func main() { // 1. order-service factory called // 2. Needs user-service -> user-service factory called // 3. Needs user-repo -> user-repo factory called - // 4. Needs db-main -> db-main factory called + // 4. Needs db_main -> db_main factory called // 5. All cached for future use orderSvc := lokstra_registry.MustGetService[*service.OrderService]("order-service") diff --git a/docs/02-framework-guide/04-config/examples/01-basic-config/main.go b/docs/02-framework-guide/04-config/examples/01-basic-config/main.go index 0ea87ccc..333fe952 100644 --- a/docs/02-framework-guide/04-config/examples/01-basic-config/main.go +++ b/docs/02-framework-guide/04-config/examples/01-basic-config/main.go @@ -24,7 +24,7 @@ func main() { registerMiddlewareTypes() // STEP 4: Initialize and Run Server - if err := lokstra.InitAndRunServer(); err != nil { + if err := lokstra.RunConfiguredServer(); err != nil { log.Fatal("Failed to run server:", err) } } diff --git a/docs/02-framework-guide/04-config/examples/02-multi-file/main.go b/docs/02-framework-guide/04-config/examples/02-multi-file/main.go index 5f3fd4b6..b8e3e161 100644 --- a/docs/02-framework-guide/04-config/examples/02-multi-file/main.go +++ b/docs/02-framework-guide/04-config/examples/02-multi-file/main.go @@ -2,17 +2,21 @@ package main import ( "github.com/primadi/lokstra" - "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra/common/logger" ) func main() { lokstra.Bootstrap() - // Load multiple config files in order - // Later files override earlier ones - // Base config + environment-specific config - lokstra_registry.RunServerFromConfig( + if err := lokstra.LoadConfig( "config/base.yaml", "config/dev.yaml", // or production.yaml for prod - ) + ); err != nil { + logger.LogPanic("❌ Failed to load config:", err) + } + + if err := lokstra.RunConfiguredServer(); err != nil { + logger.LogPanic("❌ Failed to run server:", err) + } + } diff --git a/docs/02-framework-guide/04-config/examples/06-handlers/main.go b/docs/02-framework-guide/04-config/examples/06-handlers/main.go index 91d1aac4..ccf5d45c 100644 --- a/docs/02-framework-guide/04-config/examples/06-handlers/main.go +++ b/docs/02-framework-guide/04-config/examples/06-handlers/main.go @@ -23,7 +23,7 @@ func main() { registerMiddlewareTypes() // STEP 4: Initialize and Run Server - if err := lokstra.InitAndRunServer(); err != nil { + if err := lokstra.RunConfiguredServer(); err != nil { log.Fatal("Failed to run server:", err) } } diff --git a/docs/03-api-reference/02-registry/lokstra_registry.md b/docs/03-api-reference/02-registry/lokstra_registry.md index 44fbbe5c..cf01910f 100644 --- a/docs/03-api-reference/02-registry/lokstra_registry.md +++ b/docs/03-api-reference/02-registry/lokstra_registry.md @@ -164,7 +164,7 @@ func(cfg map[string]any) any // With config **Example:** ```go // With config -lokstra_registry.RegisterLazyService("db-main", func(cfg map[string]any) any { +lokstra_registry.RegisterLazyService("db_main", func(cfg map[string]any) any { dsn := cfg["dsn"].(string) return db.NewConnection(dsn) }, map[string]any{ @@ -173,7 +173,7 @@ lokstra_registry.RegisterLazyService("db-main", func(cfg map[string]any) any { // Without params (resolve deps manually) lokstra_registry.RegisterLazyService("user-repo", func() any { - db := lokstra_registry.MustGetService[*DB]("db-main") + db := lokstra_registry.MustGetService[*DB]("db_main") return repository.NewUserRepository(db) }, nil) ``` diff --git a/internal/auto_create_dbpool.go b/internal/auto_create_dbpool.go deleted file mode 100644 index 59d63905..00000000 --- a/internal/auto_create_dbpool.go +++ /dev/null @@ -1,3 +0,0 @@ -package internal - -var AutoCreateDbPoolManager func() diff --git a/bootstrap.go b/lokstra_bootstrap.go similarity index 77% rename from bootstrap.go rename to lokstra_bootstrap.go index 3fd4779b..fa2f6402 100644 --- a/bootstrap.go +++ b/lokstra_bootstrap.go @@ -7,14 +7,9 @@ import ( "path/filepath" "strings" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/annotation" - "github.com/primadi/lokstra/core/deploy" - "github.com/primadi/lokstra/internal" "github.com/primadi/lokstra/lokstra_registry" - "github.com/primadi/lokstra/serviceapi" - "github.com/primadi/lokstra/services/dbpool_manager" - "github.com/primadi/lokstra/services/sync_config_pg" - "github.com/primadi/lokstra/syncmap" ) type RunMode string @@ -114,18 +109,18 @@ func Bootstrap(scanPath ...string) { func DetectRunMode() RunMode { exe, err := os.Executable() if err != nil { - deploy.LogDebug("[Lokstra] Warning: cannot get executable path:", err) + logger.LogDebug("[Lokstra] Warning: cannot get executable path:", err) return RunModeProd } exePath := filepath.ToSlash(exe) exeName := filepath.Base(exe) - deploy.LogDebug("[Lokstra] Executable: %s\n", exePath) + logger.LogDebug("[Lokstra] Executable: %s\n", exePath) // 1️⃣ Check if running under Delve debugger // Delve wraps the binary with __debug_bin if strings.Contains(exeName, "__debug_bin") { - deploy.LogDebug("[Lokstra] Detected: Delve debugger (debug binary)") + logger.LogDebug("[Lokstra] Detected: Delve debugger (debug binary)") return RunModeDebug } @@ -133,7 +128,7 @@ func DetectRunMode() RunMode { // "go run" creates temporary executables in go-build cache directory if strings.Contains(exePath, "go-build") || strings.Contains(exePath, os.TempDir()) { - deploy.LogDebug("[Lokstra] Detected: go run (temporary build)") + logger.LogDebug("[Lokstra] Detected: go run (temporary build)") return RunModeDev } @@ -141,7 +136,7 @@ func DetectRunMode() RunMode { // Windows: .exe extension confirms it's a compiled binary // Linux/Mac: no .exe, but also not in temp/go-build, so it's compiled // Default to production mode for all compiled binaries - deploy.LogDebug("[Lokstra] Detected: compiled binary (production mode)") + logger.LogDebug("[Lokstra] Detected: compiled binary (production mode)") return RunModeProd } @@ -236,47 +231,13 @@ func relaunchWithDlv() { fmt.Println("╚════════════════════════════════════════════════════════════════╝") fmt.Println("") fmt.Println("⚠️ Code generation detected changes.") - fmt.Println("⚠️ Please STOP and RESTART your debugger to load the new code.") - fmt.Println("") - fmt.Println("Press Ctrl+C or stop the debugger, then press F5 to restart.") + fmt.Println("⚠️ Please RESTART your debugger to load the new code.") fmt.Println("") // Exit cleanly so debugger can be restarted os.Exit(0) } -// auto create dbpool-manager service if not exists -func autoCreateDbPoolManager() { - // Register SyncConfigPG service type - sync_config_pg.Register() - - pm := lokstra_registry.GetService[serviceapi.DbPoolManager]("dbpool-manager") - if pm != nil { - return // Already registered - } - - // Check if sync mode is enabled via config - useSync := lokstra_registry.GetConfig("dbpool-manager.use_sync", true) - - if useSync { - // Create SyncMaps for tenant and named pools using syncmap package - dbPools := syncmap.NewSyncMap[*dbpool_manager.DsnSchema]("db-pools") - - pm = dbpool_manager.NewPgxSyncPoolManager(dbPools) - deploy.LogDebug("[Lokstra] DbPoolManager initialized with distributed sync") - } else { - // Default: use regular pool manager (local sync.Map) - pm = dbpool_manager.NewPgxPoolManager() - deploy.LogDebug("[Lokstra] DbPoolManager initialized with local sync") - } - - lokstra_registry.RegisterService("dbpool-manager", pm) -} - -func init() { - internal.AutoCreateDbPoolManager = autoCreateDbPoolManager -} - // LoadConfigFromFolder loads configuration from the specified folder path. // It also ensures that the dbpool-manager service is registered before loading config. func LoadConfigFromFolder(folderPath string) error { @@ -285,17 +246,17 @@ func LoadConfigFromFolder(folderPath string) error { // LoadConfig loads configuration from the specified file path. // It also ensures that the dbpool-manager service is registered before loading config. -func LoadConfig(filePath string) error { - return lokstra_registry.LoadConfig(filePath) +func LoadConfig(filePath ...string) error { + return lokstra_registry.LoadConfig(filePath...) } -// SetupNamedDbPools sets up database pools from loaded config. +// LoadNamedDbPoolsFromConfig sets up database pools from loaded config. // Must be called AFTER LoadConfig() if you use named-db-pools in config. -func SetupNamedDbPools() error { - return lokstra_registry.SetupNamedDbPools() +func LoadNamedDbPoolsFromConfig() error { + return lokstra_registry.LoadNamedDbPoolsFromConfig() } -// InitAndRunServer initializes and runs the server based on loaded configuration. -func InitAndRunServer() error { - return lokstra_registry.InitAndRunServer() +// RunConfiguredServer initializes and runs the server based on loaded configuration. +func RunConfiguredServer() error { + return lokstra_registry.RunConfiguredServer() } diff --git a/lokstra.go b/lokstra_helper.go similarity index 80% rename from lokstra.go rename to lokstra_helper.go index b0012bdb..4648600f 100644 --- a/lokstra.go +++ b/lokstra_helper.go @@ -3,7 +3,6 @@ package lokstra import ( "github.com/primadi/lokstra/api_client" "github.com/primadi/lokstra/core/app" - "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/core/router" "github.com/primadi/lokstra/core/server" @@ -35,23 +34,13 @@ func NewAppWithConfig(name string, addr string, listenerType string, return app.NewWithConfig(name, addr, listenerType, config, routers...) } +// Create a new Server instance with given apps func NewServer(name string, apps ...*app.App) *server.Server { return server.New(name, apps...) } +// FetchAndCast performs an API fetch and casts the result to the specified type T func FetchAndCast[T any](client *api_client.ClientRouter, path string, opts ...api_client.FetchOption) (T, error) { return api_client.FetchAndCast[T](client, path, opts...) } - -type LogLevel = deploy.LogLevel - -const LogLevelDebug = deploy.LogLevelDebug -const LogLevelInfo = deploy.LogLevelInfo -const LogLevelWarn = deploy.LogLevelWarn -const LogLevelError = deploy.LogLevelError -const LogLevelSilent = deploy.LogLevelSilent - -func SetLogLevel(level LogLevel) { - deploy.SetLogLevel(level) -} diff --git a/lokstra_init_option.go b/lokstra_init_option.go new file mode 100644 index 00000000..1061668c --- /dev/null +++ b/lokstra_init_option.go @@ -0,0 +1,96 @@ +package lokstra + +import ( + "github.com/primadi/lokstra/common/logger" +) + +// Options pattern untuk QuickStart +type InitializeOption func(*InitializeConfig) + +func WithLogLevel(level logger.LogLevel) InitializeOption { + return func(c *InitializeConfig) { c.LogLevel = level } +} + +func WithAnnotations(enable bool, paths ...string) InitializeOption { + return func(c *InitializeConfig) { + c.UseAnnotation = enable + c.AnnotationScanPaths = paths + } +} + +func WithoutYAMLConfig() InitializeOption { + return func(c *InitializeConfig) { + c.ConfigSource = ConfigNone + c.ConfigPath = nil + } +} + +func WithYAMLConfigPath(paths ...string) InitializeOption { + return func(c *InitializeConfig) { + c.ConfigSource = ConfigFromFile + c.ConfigPath = paths + } +} + +func WithYAMLConfigFolder(folder string) InitializeOption { + return func(c *InitializeConfig) { + c.ConfigSource = ConfigFromFolder + c.ConfigPath = []string{folder} + } +} + +func WithoutPgSyncMap() InitializeOption { + return func(c *InitializeConfig) { + c.UsePgxSyncMap = false + } +} + +func WithPgSyncMap(dbPoolName ...string) InitializeOption { + return func(c *InitializeConfig) { + c.UsePgxSyncMap = true + if len(dbPoolName) == 0 { + c.PgxSyncMapDbPoolName = "db_main" + } else { + if len(dbPoolName) > 1 { + panic("WithPgSyncMap only accepts zero or one argument for dbPoolName") + } + c.PgxSyncMapDbPoolName = dbPoolName[0] + } + } +} + +func WithoutPgDatabase() InitializeOption { + return func(c *InitializeConfig) { + c.DbPoolManagerEnable = false + } +} + +func WithPgDatabase() InitializeOption { + return func(c *InitializeConfig) { + c.DbPoolManagerEnable = true + } +} + +func WithoutDbMigrations() InitializeOption { + return func(c *InitializeConfig) { + c.CheckDbMigration = false + } +} + +func WithDbMigrations(folder string) InitializeOption { + return func(c *InitializeConfig) { + c.CheckDbMigration = true + c.MigrationFolder = folder + } +} + +func WithServerInitFunc(initFunc func() error) InitializeOption { + return func(c *InitializeConfig) { + c.ServerInitFunc = initFunc + } +} +func WithoutAutoRunServer() InitializeOption { + return func(c *InitializeConfig) { + c.InitAndRunServer = false + } +} diff --git a/lokstra_initialize.go b/lokstra_initialize.go new file mode 100644 index 00000000..0c240022 --- /dev/null +++ b/lokstra_initialize.go @@ -0,0 +1,150 @@ +package lokstra + +import ( + "fmt" + + "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/services/sync_config_pg" +) + +type ConfigSourceType int + +const ( + ConfigNone ConfigSourceType = iota + ConfigFromFile + ConfigFromFolder +) + +type InitializeConfig struct { + // 1. SetLogLevel + LogLevel logger.LogLevel + + // 2.Bootstrap + UseAnnotation bool + AnnotationScanPaths []string + + // 3. LoadConfig + ConfigSource ConfigSourceType + ConfigPath []string + + // 4. UsePgxSyncMap + // If true, You have to use DbPoolManager also + UsePgxSyncMap bool + PgxSyncMapDbPoolName string + + // 5. DbPoolManagerEnable + DbPoolManagerEnable bool + + // 6. CheckDbMigrations + CheckDbMigration bool + MigrationFolder string + + // 7. ServerInit Func + ServerInitFunc func() error + + // 8. Init and Run Server + InitAndRunServer bool +} + +func BootstrapAndRun(opts ...InitializeOption) error { + cfg := &InitializeConfig{ + LogLevel: logger.LogLevelInfo, + UseAnnotation: true, // Auto-detect @RouterService + ConfigSource: ConfigFromFolder, + DbPoolManagerEnable: true, // Auto if config has database.* + UsePgxSyncMap: true, + PgxSyncMapDbPoolName: "db_main", + CheckDbMigration: true, + MigrationFolder: "migrations", + InitAndRunServer: true, + } + + // Apply options + for _, opt := range opts { + opt(cfg) + } + + return BootstrapAndRunWithConfig(cfg) +} + +// Initialize lokstra framework with given config +func BootstrapAndRunWithConfig(cfg *InitializeConfig) error { + if !cfg.DbPoolManagerEnable { + if cfg.UsePgxSyncMap { + return fmt.Errorf("PgxSyncMap requires DbPoolManager to be enabled") + } + if cfg.CheckDbMigration { + return fmt.Errorf("DB Migration check requires DbPoolManager to be enabled") + } + } + + // 1. Set log level + logger.SetLogLevel(cfg.LogLevel) + + // 2. Bootstrap + if cfg.UseAnnotation { + Bootstrap(cfg.AnnotationScanPaths...) + } + + // 3. LoadConfig + switch cfg.ConfigSource { + case ConfigFromFile: + if len(cfg.ConfigPath) == 0 { + cfg.ConfigPath = []string{"config.yaml"} + } + if err := LoadConfig(cfg.ConfigPath...); err != nil { + return err + } + case ConfigFromFolder: + lenConfig := len(cfg.ConfigPath) + if lenConfig == 0 { + cfg.ConfigPath = []string{"config"} + } else if lenConfig > 1 { + return fmt.Errorf("ConfigPath for ConfigFromFolder should contain only one folder path") + } + + if err := LoadConfigFromFolder(cfg.ConfigPath[0]); err != nil { + return err + } + } + + // 4. Use Pgx SyncMap + if cfg.UsePgxSyncMap { + if len(cfg.PgxSyncMapDbPoolName) == 0 { + return fmt.Errorf("PgxSyncMapDbPoolName must be set when UsePgxSyncMap is true") + } + sync_config_pg.Register(cfg.PgxSyncMapDbPoolName) + } + + // 5. PgxDbPoolManager + if cfg.DbPoolManagerEnable { + UsePgxDbPoolManager(cfg.UsePgxSyncMap) + + if err := LoadNamedDbPoolsFromConfig(); err != nil { + return err + } + } + + // 6. Check DB Migrations + if cfg.CheckDbMigration { + if err := CheckDbMigrationsAuto(cfg.MigrationFolder); err != nil { + return err + } + } + + // 7. Server Init Func + if cfg.ServerInitFunc != nil { + if err := cfg.ServerInitFunc(); err != nil { + return err + } + } + + // 8. Init and Run Server + if cfg.InitAndRunServer { + if err := RunConfiguredServer(); err != nil { + return err + } + } + + return nil +} diff --git a/migration.go b/lokstra_migration.go similarity index 91% rename from migration.go rename to lokstra_migration.go index 9083e486..f85979e7 100644 --- a/migration.go +++ b/lokstra_migration.go @@ -3,10 +3,10 @@ package lokstra import ( "context" "fmt" - "log" "os" "path/filepath" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/serviceapi" @@ -175,24 +175,24 @@ func CheckDbMigration(cfg *MigrationConfig) error { // Skip if should not run if !shouldRun { if !cfg.Silent { - log.Printf("[Lokstra] Skipping migrations (mode=%s, force=%s)", mode, cfg.Force) + logger.LogInfo("[Lokstra] Skipping migrations (mode=%s, force=%s)", mode, cfg.Force) } return nil } // Get database pool - dbPool, ok := lokstra_registry.GetServiceAny(cfg.DbPoolName) + pool, ok := lokstra_registry.GetServiceAny(cfg.DbPoolName) if !ok { return fmt.Errorf("database pool '%s' not found - check your config.yaml named-db-pools section", cfg.DbPoolName) } - dbPoolWithSchema, ok := dbPool.(serviceapi.DbPoolWithSchema) + dbPool, ok := pool.(serviceapi.DbPool) if !ok { return fmt.Errorf("service '%s' is not a DbPoolWithSchema", cfg.DbPoolName) } // Create migration runner with custom schema table if specified - runner := migration_runner.New(dbPoolWithSchema, cfg.MigrationsDir) + runner := migration_runner.New(dbPool, cfg.MigrationsDir) if cfg.SchemaTable != "" && cfg.SchemaTable != "schema_migrations" { runner = runner.WithSchemaTable(cfg.SchemaTable) } @@ -200,7 +200,7 @@ func CheckDbMigration(cfg *MigrationConfig) error { // Run migrations ctx := context.Background() if !cfg.Silent { - log.Printf("[Lokstra] Running migrations (mode=%s, force=%s, dir=%s, db=%s, schema=%s)", + logger.LogInfo("[Lokstra] Running migrations (mode=%s, force=%s, dir=%s, db=%s, schema=%s)", mode, cfg.Force, cfg.MigrationsDir, cfg.DbPoolName, cfg.SchemaTable) } @@ -209,7 +209,7 @@ func CheckDbMigration(cfg *MigrationConfig) error { } if !cfg.Silent { - log.Printf("[Lokstra] Migrations completed successfully") + logger.LogInfo("[Lokstra] Migrations completed successfully") } return nil @@ -262,11 +262,18 @@ func loadMigrationYaml(path string) (*MigrationYamlConfig, error) { // // // Auto-scan and run all database migrations // if err := lokstra.CheckDbMigrationsAuto("migrations"); err != nil { -// log.Fatalf("Migrations failed: %v", err) +// logger.LogPanic("Migrations failed: %v", err) // } // // lokstra_registry.RunServerFromConfig() // } +// +// Example migration.yaml: +// +// dbpool-name: main-db +// schema-table: schema_migrations +// force: auto +// description: Main application database with users, orders, products func CheckDbMigrationsAuto(configFolder string) error { // Read all subdirectories sorted alphabetically basePath := utils.GetBasePath() @@ -304,7 +311,7 @@ func CheckDbMigrationsAuto(configFolder string) error { for _, folder := range migrationFolders { folderPath := filepath.Join(rootDir, folder) - log.Printf("[Lokstra] Processing migration folder: %s", folder) + logger.LogInfo("[Lokstra] Processing migration folder: %s", folder) // Run migration for this folder err := CheckDbMigration(&MigrationConfig{ @@ -345,6 +352,6 @@ func CheckDbMigrationsAuto(configFolder string) error { } } - log.Printf("[Lokstra] Multi-database migrations completed: %d successful, %d skipped", successCount, skippedCount) + logger.LogInfo("[Lokstra] Multi-database migrations completed: %d successful, %d skipped", successCount, skippedCount) return nil } diff --git a/lokstra_pgx_dbpool_manager.go b/lokstra_pgx_dbpool_manager.go new file mode 100644 index 00000000..5e51b626 --- /dev/null +++ b/lokstra_pgx_dbpool_manager.go @@ -0,0 +1,27 @@ +package lokstra + +import ( + "github.com/primadi/lokstra/common/logger" + "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra/serviceapi" + "github.com/primadi/lokstra/services/dbpool_manager" +) + +// auto create dbpool-manager service if not exists +func UsePgxDbPoolManager(useSync bool) { + pm := lokstra_registry.GetService[serviceapi.DbPoolManager]("dbpool-manager") + if pm != nil { + return // Already registered + } + + if useSync { + pm = dbpool_manager.NewPgxSyncDbPoolManager() + logger.LogDebug("[Lokstra] DbPoolManager initialized with distributed sync") + } else { + // Default: use regular pool manager (local sync.Map) + pm = dbpool_manager.NewPgxPoolManager() + logger.LogDebug("[Lokstra] DbPoolManager initialized with local sync") + } + + lokstra_registry.RegisterService("dbpool-manager", pm) +} diff --git a/lokstra_registry/config_test.go b/lokstra_registry/config_test.go index 82cfc30a..383ff7e0 100644 --- a/lokstra_registry/config_test.go +++ b/lokstra_registry/config_test.go @@ -22,15 +22,15 @@ type ServerConfig struct { func TestGetConfig_FlatAccess(t *testing.T) { // Setup registry := deploy.Global() - registry.SetConfig("global-db.dsn", "postgres://localhost/test") - registry.SetConfig("global-db.schema", "public") + registry.SetConfig("db_main.dsn", "postgres://localhost/test") + registry.SetConfig("db_main.schema", "public") // Test flat access - dsn := lokstra_registry.GetConfig("global-db.dsn", "default") + dsn := lokstra_registry.GetConfig("db_main.dsn", "default") if dsn != "postgres://localhost/test" { t.Errorf("Expected 'postgres://localhost/test', got '%s'", dsn) } - schema := lokstra_registry.GetConfig("global-db.schema", "default") + schema := lokstra_registry.GetConfig("db_main.schema", "default") if schema != "public" { t.Errorf("Expected 'public', got '%s'", schema) } diff --git a/lokstra_registry/deployment.go b/lokstra_registry/deployment.go index 1886625b..2d6c4d8c 100644 --- a/lokstra_registry/deployment.go +++ b/lokstra_registry/deployment.go @@ -2,12 +2,12 @@ package lokstra_registry import ( "fmt" - "log" "os" "strconv" "strings" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/app" "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/deploy/loader" @@ -46,7 +46,7 @@ func SetCurrentServer(compositeKey string) error { return fmt.Errorf("no server topologies found in global registry") } compositeKey = firstKey - log.Printf("🎯 Auto-selected first server: %s", compositeKey) + logger.LogDebug("🎯 Auto-selected first server: %s", compositeKey) } // Shorthand support: "api" → "default.api" @@ -54,7 +54,7 @@ func SetCurrentServer(compositeKey string) error { if len(parts) == 1 { // No dot - assume shorthand for "default.{serverName}" compositeKey = "default." + compositeKey - log.Printf("🎯 Using shorthand: %s", compositeKey) + logger.LogDebug("🎯 Using shorthand: %s", compositeKey) } else if len(parts) != 2 { return fmt.Errorf("invalid server key format, expected 'deployment.server' or 'server', got: %s", compositeKey) } @@ -156,8 +156,8 @@ func PrintCurrentServerInfo() error { return nil } -// RunCurrentServer builds and runs the current server based on deployment config -func RunCurrentServer(timeout time.Duration) error { +// runCurrentServer builds and runs the current server based on deployment config +func runCurrentServer(timeout time.Duration) error { if currentCompositeKey == "" { return fmt.Errorf("no server set - call SetCurrentServer first") } @@ -193,7 +193,7 @@ func RunCurrentServer(timeout time.Duration) error { return fmt.Errorf("failed to register definitions for runtime: %w", err) } - log.Printf("📝 Normalized and registered definitions for server %s.%s", deploymentName, serverName) + logger.LogDebug("📝 Normalized and registered definitions for server %s.%s", deploymentName, serverName) } // Get apps from topology @@ -228,7 +228,7 @@ func RunCurrentServer(timeout time.Duration) error { rewrites[rw.Pattern] = rw.Replacement } r.SetPathRewrites(rewrites) - log.Printf("🔧 Applied %d path rewrite rule(s) to router '%s'\n", len(rewrites), routerName) + logger.LogDebug("🔧 Applied %d path rewrite rule(s) to router '%s'\n", len(rewrites), routerName) } // Apply router-level middleware overrides if specified @@ -240,7 +240,7 @@ func RunCurrentServer(timeout time.Duration) error { } // Apply middleware overrides from YAML config router.ApplyMiddlewares(r, middlewares...) - log.Printf("🔧 Applied router-level middlewares to '%s': %v\n", routerName, routerDef.Middlewares) + logger.LogDebug("🔧 Applied router-level middlewares to '%s': %v\n", routerName, routerDef.Middlewares) } // Apply route-level overrides (custom routes) @@ -258,7 +258,7 @@ func RunCurrentServer(timeout time.Duration) error { if mw != nil { options = append(options, mw) } else { - log.Printf("⚠️ Warning: Middleware '%s' not found for route '%s'\n", + logger.LogWarning("⚠️ Warning: Middleware '%s' not found for route '%s'\n", mwName, customRoute.Name) } } @@ -268,10 +268,10 @@ func RunCurrentServer(timeout time.Duration) error { if len(options) > 0 { err := r.UpdateRoute(customRoute.Name, options...) if err != nil { - log.Printf("⚠️ Warning: Failed to update route '%s' in router '%s': %v\n", + logger.LogWarning("⚠️ Warning: Failed to update route '%s' in router '%s': %v\n", customRoute.Name, routerName, err) } else { - log.Printf("🔧 Applied route-level middlewares to '%s.%s': %v\n", + logger.LogDebug("🔧 Applied route-level middlewares to '%s.%s': %v\n", routerName, customRoute.Name, customRoute.Middlewares) } } @@ -371,7 +371,7 @@ func applyAppHandlerConfigurations(coreApp *app.App, config *schema.DeployConfig spaRouter.ANYPrefix(spaDef.Prefix, handler) coreApp.AddRouter(spaRouter) - log.Printf("📦 [%s] Mounted SPA: %s -> %s\n", coreApp.GetName(), spaDef.Prefix, spaDef.Dir) + logger.LogDebug("📦 [%s] Mounted SPA: %s -> %s\n", coreApp.GetName(), spaDef.Prefix, spaDef.Dir) } } @@ -389,7 +389,7 @@ func applyAppHandlerConfigurations(coreApp *app.App, config *schema.DeployConfig staticRouter.ANYPrefix(staticDef.Prefix, handler) coreApp.AddRouter(staticRouter) - log.Printf("📦 [%s] Mounted Static: %s -> %s\n", coreApp.GetName(), staticDef.Prefix, staticDef.Dir) + logger.LogDebug("📦 [%s] Mounted Static: %s -> %s\n", coreApp.GetName(), staticDef.Prefix, staticDef.Dir) } } @@ -411,5 +411,5 @@ func RunServer(compositeKey string, timeout time.Duration) error { } // Run the server - return RunCurrentServer(timeout) + return runCurrentServer(timeout) } diff --git a/lokstra_registry/helper.go b/lokstra_registry/helper.go index f124aac5..ea531f62 100644 --- a/lokstra_registry/helper.go +++ b/lokstra_registry/helper.go @@ -1,73 +1,14 @@ package lokstra_registry import ( - "log" "path/filepath" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" - "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/deploy/loader" ) -// ===== LEGACY API (For Backward Compatibility) ===== - -// RunServerFromConfig loads configuration from specified YAML file(s) and runs the server. -func RunServerFromConfig(config ...string) { - // 1. Load config (loads ALL deployments into Global registry) - if err := LoadConfig(config...); err != nil { - log.Fatal("❌ Failed to load config:", err) - } - - server := GetConfig("server", "") - if server == "" { - log.Fatal("❌ 'server' not specified in config, Please add this to your config.yaml:\n" + - "configs:\n" + - " server: ${SERVER} # mandatory, default first_server_defined\n" + - " shutdown_timeout: ${SHUTDOWN_TIMEOUT:30s} # optional, default 30s") - } - - var timeout time.Duration - - timeoutStr := GetConfig("shutdown_timeout", "30s") - if dur, err := time.ParseDuration(timeoutStr); err == nil { - timeout = dur - } else { - timeout = 30 * time.Second - } - - // 2. Run server - if err := RunServer(server, timeout); err != nil { - log.Fatal("❌ Failed to run server:", err) - } -} - -// RunServerFromConfigFolder loads all YAML files from the specified folder and runs the server. -func RunServerFromConfigFolder(configFolder string) { - // Load all YAML files in the specified config folder - basePath := utils.GetBasePath() - configFolder = filepath.Join(basePath, configFolder) - files, err := filepath.Glob(filepath.Join(configFolder, "*.yaml")) - if err != nil { - log.Fatalf("failed to read config folder: %v", err) - } - - if len(files) == 0 { - log.Printf("no YAML config found in folder: %s", configFolder) - return - } - - lenPrefix := len(basePath) + 1 - for i, f := range files { - files[i] = f[lenPrefix:] - } - - // Kirim semua file ke fungsi berikutnya - RunServerFromConfig(files...) -} - -// ===== NEW API (Recommended Flow) ===== - // LoadConfig loads YAML configuration file(s) and registers lazy load services. // This makes config available for service/middleware registration. // Config paths are relative to the project base path. @@ -75,7 +16,7 @@ func RunServerFromConfigFolder(configFolder string) { // Example: // // if err := lokstra_registry.LoadConfig("config.yaml"); err != nil { -// log.Fatal(err) +// logger.LogPanic(err) // } // // After calling LoadConfig, you can: @@ -92,7 +33,7 @@ func LoadConfig(configPaths ...string) error { return err } - deploy.LogDebug("✅ Config loaded successfully from: %v", configPaths) + logger.LogDebug("✅ Config loaded successfully from: %v", configPaths) return nil } @@ -102,7 +43,7 @@ func LoadConfig(configPaths ...string) error { // Example: // // if err := lokstra_registry.LoadConfigFromFolder("config"); err != nil { -// log.Fatal(err) +// logger.LogPanic(err) // } func LoadConfigFromFolder(configFolder string) error { // Load all YAML files in the specified config folder @@ -114,7 +55,7 @@ func LoadConfigFromFolder(configFolder string) error { } if len(files) == 0 { - log.Printf("⚠️ No YAML config found in folder: %s", configFolder) + logger.LogInfo("⚠️ No YAML config found in folder: %s", configFolder) return nil } @@ -126,23 +67,23 @@ func LoadConfigFromFolder(configFolder string) error { return LoadConfig(files...) } -// SetupNamedDbPools sets up database pools from loaded config. +// LoadNamedDbPoolsFromConfig sets up database pools from loaded config. // Must be called AFTER LoadConfig() if you use named-db-pools in config. // Call this explicitly only if you need DB pools. // // Example: // // if err := lokstra_registry.LoadConfig("config.yaml"); err != nil { -// log.Fatal(err) +// logger.LogPanic(err) // } -// if err := lokstra_registry.SetupNamedDbPools(); err != nil { -// log.Fatal(err) +// if err := lokstra_registry.LoadNamedDbPoolsFromConfig(); err != nil { +// logger.LogPanic(err) // } -func SetupNamedDbPools() error { - return loader.SetupNamedDbPools() +func LoadNamedDbPoolsFromConfig() error { + return loader.LoadNamedDbPoolsFromConfig() } -// InitAndRunServer initializes and runs the server based on loaded config. +// RunConfiguredServer initializes and runs the server based on loaded config. // Must be called after LoadConfig() and service/middleware registration. // // This function: @@ -156,10 +97,10 @@ func SetupNamedDbPools() error { // // Example: // -// if err := lokstra_registry.InitAndRunServer(); err != nil { -// log.Fatal(err) +// if err := lokstra_registry.RunConfiguredServer(); err != nil { +// logger.LogPanic(err) // } -func InitAndRunServer() error { +func RunConfiguredServer() error { server := GetConfig("server", "") var timeout time.Duration diff --git a/lokstra_registry/registry.go b/lokstra_registry/registry.go index 91059212..d74b77b8 100644 --- a/lokstra_registry/registry.go +++ b/lokstra_registry/registry.go @@ -301,7 +301,7 @@ func HasService(name string) bool { // // Example with config: // -// lokstra_registry.RegisterLazyService("db-main", func(cfg map[string]any) any { +// lokstra_registry.RegisterLazyService("db_main", func(cfg map[string]any) any { // return db.NewConnection(cfg["dsn"].(string)) // }, map[string]any{ // "dsn": "postgresql://localhost/main", @@ -310,7 +310,7 @@ func HasService(name string) bool { // Example without params: // // lokstra_registry.RegisterLazyService("user-repo", func() any { -// db := lokstra_registry.MustGetService[*DB]("db-main") +// db := lokstra_registry.MustGetService[*DB]("db_main") // return repository.NewUserRepository(db) // }, nil) // @@ -492,18 +492,18 @@ func SetConfig(key string, value any) { // Usage examples: // // // Simple types (direct type assertion) -// dsn := GetConfig("global-db.dsn", "") +// dsn := GetConfig("db_main.dsn", "") // port := GetConfig("server.port", 8080) // // // Map access -// dbConfig := GetConfig[map[string]any]("global-db", nil) +// dbConfig := GetConfig[map[string]any]("db_main", nil) // // // Struct binding (automatic conversion from map) // type DBConfig struct { // DSN string `json:"dsn"` // Schema string `json:"schema"` // } -// dbConfig := GetConfig[DBConfig]("global-db", DBConfig{}) +// dbConfig := GetConfig[DBConfig]("db_main", DBConfig{}) func GetConfig[T any](name string, defaultValue T) T { value, ok := deploy.Global().GetConfig(name) if !ok { diff --git a/loksttra_pgx_sync_config.go b/loksttra_pgx_sync_config.go new file mode 100644 index 00000000..2ab8207d --- /dev/null +++ b/loksttra_pgx_sync_config.go @@ -0,0 +1,8 @@ +package lokstra + +import "github.com/primadi/lokstra/services/sync_config_pg" + +func UsePgxSyncConfig(syncDbPoolName string) { + // Register SyncConfigPG service type (SyncMap based) + sync_config_pg.Register(syncDbPoolName) +} diff --git a/middleware/recovery/recovery.go b/middleware/recovery/recovery.go index 1beddc87..fff3715e 100644 --- a/middleware/recovery/recovery.go +++ b/middleware/recovery/recovery.go @@ -2,9 +2,9 @@ package recovery import ( "fmt" - "log" "runtime/debug" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/lokstra_registry" @@ -54,14 +54,14 @@ func Middleware(cfg *Config) request.HandlerFunc { // Log panic if enabled if cfg.EnableLogging { - log.Printf("[PANIC RECOVERY] %v\n%s", r, stack) + logger.LogError("[PANIC RECOVERY] %v\n%s", r, stack) } // Use custom handler if provided if cfg.CustomHandler != nil { err := cfg.CustomHandler(c, r, stack) if err != nil { - log.Printf("[RECOVERY] Custom handler error: %v", err) + logger.LogError("[RECOVERY] Custom handler error: %v", err) } return } diff --git a/middleware/request_logger/request_logger.go b/middleware/request_logger/request_logger.go index feab4ff7..6afa1ecd 100644 --- a/middleware/request_logger/request_logger.go +++ b/middleware/request_logger/request_logger.go @@ -2,9 +2,9 @@ package request_logger import ( "fmt" - "log" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/lokstra_registry" @@ -24,7 +24,7 @@ type Config struct { SkipPaths []string // CustomLogger is a custom logging function - // If nil, uses default log.Printf + // If nil, uses default logger.LogInfo CustomLogger func(format string, args ...any) } @@ -54,7 +54,7 @@ func Middleware(cfg *Config) request.HandlerFunc { cfg.SkipPaths = defConfig.SkipPaths } if cfg.CustomLogger == nil { - cfg.CustomLogger = log.Printf + cfg.CustomLogger = logger.LogInfo } return request.HandlerFunc(func(c *request.Context) error { @@ -75,11 +75,8 @@ func Middleware(cfg *Config) request.HandlerFunc { // Calculate duration duration := time.Since(start) - // Get status code from writer wrapper - statusCode := c.W.StatusCode() - if statusCode == 0 { - statusCode = 200 // Default if not set - } + // Get status code using helper function + statusCode := c.StatusCode() // Format and log request if cfg.EnableColors { diff --git a/middleware/slow_request_logger/slow_request_logger.go b/middleware/slow_request_logger/slow_request_logger.go index 157373dd..1abcd99c 100644 --- a/middleware/slow_request_logger/slow_request_logger.go +++ b/middleware/slow_request_logger/slow_request_logger.go @@ -2,9 +2,9 @@ package slow_request_logger import ( "fmt" - "log" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/lokstra_registry" @@ -29,7 +29,7 @@ type Config struct { SkipPaths []string // CustomLogger is a custom logging function - // If nil, uses default log.Printf + // If nil, uses default logger.LogInfo CustomLogger func(format string, args ...any) } @@ -60,7 +60,7 @@ func Middleware(cfg *Config) request.HandlerFunc { cfg.SkipPaths = defConfig.SkipPaths } if cfg.CustomLogger == nil { - cfg.CustomLogger = log.Printf + cfg.CustomLogger = logger.LogInfo } return request.HandlerFunc(func(c *request.Context) error { diff --git a/project_templates/01_router/01_router_only/main.go b/project_templates/01_router/01_router_only/main.go index 3367bd8a..1dd8f6cf 100644 --- a/project_templates/01_router/01_router_only/main.go +++ b/project_templates/01_router/01_router_only/main.go @@ -1,8 +1,9 @@ package main import ( - "log" "net/http" + + "github.com/primadi/lokstra/common/logger" ) func main() { @@ -10,10 +11,10 @@ func main() { router := setupRouter() // Start the HTTP server - log.Println("Starting server on :3000") + logger.LogInfo("Starting server on :3000") router.PrintRoutes() if err := http.ListenAndServe(":3000", router); err != nil { - log.Fatal("Server failed to start:", err) + logger.LogPanic("Server failed to start: %v", err) } } diff --git a/project_templates/01_router/01_router_only/middleware.go b/project_templates/01_router/01_router_only/middleware.go index 9740f73f..320a1a1c 100644 --- a/project_templates/01_router/01_router_only/middleware.go +++ b/project_templates/01_router/01_router_only/middleware.go @@ -1,9 +1,9 @@ package main import ( - "log" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/request" ) @@ -19,7 +19,7 @@ func customLoggingMiddleware() request.HandlerFunc { path := c.R.URL.Path // Log the incoming request - log.Printf("[CUSTOM] Incoming request: %s %s", method, path) + logger.LogInfo("[CUSTOM] Incoming request: %s %s", method, path) // Continue processing the request // Call Next() to pass the request to the next handler in the chain @@ -29,7 +29,7 @@ func customLoggingMiddleware() request.HandlerFunc { duration := time.Since(startTime) // Log the completed request with timing - log.Printf("[CUSTOM] Completed request: %s %s - took %v", method, path, duration) + logger.LogInfo("[CUSTOM] Completed request: %s %s - took %v", method, path, duration) return err }) diff --git a/project_templates/01_router/02_single_app/main.go b/project_templates/01_router/02_single_app/main.go index d84e1852..725fe824 100644 --- a/project_templates/01_router/02_single_app/main.go +++ b/project_templates/01_router/02_single_app/main.go @@ -1,10 +1,10 @@ package main import ( - "log" "time" "github.com/primadi/lokstra" + "github.com/primadi/lokstra/common/logger" ) func main() { @@ -21,11 +21,11 @@ func main() { // Run the app with graceful shutdown (30 second timeout) // This handles SIGINT/SIGTERM signals automatically - log.Println("Starting application...") - log.Println("Press Ctrl+C to gracefully shutdown") + logger.LogInfo("Starting application...") + logger.LogInfo("Press Ctrl+C to gracefully shutdown") if err := app.Run(30 * time.Second); err != nil { - log.Fatal("Failed to start the app:", err) + logger.LogPanic("Failed to start the app: %v", err) } - log.Println("Application stopped gracefully") + logger.LogInfo("Application stopped gracefully") } diff --git a/project_templates/01_router/02_single_app/middleware.go b/project_templates/01_router/02_single_app/middleware.go index 9740f73f..320a1a1c 100644 --- a/project_templates/01_router/02_single_app/middleware.go +++ b/project_templates/01_router/02_single_app/middleware.go @@ -1,9 +1,9 @@ package main import ( - "log" "time" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/request" ) @@ -19,7 +19,7 @@ func customLoggingMiddleware() request.HandlerFunc { path := c.R.URL.Path // Log the incoming request - log.Printf("[CUSTOM] Incoming request: %s %s", method, path) + logger.LogInfo("[CUSTOM] Incoming request: %s %s", method, path) // Continue processing the request // Call Next() to pass the request to the next handler in the chain @@ -29,7 +29,7 @@ func customLoggingMiddleware() request.HandlerFunc { duration := time.Since(startTime) // Log the completed request with timing - log.Printf("[CUSTOM] Completed request: %s %s - took %v", method, path, duration) + logger.LogInfo("[CUSTOM] Completed request: %s %s - took %v", method, path, duration) return err }) diff --git a/project_templates/01_router/03_multi_app/main.go b/project_templates/01_router/03_multi_app/main.go index 181a911b..c011e0b4 100644 --- a/project_templates/01_router/03_multi_app/main.go +++ b/project_templates/01_router/03_multi_app/main.go @@ -1,10 +1,10 @@ package main import ( - "log" "time" "github.com/primadi/lokstra" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/project_templates/01_router/03_multi_app/adminapp" "github.com/primadi/lokstra/project_templates/01_router/03_multi_app/mainapp" ) @@ -24,14 +24,14 @@ func main() { // Run the server - starts all apps with graceful shutdown (30 second timeout) // Press Ctrl+C to gracefully shutdown all apps - log.Println("Starting multi-app server...") - log.Println("Main API: http://localhost:3000") - log.Println("Admin API: http://localhost:3001") - log.Println("Press Ctrl+C to gracefully shutdown") + logger.LogInfo("Starting multi-app server...") + logger.LogInfo("Main API: http://localhost:3000") + logger.LogInfo("Admin API: http://localhost:3001") + logger.LogInfo("Press Ctrl+C to gracefully shutdown") if err := server.Run(30 * time.Second); err != nil { - log.Fatal("Failed to start the server:", err) + logger.LogPanic("Failed to start the server: %v", err) } - log.Println("All applications stopped gracefully") + logger.LogInfo("All applications stopped gracefully") } diff --git a/project_templates/02_app_framework/01_medium_system/main.go b/project_templates/02_app_framework/01_medium_system/main.go index e79f2c10..b4fdc62e 100644 --- a/project_templates/02_app_framework/01_medium_system/main.go +++ b/project_templates/02_app_framework/01_medium_system/main.go @@ -3,8 +3,8 @@ package main import ( "fmt" - "github.com/primadi/lokstra/core/deploy" - "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra" + "github.com/primadi/lokstra/common/logger" ) func main() { @@ -18,12 +18,12 @@ func main() { // Set log level from environment variable LOKSTRA_LOG_LEVEL // Supported values: silent, error, warn, info, debug // Default: info - deploy.SetLogLevelFromEnv() + logger.SetLogLevelFromEnv() // Or set manually: - // deploy.SetLogLevel(deploy.LogLevelDebug) // Show all debug logs - // deploy.SetLogLevel(deploy.LogLevelInfo) // Default - // deploy.SetLogLevel(deploy.LogLevelSilent) // No logs + // logger.SetLogLevel(logger.LogLevelDebug) // Show all debug logs + // logger.SetLogLevel(logger.LogLevelInfo) // Default + // logger.SetLogLevel(logger.LogLevelSilent) // No logs // 1. Register service types registerServiceTypes() @@ -32,5 +32,11 @@ func main() { registerMiddlewareTypes() // 3. Run server from config - lokstra_registry.RunServerFromConfig() + if err := lokstra.LoadConfig(); err != nil { + logger.LogPanic("❌ Failed to load config:", err) + } + + if err := lokstra.RunConfiguredServer(); err != nil { + logger.LogPanic("❌ Failed to run server:", err) + } } diff --git a/project_templates/02_app_framework/01_medium_system/register.go b/project_templates/02_app_framework/01_medium_system/register.go index 6f7aefa0..af10bb20 100644 --- a/project_templates/02_app_framework/01_medium_system/register.go +++ b/project_templates/02_app_framework/01_medium_system/register.go @@ -1,9 +1,8 @@ package main import ( - "log" - "github.com/google/uuid" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/deploy" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/lokstra_registry" @@ -59,7 +58,7 @@ func requestLoggerFactory() request.HandlerFunc { return func(ctx *request.Context) error { // Before request reqID := uuid.New().String() - log.Printf("→ [%s] %s %s", reqID, ctx.R.Method, ctx.R.URL.Path) + logger.LogInfo("→ [%s] %s %s", reqID, ctx.R.Method, ctx.R.URL.Path) ctx.Set("request_id", reqID) // Process request @@ -67,9 +66,9 @@ func requestLoggerFactory() request.HandlerFunc { // After request if err != nil { - log.Printf("← [%s] ERROR: %v", reqID, err) + logger.LogError("← [%s] ERROR: %v", reqID, err) } else { - log.Printf("← [%s] SUCCESS (status: %d)", reqID, ctx.Resp.RespStatusCode) + logger.LogInfo("← [%s] SUCCESS (status: %d)", reqID, ctx.Resp.RespStatusCode) } return err diff --git a/project_templates/02_app_framework/01_medium_system/repository/order_repository.go b/project_templates/02_app_framework/01_medium_system/repository/order_repository.go index 83807bff..e871c02e 100644 --- a/project_templates/02_app_framework/01_medium_system/repository/order_repository.go +++ b/project_templates/02_app_framework/01_medium_system/repository/order_repository.go @@ -2,10 +2,10 @@ package repository import ( "fmt" - "log" "time" "github.com/primadi/lokstra/api_client" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/project_templates/02_app_framework/01_medium_system/domain/order" ) @@ -22,7 +22,7 @@ var _ order.OrderRepository = (*OrderRepositoryMemory)(nil) // NewOrderRepositoryMemory creates a new in-memory order repository with seed data func NewOrderRepositoryMemory(config map[string]any) *OrderRepositoryMemory { dsn := utils.GetValueFromMap(config, "dsn", "memory://orders") - log.Printf("⚙️ Initializing OrderRepositoryMemory with DSN: %s", dsn) + logger.LogInfo("⚙️ Initializing OrderRepositoryMemory with DSN: %s", dsn) repo := &OrderRepositoryMemory{ orders: make(map[int]*order.Order), diff --git a/project_templates/02_app_framework/01_medium_system/repository/user_repository.go b/project_templates/02_app_framework/01_medium_system/repository/user_repository.go index 09a43764..53900a86 100644 --- a/project_templates/02_app_framework/01_medium_system/repository/user_repository.go +++ b/project_templates/02_app_framework/01_medium_system/repository/user_repository.go @@ -2,9 +2,9 @@ package repository import ( "fmt" - "log" "github.com/primadi/lokstra/api_client" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/common/utils" "github.com/primadi/lokstra/project_templates/02_app_framework/01_medium_system/domain/user" ) @@ -21,7 +21,7 @@ var _ user.UserRepository = (*UserRepositoryMemory)(nil) // NewUserRepositoryMemory creates a new in-memory user repository with seed data func NewUserRepositoryMemory(config map[string]any) *UserRepositoryMemory { dsn := utils.GetValueFromMap(config, "dsn", "memory://users") - log.Printf("⚙️ Initializing UserRepositoryMemory with DSN: %s", dsn) + logger.LogInfo("⚙️ Initializing UserRepositoryMemory with DSN: %s", dsn) repo := &UserRepositoryMemory{ users: make(map[int]*user.User), diff --git a/project_templates/02_app_framework/02_enterprise_modular/main.go b/project_templates/02_app_framework/02_enterprise_modular/main.go index 64a1b3fd..c7659729 100644 --- a/project_templates/02_app_framework/02_enterprise_modular/main.go +++ b/project_templates/02_app_framework/02_enterprise_modular/main.go @@ -3,8 +3,8 @@ package main import ( "fmt" - "github.com/primadi/lokstra/core/deploy" - "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra" + "github.com/primadi/lokstra/common/logger" ) func main() { @@ -15,7 +15,7 @@ func main() { fmt.Println("╚═══════════════════════════════════════════════╝") fmt.Println("") - deploy.SetLogLevelFromEnv() + logger.SetLogLevelFromEnv() // 1. Register service types from all modules registerServiceTypes() @@ -25,5 +25,11 @@ func main() { // 3. Run server from config folder // Lokstra will automatically merge all YAML files in config/ folder - lokstra_registry.RunServerFromConfigFolder("config") + if err := lokstra.LoadConfigFromFolder("config"); err != nil { + logger.LogPanic("❌ Failed to load config:", err) + } + + if err := lokstra.RunConfiguredServer(); err != nil { + logger.LogPanic("❌ Failed to run server:", err) + } } diff --git a/project_templates/02_app_framework/02_enterprise_modular/register.go b/project_templates/02_app_framework/02_enterprise_modular/register.go index 82893b1a..550377b9 100644 --- a/project_templates/02_app_framework/02_enterprise_modular/register.go +++ b/project_templates/02_app_framework/02_enterprise_modular/register.go @@ -1,9 +1,8 @@ package main import ( - "log" - "github.com/google/uuid" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/middleware/recovery" @@ -30,7 +29,7 @@ func requestLoggerFactory() request.HandlerFunc { return func(ctx *request.Context) error { // Before request reqID := uuid.New().String() - log.Printf("→ [%s] %s %s", reqID, ctx.R.Method, ctx.R.URL.Path) + logger.LogInfo("→ [%s] %s %s", reqID, ctx.R.Method, ctx.R.URL.Path) ctx.Set("request_id", reqID) // Process request @@ -38,9 +37,9 @@ func requestLoggerFactory() request.HandlerFunc { // After request if err != nil { - log.Printf("← [%s] ERROR: %v", reqID, err) + logger.LogError("← [%s] ERROR: %v", reqID, err) } else { - log.Printf("← [%s] SUCCESS (status: %d)", reqID, ctx.Resp.RespStatusCode) + logger.LogInfo("← [%s] SUCCESS (status: %d)", reqID, ctx.Resp.RespStatusCode) } return err diff --git a/project_templates/02_app_framework/03_enterprise_router_service/config/deployment.yaml b/project_templates/02_app_framework/03_enterprise_router_service/config/deployment.yaml index 1a078afb..f9fdca44 100644 --- a/project_templates/02_app_framework/03_enterprise_router_service/config/deployment.yaml +++ b/project_templates/02_app_framework/03_enterprise_router_service/config/deployment.yaml @@ -6,7 +6,7 @@ configs: order-repository: order-repository named-db-pools: - global-db: + db_main: dsn: ${GLOBAL_DB_DSN:postgres://postgres:adm1n@localhost:5432/lokstra_db} schema: ${GLOBAL_DB_SCHEMA:lokstra_auth} diff --git a/project_templates/02_app_framework/03_enterprise_router_service/main.go b/project_templates/02_app_framework/03_enterprise_router_service/main.go index 47467a6b..367a2c8b 100644 --- a/project_templates/02_app_framework/03_enterprise_router_service/main.go +++ b/project_templates/02_app_framework/03_enterprise_router_service/main.go @@ -2,56 +2,32 @@ package main import ( "fmt" - "log" "github.com/primadi/lokstra" - "github.com/primadi/lokstra/core/deploy" ) // NEW RECOMMENDED FLOW // This flow separates config loading from service registration, // allowing services to access config during registration. func main() { - lokstra.SetLogLevel(lokstra.LogLevelDebug) - - lokstra.Bootstrap() - - fmt.Println("") - fmt.Println("╔═══════════════════════════════════════════════╗") - fmt.Println("║ LOKSTRA ENTERPRISE MODULAR TEMPLATE ║") - fmt.Println("║ Domain-Driven Design with Bounded Contexts ║") - fmt.Println("║ [Config First] ║") - fmt.Println("╚═══════════════════════════════════════════════╝") - fmt.Println("") - - deploy.SetLogLevelFromEnv() - - // ===== STEP 1: Load Config ===== - // Config is loaded first, making it available for service/middleware registration - // This registers lazy load services and deployment structure from YAML - if err := lokstra.LoadConfigFromFolder("config"); err != nil { - log.Fatal("❌ Failed to load config:", err) - } - - // ===== STEP 2: Register Service Types ===== - // At this point, config is already loaded and available - // Service factories can now access config via lokstra_registry.GetConfig() - registerServiceTypes() - - // ===== STEP 3: Register Manual Routers ===== - // Register routers that are not generated from @RouterService annotations - registerRouters() - - // ===== STEP 4: Register Middleware Types ===== - // Middleware factories can also access config if needed - registerMiddlewareTypes() - - // ===== STEP 5: Initialize and Run Server ===== - // This will: - // - Select server based on config (or auto-select first server) - // - Read shutdown timeout from config - // - Start the server - if err := lokstra.InitAndRunServer(); err != nil { - log.Fatal("❌ Failed to run server:", err) + if err := lokstra.BootstrapAndRun( + // lokstra.WithLogLevel(logger.LogLevelDebug), + lokstra.WithoutDbMigrations(), + lokstra.WithServerInitFunc(func() error { + fmt.Println("") + fmt.Println("╔═══════════════════════════════════════════════╗") + fmt.Println("║ LOKSTRA ENTERPRISE MODULAR TEMPLATE ║") + fmt.Println("║ Domain-Driven Design with Bounded Contexts ║") + fmt.Println("║ [Config First] ║") + fmt.Println("╚═══════════════════════════════════════════════╝") + fmt.Println("") + + registerServiceTypes() + registerRouters() + registerMiddlewareTypes() + + return nil + })); err != nil { + panic("❌ Failed to initialize lokstra:" + err.Error()) } } diff --git a/project_templates/02_app_framework/03_enterprise_router_service/main_alt.go b/project_templates/02_app_framework/03_enterprise_router_service/main_alt.go index cb7be419..d5897f05 100644 --- a/project_templates/02_app_framework/03_enterprise_router_service/main_alt.go +++ b/project_templates/02_app_framework/03_enterprise_router_service/main_alt.go @@ -4,8 +4,7 @@ import ( "fmt" "github.com/primadi/lokstra" - "github.com/primadi/lokstra/core/deploy" - "github.com/primadi/lokstra/lokstra_registry" + "github.com/primadi/lokstra/common/logger" ) func AltMain() { @@ -18,7 +17,7 @@ func AltMain() { fmt.Println("╚═══════════════════════════════════════════════╝") fmt.Println("") - deploy.SetLogLevelFromEnv() + logger.SetLogLevelFromEnv() // 1. Register service types from all modules registerServiceTypes() @@ -28,5 +27,11 @@ func AltMain() { // 3. Run server from config folder // Lokstra will automatically merge all YAML files in config/ folder - lokstra_registry.RunServerFromConfigFolder("config") + if err := lokstra.LoadConfigFromFolder("config"); err != nil { + logger.LogPanic("❌ Failed to load config:", err) + } + + if err := lokstra.RunConfiguredServer(); err != nil { + logger.LogPanic("❌ Failed to run server:", err) + } } diff --git a/project_templates/02_app_framework/03_enterprise_router_service/modules/order/application/zz_cache.lokstra.json b/project_templates/02_app_framework/03_enterprise_router_service/modules/order/application/zz_cache.lokstra.json index 7596df6d..df6f6f8c 100644 --- a/project_templates/02_app_framework/03_enterprise_router_service/modules/order/application/zz_cache.lokstra.json +++ b/project_templates/02_app_framework/03_enterprise_router_service/modules/order/application/zz_cache.lokstra.json @@ -12,6 +12,6 @@ "generated_mod_time": "2025-12-10T01:55:27.3979337+07:00" } }, - "updated_at": "2025-12-10T03:57:17.4526734+07:00", + "updated_at": "2025-12-12T15:39:36.8536949+07:00", "generated_checksum": "3ae8ac2a982d5877f5c7da1f9297cf5364ea09253728c7980f9c8cc42f1415b0" } \ No newline at end of file diff --git a/project_templates/02_app_framework/03_enterprise_router_service/modules/user/application/zz_cache.lokstra.json b/project_templates/02_app_framework/03_enterprise_router_service/modules/user/application/zz_cache.lokstra.json index fa477d8a..63956bf2 100644 --- a/project_templates/02_app_framework/03_enterprise_router_service/modules/user/application/zz_cache.lokstra.json +++ b/project_templates/02_app_framework/03_enterprise_router_service/modules/user/application/zz_cache.lokstra.json @@ -12,6 +12,6 @@ "generated_mod_time": "2025-12-10T03:50:06.636111+07:00" } }, - "updated_at": "2025-12-10T03:57:17.4526734+07:00", + "updated_at": "2025-12-12T15:39:36.8554716+07:00", "generated_checksum": "f1d4037b46a04ef378b4d985efb148c26f695e3f6db1601bee944b4992502afb" } \ No newline at end of file diff --git a/project_templates/02_app_framework/03_enterprise_router_service/register.go b/project_templates/02_app_framework/03_enterprise_router_service/register.go index 3406a5de..8073d2f1 100644 --- a/project_templates/02_app_framework/03_enterprise_router_service/register.go +++ b/project_templates/02_app_framework/03_enterprise_router_service/register.go @@ -1,9 +1,8 @@ package main import ( - "log" - "github.com/google/uuid" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/core/request" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/middleware/recovery" @@ -22,7 +21,7 @@ func registerRouters() { // Register manual routers (not generated from @RouterService) healthRouter := NewHealthRouter() lokstra_registry.RegisterRouter("health-router", healthRouter) - log.Println("✅ Registered manual router: health-router") + logger.LogInfo("✅ Registered manual router: health-router") } func registerMiddlewareTypes() { @@ -34,9 +33,9 @@ func registerMiddlewareTypes() { lokstra_registry.RegisterMiddlewareFactory("simple-auth", simpleAuthFactory) lokstra_registry.RegisterMiddlewareFactory("mw-test", func(config map[string]any) request.HandlerFunc { return func(ctx *request.Context) error { - log.Printf("→ [mw-test] Before request | Param1: %v, Param2: %v", config["param1"], config["param2"]) + logger.LogInfo("→ [mw-test] Before request | Param1: %v, Param2: %v", config["param1"], config["param2"]) err := ctx.Next() - log.Println("← [mw-test] After request") + logger.LogInfo("← [mw-test] After request") return err } }) @@ -46,7 +45,7 @@ func requestLoggerFactory(config map[string]any) request.HandlerFunc { return func(ctx *request.Context) error { // Before request reqID := uuid.New().String() - log.Printf("→ [%s] %s %s", reqID, ctx.R.Method, ctx.R.URL.Path) + logger.LogInfo("→ [%s] %s %s", reqID, ctx.R.Method, ctx.R.URL.Path) ctx.Set("request_id", reqID) // Process request @@ -54,9 +53,9 @@ func requestLoggerFactory(config map[string]any) request.HandlerFunc { // After request if err != nil { - log.Printf("← [%s] ERROR: %v", reqID, err) + logger.LogInfo("← [%s] ERROR: %v", reqID, err) } else { - log.Printf("← [%s] SUCCESS (status: %d)", reqID, ctx.Resp.RespStatusCode) + logger.LogInfo("← [%s] SUCCESS (status: %d)", reqID, ctx.Resp.RespStatusCode) } return err @@ -73,14 +72,14 @@ func simpleAuthFactory(config map[string]any) request.HandlerFunc { // Check if Authorization header exists if authHeader == "" { - log.Printf("🔒 [simple-auth] Missing Authorization header") + logger.LogInfo("🔒 [simple-auth] Missing Authorization header") return ctx.Api.Unauthorized("Missing Authorization header") } // Check Bearer token format const bearerPrefix = "Bearer " if len(authHeader) < len(bearerPrefix) || authHeader[:len(bearerPrefix)] != bearerPrefix { - log.Printf("🔒 [simple-auth] Invalid Authorization format") + logger.LogInfo("🔒 [simple-auth] Invalid Authorization format") return ctx.Api.Unauthorized("Invalid Authorization format. Use 'Bearer '") } @@ -90,7 +89,7 @@ func simpleAuthFactory(config map[string]any) request.HandlerFunc { // Simple validation: accept tokens starting with "demo-" // In production, validate against database or JWT if len(token) < 5 || token[:5] != "demo-" { - log.Printf("🔒 [simple-auth] Invalid token: %s", token) + logger.LogInfo("🔒 [simple-auth] Invalid token: %s", token) return ctx.Api.Unauthorized("Invalid token") } @@ -99,7 +98,7 @@ func simpleAuthFactory(config map[string]any) request.HandlerFunc { ctx.Set("user_id", userID) ctx.Set("authenticated", true) - log.Printf("✅ [simple-auth] Authenticated user: %s", userID) + logger.LogInfo("✅ [simple-auth] Authenticated user: %s", userID) // Continue to next handler return ctx.Next() diff --git a/project_templates/02_app_framework/04_sync_config/config/config.yaml b/project_templates/02_app_framework/04_sync_config/config/config.yaml index 24767390..7854b1b7 100644 --- a/project_templates/02_app_framework/04_sync_config/config/config.yaml +++ b/project_templates/02_app_framework/04_sync_config/config/config.yaml @@ -3,12 +3,12 @@ configs: dbpool-manager: use_sync: true - # dbpool_name: global-db + # dbpool_name: db_main # heartbeat_interval: ${DBPOOL_MANAGER_HEARTBEAT_INTERVAL:5} # in minutes # reconnect_interval: ${DBPOOL_MANAGER_RECONNECT_INTERVAL:5} # in seconds named-db-pools: - global-db: + db_main: dsn: ${GLOBAL_DB_DSN:postgres://postgres:adm1n@localhost:5432/lokstra_db} schema: ${GLOBAL_DB_SCHEMA:lokstra_auth} diff --git a/project_templates/02_app_framework/04_sync_config/main.go b/project_templates/02_app_framework/04_sync_config/main.go index ec8074d0..c94d0189 100644 --- a/project_templates/02_app_framework/04_sync_config/main.go +++ b/project_templates/02_app_framework/04_sync_config/main.go @@ -22,7 +22,7 @@ func main() { registerRouters() // 5. Run the server - if err := lokstra.InitAndRunServer(); err != nil { + if err := lokstra.RunConfiguredServer(); err != nil { panic(err) } } diff --git a/project_templates/02_app_framework/04_sync_config/migrations/001_lokstra_core/migration.yaml b/project_templates/02_app_framework/04_sync_config/migrations/001_lokstra_core/migration.yaml index eeb2920a..b2f341a7 100644 --- a/project_templates/02_app_framework/04_sync_config/migrations/001_lokstra_core/migration.yaml +++ b/project_templates/02_app_framework/04_sync_config/migrations/001_lokstra_core/migration.yaml @@ -1,4 +1,4 @@ -dbpool-name: "global-db" +dbpool-name: "db_main" description: "Initial migration setup for lokstra core services" # schema-table: "schema_migrations" # force: "auto" diff --git a/project_templates/02_app_framework/test/main.go b/project_templates/02_app_framework/test/main.go index f3e8f709..f96218bf 100644 --- a/project_templates/02_app_framework/test/main.go +++ b/project_templates/02_app_framework/test/main.go @@ -17,7 +17,7 @@ func main() { registerRouters() // 4. Run the server - if err := lokstra.InitAndRunServer(); err != nil { + if err := lokstra.RunConfiguredServer(); err != nil { panic(err) } } diff --git a/serviceapi/dbpool.go b/serviceapi/dbpool.go index cad6a250..18c7f446 100644 --- a/serviceapi/dbpool.go +++ b/serviceapi/dbpool.go @@ -5,33 +5,16 @@ import ( ) // DbPool defines a connection pool interface -// supporting schema-aware connection acquisition +// supporting schema-aware or rlsID-aware connection acquisition // and future multi-backend support. type DbPool interface { - // Acquire a connection for the specified schema. - // If schema is empty, no search_path is set. - // For multi-tenant, use AcquireMultiTenant with tenantID. - Acquire(ctx context.Context, schema string) (DbConn, error) - - // Acquires a connection for the specified schema and tenantID. - // If schema is empty, no search_path is set. - // If tenantID is empty, no LOCAL app.current_tenant is set. - // This is useful for multi-tenant applications. - AcquireMultiTenant(ctx context.Context, schema string, tenantID string) (DbConn, error) - - Shutdownable -} - -type DbPoolWithSchema interface { Acquire(ctx context.Context) (DbConn, error) Shutdownable } -type DbPoolWithTenant interface { - Acquire(ctx context.Context) (DbConn, error) - - Shutdownable +type DbPoolSchemaRls interface { + SetSchemaRls(schema string, rlsID string) } type RowMap = map[string]any diff --git a/serviceapi/dbpool_manager.go b/serviceapi/dbpool_manager.go index d833204f..11f1bde4 100644 --- a/serviceapi/dbpool_manager.go +++ b/serviceapi/dbpool_manager.go @@ -6,35 +6,25 @@ import ( type DbPoolManager interface { // get or create DbPool for the given dsn - GetDsnPool(dsn string) (DbPool, error) + GetDbPool(dsn, schema, rlsID string) (DbPool, error) - // --------------------------------------- - // Tenant based pool management - //---------------------------------------- - - // set dsn and schema for the given tenant - SetTenantDsn(tenant string, dsn string, schema string) - // get dsn and schema for the given tenant - GetTenantDsn(tenant string) (string, string, error) - // get DbPool for the given tenant - GetTenantPool(tenant string) (DbPoolWithTenant, error) - // remove tenant mapping - RemoveTenant(tenant string) - // acquire connection for the given tenant - AcquireTenantConn(ctx context.Context, tenant string) (DbConn, error) + // acquire connection for the given dsn, schema, and rlsID + // if pool for dsn does not exist, create it + // if schema and rlsID are provided, set them accordingly + AcquireConn(ctx context.Context, dsn string, schema string, rlsID string) (DbConn, error) // --------------------------------------- // Named based pool management //---------------------------------------- - // set dsn and schema for the given name - SetNamedDsn(name string, dsn string, schema string) - // get dsn and schema for the given name - GetNamedDsn(name string) (string, string, error) + // set name for the given dsn, schema, and rlsID + SetNamedDbPool(name string, dsn string, schema string, rlsID string) + // get dsn, schema, rlsID for the given name + GetNamedDbPoolInfo(name string) (string, string, string, error) // get DbPool for the given name - GetNamedPool(name string) (DbPoolWithSchema, error) + GetNamedDbPool(name string) (DbPool, error) // remove name mapping - RemoveNamed(name string) + RemoveNamedDbPool(name string) // acquire connection for the given name AcquireNamedConn(ctx context.Context, name string) (DbConn, error) diff --git a/services/dbpool_manager/dbpool_manager.go b/services/dbpool_manager/dbpool_manager.go new file mode 100644 index 00000000..97ba356b --- /dev/null +++ b/services/dbpool_manager/dbpool_manager.go @@ -0,0 +1,145 @@ +package dbpool_manager + +import ( + "context" + "errors" + "sync" + + "github.com/primadi/lokstra/serviceapi" + "github.com/primadi/lokstra/services/dbpool_pg" +) + +// DbPoolInfo holds database DSN, schema name, and RLS ID +type DbPoolInfo struct { + Dsn string + Schema string + RlsId string +} + +type DbPoolManager struct { + pools map[string]serviceapi.DbPool // key: dsn + namedPools map[string]*DbPoolInfo // key: name + mu sync.RWMutex + newPoolFunc func(dsn, schema, rlsID string) (serviceapi.DbPool, error) +} + +// AcquireConn implements serviceapi.DbPoolManager. +func (p *DbPoolManager) AcquireConn(ctx context.Context, dsn string, schema string, rlsID string) (serviceapi.DbConn, error) { + p.mu.Lock() + defer p.mu.Unlock() + + dbPool, ok := p.pools[dsn] + if !ok { + var err error + dbPool, err = p.newPoolFunc(dsn, schema, rlsID) + if err != nil { + return nil, err + } + p.pools[dsn] = dbPool + } + + dbPoolWithRls, ok := dbPool.(serviceapi.DbPoolSchemaRls) + if !ok { + return nil, errors.New("dbpool: pool does not support schema or RLS") + } + dbPoolWithRls.SetSchemaRls(schema, rlsID) + + return dbPool.Acquire(ctx) +} + +// AcquireNamedConn implements serviceapi.DbPoolManager. +func (p *DbPoolManager) AcquireNamedConn(ctx context.Context, name string) (serviceapi.DbConn, error) { + p.mu.RLock() + dbPoolInfo, ok := p.namedPools[name] + p.mu.RUnlock() + if !ok { + return nil, errors.New("dbpool: named pool not found: " + name) + } + return p.AcquireConn(ctx, dbPoolInfo.Dsn, dbPoolInfo.Schema, dbPoolInfo.RlsId) +} + +// GetDbPool implements serviceapi.DbPoolManager. +func (p *DbPoolManager) GetDbPool(dsn string, schema string, rlsID string) (serviceapi.DbPool, error) { + p.mu.Lock() + defer p.mu.Unlock() + + dbPool, ok := p.pools[dsn] + if ok { + return dbPool, nil + } + newPool, err := p.newPoolFunc(dsn, schema, rlsID) + if err != nil { + return nil, err + } + p.pools[dsn] = newPool + return newPool, nil +} + +// GetNamedDbPool implements serviceapi.DbPoolManager. +func (p *DbPoolManager) GetNamedDbPool(name string) (serviceapi.DbPool, error) { + p.mu.RLock() + dbPoolInfo, ok := p.namedPools[name] + p.mu.RUnlock() + if !ok { + return nil, errors.New("dbpool: named pool not found: " + name) + } + return p.GetDbPool(dbPoolInfo.Dsn, dbPoolInfo.Schema, dbPoolInfo.RlsId) +} + +// GetNamedDbPoolInfo implements serviceapi.DbPoolManager. +func (p *DbPoolManager) GetNamedDbPoolInfo(name string) (string, string, string, error) { + p.mu.RLock() + dbPoolInfo, ok := p.namedPools[name] + p.mu.RUnlock() + if !ok { + return "", "", "", errors.New("dbpool: named pool not found: " + name) + } + return dbPoolInfo.Dsn, dbPoolInfo.Schema, dbPoolInfo.RlsId, nil +} + +// RemoveNamedDbPool implements serviceapi.DbPoolManager. +func (p *DbPoolManager) RemoveNamedDbPool(name string) { + p.mu.Lock() + defer p.mu.Unlock() + delete(p.namedPools, name) +} + +// SetNamedDbPool implements serviceapi.DbPoolManager. +func (p *DbPoolManager) SetNamedDbPool(name string, dsn string, schema string, rlsID string) { + p.mu.Lock() + defer p.mu.Unlock() + p.namedPools[name] = &DbPoolInfo{ + Dsn: dsn, + Schema: schema, + RlsId: rlsID, + } +} + +// Shutdown implements serviceapi.DbPoolManager. +func (p *DbPoolManager) Shutdown() error { + p.mu.Lock() + defer p.mu.Unlock() + + for _, pool := range p.pools { + _ = pool.Shutdown() + } + + return nil +} + +var _ serviceapi.DbPoolManager = (*DbPoolManager)(nil) + +func NewPoolManager(newPoolFunc func(dsn, schema, rlsID string) (serviceapi.DbPool, error)) serviceapi.DbPoolManager { + return &DbPoolManager{ + pools: make(map[string]serviceapi.DbPool), + namedPools: make(map[string]*DbPoolInfo), + newPoolFunc: newPoolFunc, + } +} + +func NewPgxPoolManager() serviceapi.DbPoolManager { + return NewPoolManager(func(dsn, schema, rlsID string) (serviceapi.DbPool, error) { + return dbpool_pg.NewPgxPostgresPool(dsn, schema, rlsID) + }, + ) +} diff --git a/services/dbpool_manager/pool_manager.go b/services/dbpool_manager/pool_manager.go deleted file mode 100644 index 17ec7849..00000000 --- a/services/dbpool_manager/pool_manager.go +++ /dev/null @@ -1,175 +0,0 @@ -package dbpool_manager - -import ( - "context" - "fmt" - "sync" - - "github.com/primadi/lokstra/serviceapi" - "github.com/primadi/lokstra/services/dbpool_pg" -) - -// DsnSchema holds database DSN and schema name -type DsnSchema struct { - Dsn string - Schema string -} - -// For backward compatibility with internal code -type dsnSchema = DsnSchema - -type PoolManager struct { - pools *sync.Map // map[dsn]serviceapi.DbPool - aliasPools *sync.Map // map[alias]dsnSchema (unified tenant and named pools) - newPoolFunc func(dsn string) (serviceapi.DbPool, error) -} - -var _ serviceapi.DbPoolManager = (*PoolManager)(nil) - -func NewPoolManager(newPoolFunc func(dsn string) (serviceapi.DbPool, error)) serviceapi.DbPoolManager { - return &PoolManager{ - pools: &sync.Map{}, - aliasPools: &sync.Map{}, - newPoolFunc: newPoolFunc, - } -} - -func NewPgxPoolManager() serviceapi.DbPoolManager { - return &PoolManager{ - pools: &sync.Map{}, - aliasPools: &sync.Map{}, - newPoolFunc: func(dsn string) (serviceapi.DbPool, error) { - return dbpool_pg.NewPgxPostgresPool(context.Background(), dsn) - }, - } -} - -// AcquireNamedConn implements serviceapi.DbPoolManager. -func (m *PoolManager) AcquireNamedConn(ctx context.Context, name string) (serviceapi.DbConn, error) { - return m.acquireAliasConn(ctx, "named:"+name, false) -} - -// AcquireTenantConn implements serviceapi.DbPoolManager. -func (m *PoolManager) AcquireTenantConn(ctx context.Context, tenant string) (serviceapi.DbConn, error) { - return m.acquireAliasConn(ctx, "tenant:"+tenant, true) -} - -// GetNamedDsn implements serviceapi.DbPoolManager. -func (m *PoolManager) GetNamedDsn(name string) (string, string, error) { - return m.getAliasDsn("named:" + name) -} - -// GetNamedPool implements serviceapi.DbPoolManager. -func (m *PoolManager) GetNamedPool(name string) (serviceapi.DbPoolWithSchema, error) { - dsn, schema, err := m.GetNamedDsn(name) - if err != nil { - return nil, err - } - dbPool, err := m.GetDsnPool(dsn) - if err != nil { - return nil, err - } - return dbpool_pg.NewDbPoolWithSchema(dbPool, schema), nil -} - -// GetTenantDsn implements serviceapi.DbPoolManager. -func (m *PoolManager) GetTenantDsn(tenant string) (string, string, error) { - return m.getAliasDsn("tenant:" + tenant) -} - -// GetTenantPool implements serviceapi.DbPoolManager. -func (m *PoolManager) GetTenantPool(tenant string) (serviceapi.DbPoolWithTenant, error) { - dsn, schema, err := m.GetTenantDsn(tenant) - if err != nil { - return nil, err - } - dbPool, err := m.GetDsnPool(dsn) - if err != nil { - return nil, err - } - return dbpool_pg.NewDbPoolWithTenant(dbPool, schema, tenant), nil -} - -// RemoveNamed implements serviceapi.DbPoolManager. -func (m *PoolManager) RemoveNamed(name string) { - m.removeAlias("named:" + name) -} - -// RemoveTenant implements serviceapi.DbPoolManager. -func (m *PoolManager) RemoveTenant(tenant string) { - m.removeAlias("tenant:" + tenant) -} - -// SetNamedDsn implements serviceapi.DbPoolManager. -func (m *PoolManager) SetNamedDsn(name string, dsn string, schema string) { - m.setAlias("named:"+name, dsn, schema) -} - -// SetTenantDsn implements serviceapi.DbPoolManager. -func (m *PoolManager) SetTenantDsn(tenant string, dsn string, schema string) { - m.setAlias("tenant:"+tenant, dsn, schema) -} - -func (m *PoolManager) GetDsnPool(dsn string) (serviceapi.DbPool, error) { - if pool, ok := m.pools.Load(dsn); ok { - if ok { - return pool.(serviceapi.DbPool), nil - } - } - - newPool, err := m.newPoolFunc(dsn) - if err != nil { - return nil, err - } - - pool, _ := m.pools.LoadOrStore(dsn, newPool) - return pool.(serviceapi.DbPool), nil -} - -func (m *PoolManager) Shutdown() error { - m.pools.Range(func(key, value any) bool { - pool := value.(serviceapi.DbPool) - _ = pool.Shutdown() - return true - }) - - return nil -} - -// ======================================== -// Internal helper methods -// ======================================== - -func (m *PoolManager) setAlias(alias, dsn, schema string) { - m.aliasPools.Store(alias, dsnSchema{Dsn: dsn, Schema: schema}) -} - -func (m *PoolManager) getAliasDsn(alias string) (string, string, error) { - _ds, ok := m.aliasPools.Load(alias) - if !ok { - return "", "", fmt.Errorf("alias pool not found: %s", alias) - } - ds := _ds.(dsnSchema) - return ds.Dsn, ds.Schema, nil -} - -func (m *PoolManager) removeAlias(alias string) { - m.aliasPools.Delete(alias) -} - -func (m *PoolManager) acquireAliasConn(ctx context.Context, alias string, isTenant bool) (serviceapi.DbConn, error) { - dsn, schema, err := m.getAliasDsn(alias) - if err != nil { - return nil, err - } - pool, err := m.GetDsnPool(dsn) - if err != nil { - return nil, err - } - if isTenant { - // Extract tenant ID from alias (remove "tenant:" prefix) - tenantID := alias[7:] // len("tenant:") = 7 - return pool.AcquireMultiTenant(ctx, schema, tenantID) - } - return pool.Acquire(ctx, schema) -} diff --git a/services/dbpool_manager/sync_pool_manager.go b/services/dbpool_manager/sync_pool_manager.go index a93c08fe..decb2a0c 100644 --- a/services/dbpool_manager/sync_pool_manager.go +++ b/services/dbpool_manager/sync_pool_manager.go @@ -2,6 +2,7 @@ package dbpool_manager import ( "context" + "errors" "sync" "github.com/primadi/lokstra/serviceapi" @@ -9,162 +10,120 @@ import ( "github.com/primadi/lokstra/syncmap" ) -type SyncPoolManager struct { - pools *sync.Map // map[dsn]serviceapi.DbPool - aliasPools *syncmap.SyncMap[*dsnSchema] // unified tenant and named pools - newPoolFunc func(dsn string) (serviceapi.DbPool, error) +type SyncDbPoolManager struct { + pools map[string]serviceapi.DbPool // key: dsn + namedPools *syncmap.SyncMap[*DbPoolInfo] // key: name + mu sync.RWMutex + newPoolFunc func(dsn, schema, rlsID string) (serviceapi.DbPool, error) } -var _ serviceapi.DbPoolManager = (*SyncPoolManager)(nil) +// AcquireConn implements serviceapi.DbPoolManager. +func (p *SyncDbPoolManager) AcquireConn(ctx context.Context, dsn string, schema string, rlsID string) (serviceapi.DbConn, error) { + p.mu.Lock() + defer p.mu.Unlock() -func NewSyncPoolManager( - aliasPools *syncmap.SyncMap[*dsnSchema], - newPoolFunc func(dsn string) (serviceapi.DbPool, error), -) serviceapi.DbPoolManager { - return &SyncPoolManager{ - pools: &sync.Map{}, - aliasPools: aliasPools, - newPoolFunc: newPoolFunc, + dbPool, ok := p.pools[dsn] + if !ok { + var err error + dbPool, err = p.newPoolFunc(dsn, schema, rlsID) + if err != nil { + return nil, err + } + p.pools[dsn] = dbPool } -} -func NewPgxSyncPoolManager( - aliasPools *syncmap.SyncMap[*dsnSchema], -) serviceapi.DbPoolManager { - return &SyncPoolManager{ - pools: &sync.Map{}, - aliasPools: aliasPools, - newPoolFunc: func(dsn string) (serviceapi.DbPool, error) { - return dbpool_pg.NewPgxPostgresPool(context.Background(), dsn) - }, + dbPoolWithRls, ok := dbPool.(serviceapi.DbPoolSchemaRls) + if !ok { + return nil, errors.New("dbpool: pool does not support schema or RLS") } -} + dbPoolWithRls.SetSchemaRls(schema, rlsID) -// AcquireNamedConn implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) AcquireNamedConn(ctx context.Context, name string) (serviceapi.DbConn, error) { - return m.acquireAliasConn(ctx, "named:"+name, false) + return dbPool.Acquire(ctx) } -// AcquireTenantConn implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) AcquireTenantConn(ctx context.Context, tenant string) (serviceapi.DbConn, error) { - return m.acquireAliasConn(ctx, "tenant:"+tenant, true) -} - -// GetNamedDsn implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) GetNamedDsn(name string) (string, string, error) { - return m.getAliasDsn("named:" + name) -} - -// GetNamedPool implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) GetNamedPool(name string) (serviceapi.DbPoolWithSchema, error) { - dsn, schema, err := m.GetNamedDsn(name) - if err != nil { - return nil, err - } - dbPool, err := m.GetDsnPool(dsn) - if err != nil { - return nil, err +// AcquireNamedConn implements serviceapi.DbPoolManager. +func (p *SyncDbPoolManager) AcquireNamedConn(ctx context.Context, name string) (serviceapi.DbConn, error) { + dbPoolInfo, ok := p.namedPools.Load(name) + if !ok { + return nil, errors.New("dbpool: named pool not found: " + name) } - return dbpool_pg.NewDbPoolWithSchema(dbPool, schema), nil + return p.AcquireConn(ctx, dbPoolInfo.Dsn, dbPoolInfo.Schema, dbPoolInfo.RlsId) } -// GetTenantDsn implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) GetTenantDsn(tenant string) (string, string, error) { - return m.getAliasDsn("tenant:" + tenant) -} +// GetDbPool implements serviceapi.DbPoolManager. +func (p *SyncDbPoolManager) GetDbPool(dsn string, schema string, rlsID string) (serviceapi.DbPool, error) { + p.mu.Lock() + defer p.mu.Unlock() -// GetTenantPool implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) GetTenantPool(tenant string) (serviceapi.DbPoolWithTenant, error) { - dsn, schema, err := m.GetTenantDsn(tenant) - if err != nil { - return nil, err + dbPool, ok := p.pools[dsn] + if ok { + return dbPool, nil } - dbPool, err := m.GetDsnPool(dsn) + newPool, err := p.newPoolFunc(dsn, schema, rlsID) if err != nil { return nil, err } - return dbpool_pg.NewDbPoolWithTenant(dbPool, schema, tenant), nil + p.pools[dsn] = newPool + return newPool, nil } -// RemoveNamed implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) RemoveNamed(name string) { - m.removeAlias("named:" + name) +// GetNamedDbPool implements serviceapi.DbPoolManager. +func (p *SyncDbPoolManager) GetNamedDbPool(name string) (serviceapi.DbPool, error) { + dbPoolInfo, ok := p.namedPools.Load(name) + if !ok { + return nil, errors.New("dbpool: named pool not found: " + name) + } + return p.GetDbPool(dbPoolInfo.Dsn, dbPoolInfo.Schema, dbPoolInfo.RlsId) } -// RemoveTenant implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) RemoveTenant(tenant string) { - m.removeAlias("tenant:" + tenant) +// GetNamedDbPoolInfo implements serviceapi.DbPoolManager. +func (p *SyncDbPoolManager) GetNamedDbPoolInfo(name string) (string, string, string, error) { + dbPoolInfo, ok := p.namedPools.Load(name) + if !ok { + return "", "", "", errors.New("dbpool: named pool not found: " + name) + } + return dbPoolInfo.Dsn, dbPoolInfo.Schema, dbPoolInfo.RlsId, nil } -// SetNamedDsn implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) SetNamedDsn(name string, dsn string, schema string) { - m.setAlias("named:"+name, dsn, schema) +// RemoveNamedDbPool implements serviceapi.DbPoolManager. +func (p *SyncDbPoolManager) RemoveNamedDbPool(name string) { + p.namedPools.Delete(context.Background(), name) } -// SetTenantDsn implements serviceapi.DbPoolManager. -func (m *SyncPoolManager) SetTenantDsn(tenant string, dsn string, schema string) { - m.setAlias("tenant:"+tenant, dsn, schema) +// SetNamedDbPool implements serviceapi.DbPoolManager. +func (p *SyncDbPoolManager) SetNamedDbPool(name string, dsn string, schema string, rlsID string) { + p.namedPools.Store(name, &DbPoolInfo{ + Dsn: dsn, + Schema: schema, + RlsId: rlsID, + }) } -func (m *SyncPoolManager) GetDsnPool(dsn string) (serviceapi.DbPool, error) { - if pool, ok := m.pools.Load(dsn); ok { - if ok { - return pool.(serviceapi.DbPool), nil - } - } - - newPool, err := m.newPoolFunc(dsn) - if err != nil { - return nil, err - } +// Shutdown implements serviceapi.DbPoolManager. +func (p *SyncDbPoolManager) Shutdown() error { + p.mu.Lock() + defer p.mu.Unlock() - pool, _ := m.pools.LoadOrStore(dsn, newPool) - return pool.(serviceapi.DbPool), nil -} - -func (m *SyncPoolManager) Shutdown() error { - m.pools.Range(func(key, value any) bool { - pool := value.(serviceapi.DbPool) + for _, pool := range p.pools { _ = pool.Shutdown() - return true - }) + } return nil } -// ======================================== -// Internal helper methods -// ======================================== - -func (m *SyncPoolManager) setAlias(alias, dsn, schema string) { - _ = m.aliasPools.Set(context.Background(), alias, &dsnSchema{Dsn: dsn, Schema: schema}) -} +var _ serviceapi.DbPoolManager = (*SyncDbPoolManager)(nil) -func (m *SyncPoolManager) getAliasDsn(alias string) (string, string, error) { - ds, err := m.aliasPools.Get(context.Background(), alias) - if err != nil { - return "", "", err +func NewSyncDbPoolManager(syncName string, newPoolFunc func(dsn, schema, rlsID string) (serviceapi.DbPool, error)) serviceapi.DbPoolManager { + return &SyncDbPoolManager{ + pools: make(map[string]serviceapi.DbPool), + namedPools: syncmap.NewSyncMap[*DbPoolInfo](syncName), + newPoolFunc: newPoolFunc, } - return ds.Dsn, ds.Schema, nil -} - -func (m *SyncPoolManager) removeAlias(alias string) { - _ = m.aliasPools.Delete(context.Background(), alias) } -func (m *SyncPoolManager) acquireAliasConn(ctx context.Context, alias string, isTenant bool) (serviceapi.DbConn, error) { - dsn, schema, err := m.getAliasDsn(alias) - if err != nil { - return nil, err - } - pool, err := m.GetDsnPool(dsn) - if err != nil { - return nil, err - } - if isTenant { - // Extract tenant ID from alias (remove "tenant:" prefix) - tenantID := alias[7:] // len("tenant:") = 7 - return pool.AcquireMultiTenant(ctx, schema, tenantID) - } - return pool.Acquire(ctx, schema) +func NewPgxSyncDbPoolManager() serviceapi.DbPoolManager { + return NewSyncDbPoolManager("dbpool", + func(dsn, schema, rlsID string) (serviceapi.DbPool, error) { + return dbpool_pg.NewPgxPostgresPool(dsn, schema, rlsID) + }) } diff --git a/services/dbpool_pg/impl_postgres.go b/services/dbpool_pg/dbconn_postgres.go similarity index 68% rename from services/dbpool_pg/impl_postgres.go rename to services/dbpool_pg/dbconn_postgres.go index c96e19ee..4ddcf554 100644 --- a/services/dbpool_pg/impl_postgres.go +++ b/services/dbpool_pg/dbconn_postgres.go @@ -6,76 +6,11 @@ import ( "fmt" "reflect" - "github.com/primadi/lokstra/serviceapi" - "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" + "github.com/primadi/lokstra/serviceapi" ) -type pgxPostgresPool struct { - dsn string - pool *pgxpool.Pool -} - -var _ serviceapi.DbPool = (*pgxPostgresPool)(nil) - -func (p *pgxPostgresPool) GetSetting(key string) any { - if key == "dsn" { - return p.dsn - } - return nil -} - -func NewPgxPostgresPool(ctx context.Context, dsn string) (*pgxPostgresPool, error) { - pool, err := pgxpool.New(ctx, dsn) - if err != nil { - return nil, err - } - if err := pool.Ping(ctx); err != nil { - return nil, err - } - return &pgxPostgresPool{ - dsn: dsn, - pool: pool, - }, nil -} - -// Shutdown implements serviceapi.DbPool. -func (p *pgxPostgresPool) Shutdown() error { - p.pool.Close() - return nil -} - -func (p *pgxPostgresPool) Acquire(ctx context.Context, schema string) (serviceapi.DbConn, error) { - return p.AcquireMultiTenant(ctx, schema, "") -} - -func (p *pgxPostgresPool) AcquireMultiTenant(ctx context.Context, schema string, tenantID string) (serviceapi.DbConn, error) { - conn, err := p.pool.Acquire(ctx) - if err != nil { - return nil, err - } - - if len(schema) > 0 { - stmt := "SET search_path TO " + pgx.Identifier{schema}.Sanitize() - if _, err := conn.Exec(ctx, stmt); err != nil { - conn.Release() - return nil, err - } - } - - if tenantID != "" { - // set RLS context - stmt := "SET LOCAL app.current_tenant = " + pgx.Identifier{tenantID}.Sanitize() - if _, err := conn.Exec(ctx, stmt); err != nil { - conn.Release() - return nil, err - } - } - - return &pgxConnWrapper{conn: conn}, nil -} - type pgxConnWrapper struct { conn *pgxpool.Conn } @@ -167,32 +102,6 @@ func pgxSelectMany(ctx context.Context, defer rows.Close() return pgx.CollectRows(rows, pgx.RowToMap) - // var resultSlice []map[string]any - // for rows.Next() { - // columns := rows.FieldDescriptions() - // values := make([]any, len(columns)) - // valuePtrs := make([]any, len(columns)) - - // for i := range columns { - // valuePtrs[i] = &values[i] - // } - - // if err := rows.Scan(valuePtrs...); err != nil { - // return nil, fmt.Errorf("failed to scan row: %w", err) - // } - - // rowMap := make(map[string]any) - // for i, col := range columns { - // rowMap[string(col.Name)] = values[i] - // } - // resultSlice = append(resultSlice, rowMap) - // } - - // if err := rows.Err(); err != nil { - // return nil, fmt.Errorf("error iterating rows: %w", err) - // } - - // return resultSlice, nil } func (c *pgxConnWrapper) SelectManyWithMapper(ctx context.Context, @@ -261,3 +170,5 @@ func (c *pgxConnWrapper) Release() error { c.conn.Release() return nil } + +var _ serviceapi.DbConn = (*pgxConnWrapper)(nil) diff --git a/services/dbpool_pg/dbpool_postgres.go b/services/dbpool_pg/dbpool_postgres.go new file mode 100644 index 00000000..fceed114 --- /dev/null +++ b/services/dbpool_pg/dbpool_postgres.go @@ -0,0 +1,75 @@ +package dbpool_pg + +import ( + "context" + + "github.com/primadi/lokstra/serviceapi" + + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgxpool" +) + +type pgxPostgresPool struct { + pool *pgxpool.Pool + dsn string + schema string + rlsID string +} + +// SetSchemaRls implements serviceapi.DbPoolSchemaRls. +func (p *pgxPostgresPool) SetSchemaRls(schema string, rlsID string) { + p.schema = schema + p.rlsID = rlsID +} + +// Shutdown implements serviceapi.DbPool. +func (p *pgxPostgresPool) Shutdown() error { + p.pool.Close() + return nil +} + +func (p *pgxPostgresPool) Acquire(ctx context.Context) (serviceapi.DbConn, error) { + conn, err := p.pool.Acquire(ctx) + if err != nil { + return nil, err + } + + if len(p.schema) > 0 { + stmt := "SET search_path TO " + pgx.Identifier{p.schema}.Sanitize() + if _, err := conn.Exec(ctx, stmt); err != nil { + conn.Release() + return nil, err + } + } + + if len(p.rlsID) > 0 { + // set RLS context + stmt := "SET LOCAL app.current_rls = " + pgx.Identifier{p.rlsID}.Sanitize() + if _, err := conn.Exec(ctx, stmt); err != nil { + conn.Release() + return nil, err + } + } + return &pgxConnWrapper{conn: conn}, nil +} + +var _ serviceapi.DbPool = (*pgxPostgresPool)(nil) +var _ serviceapi.DbPoolSchemaRls = (*pgxPostgresPool)(nil) + +func NewPgxPostgresPool(dsn string, schema string, rlsID string) (*pgxPostgresPool, error) { + ctx := context.Background() + pool, err := pgxpool.New(ctx, dsn) + if err != nil { + return nil, err + } + if err := pool.Ping(ctx); err != nil { + return nil, err + } + + return &pgxPostgresPool{ + pool: pool, + dsn: dsn, + schema: schema, + rlsID: rlsID, + }, nil +} diff --git a/services/dbpool_pg/impl_tx_postgres.go b/services/dbpool_pg/dbtx_postgres.go similarity index 100% rename from services/dbpool_pg/impl_tx_postgres.go rename to services/dbpool_pg/dbtx_postgres.go diff --git a/services/dbpool_pg/module.go b/services/dbpool_pg/module.go index d15cbf5b..cc4a2b9a 100644 --- a/services/dbpool_pg/module.go +++ b/services/dbpool_pg/module.go @@ -1,7 +1,6 @@ package dbpool_pg import ( - "context" "fmt" "strings" "time" @@ -69,7 +68,7 @@ func (cfg *Config) GetFinalDSN() string { func Service(cfg *Config) *pgxPostgresPool { dsn := cfg.GetFinalDSN() - svc, err := NewPgxPostgresPool(context.Background(), dsn) + svc, err := NewPgxPostgresPool(dsn, "", "") if err != nil { return nil } diff --git a/services/dbpool_pg/with_schema.go b/services/dbpool_pg/with_schema.go deleted file mode 100644 index 27b6f631..00000000 --- a/services/dbpool_pg/with_schema.go +++ /dev/null @@ -1,29 +0,0 @@ -package dbpool_pg - -import ( - "context" - - "github.com/primadi/lokstra/serviceapi" -) - -type PgxDbPoolWithSchema struct { - pool serviceapi.DbPool - schema string -} - -func NewDbPoolWithSchema(pool serviceapi.DbPool, schema string) serviceapi.DbPoolWithSchema { - return &PgxDbPoolWithSchema{ - pool: pool, - schema: schema, - } -} - -var _ serviceapi.DbPoolWithSchema = (*PgxDbPoolWithSchema)(nil) - -func (p *PgxDbPoolWithSchema) Acquire(ctx context.Context) (serviceapi.DbConn, error) { - return p.pool.Acquire(ctx, p.schema) -} - -func (p *PgxDbPoolWithSchema) Shutdown() error { - return p.pool.Shutdown() -} diff --git a/services/dbpool_pg/with_tenant.go b/services/dbpool_pg/with_tenant.go deleted file mode 100644 index ffa66533..00000000 --- a/services/dbpool_pg/with_tenant.go +++ /dev/null @@ -1,31 +0,0 @@ -package dbpool_pg - -import ( - "context" - - "github.com/primadi/lokstra/serviceapi" -) - -type PgxDbPoolWithTenant struct { - pool serviceapi.DbPool - schema string - tenantID string -} - -func NewDbPoolWithTenant(pool serviceapi.DbPool, schema string, tenantID string) serviceapi.DbPoolWithTenant { - return &PgxDbPoolWithTenant{ - pool: pool, - schema: schema, - tenantID: tenantID, - } -} - -var _ serviceapi.DbPoolWithTenant = (*PgxDbPoolWithTenant)(nil) - -func (p *PgxDbPoolWithTenant) Acquire(ctx context.Context) (serviceapi.DbConn, error) { - return p.pool.AcquireMultiTenant(ctx, p.schema, p.tenantID) -} - -func (p *PgxDbPoolWithTenant) Shutdown() error { - return p.pool.Shutdown() -} diff --git a/services/email_smtp/example/main.go b/services/email_smtp/example/main.go index dd046ed7..7dd1e06c 100644 --- a/services/email_smtp/example/main.go +++ b/services/email_smtp/example/main.go @@ -6,9 +6,9 @@ package main import ( "fmt" - "log" "github.com/primadi/lokstra" + "github.com/primadi/lokstra/common/logger" "github.com/primadi/lokstra/lokstra_registry" "github.com/primadi/lokstra/services/email_smtp" ) @@ -32,7 +32,7 @@ func main() { // Load config and run server (auto-registers services from deployments) if err := lokstra.LoadConfigFromFolder("configs"); err != nil { - log.Fatal(err) + logger.LogPanic(err.Error()) } // Register email_smtp service type @@ -44,7 +44,7 @@ func main() { return emailService.GetRouter() }) - if err := lokstra_registry.InitAndRunServer(); err != nil { - log.Fatal(err) + if err := lokstra_registry.RunConfiguredServer(); err != nil { + logger.LogPanic(err.Error()) } } diff --git a/services/register_all.go b/services/register_all.go index 8062d420..8ff89f05 100644 --- a/services/register_all.go +++ b/services/register_all.go @@ -21,5 +21,5 @@ func RegisterAllServices() { metrics_prometheus.Register() dbpool_pg.Register() email_smtp.Register() - sync_config_pg.Register() + sync_config_pg.Register("db_main") } diff --git a/services/sync_config_pg/config_test.yaml b/services/sync_config_pg/config_test.yaml index 497f3fa7..d9383d0b 100644 --- a/services/sync_config_pg/config_test.yaml +++ b/services/sync_config_pg/config_test.yaml @@ -3,11 +3,11 @@ configs: dbpool-manager: use-sync: true - dbpool-name: global-db + dbpool-name: db_main heartbeat-interval: ${DBPOOL_MANAGER_HEARTBEAT_INTERVAL:5} # in minutes reconnect-interval: ${DBPOOL_MANAGER_RECONNECT_INTERVAL:5} # in seconds named-db-pools: - global-db: + db_main: dsn: ${GLOBAL_DB_DSN:postgres://postgres:adm1n@localhost:5432/lokstra_db} schema: ${GLOBAL_DB_SCHEMA:lokstra_auth} \ No newline at end of file diff --git a/services/sync_config_pg/module.go b/services/sync_config_pg/module.go index 46e03966..d65bb3de 100644 --- a/services/sync_config_pg/module.go +++ b/services/sync_config_pg/module.go @@ -43,7 +43,7 @@ type subscriber struct { type syncConfigPG struct { cfg *Config - dbPool serviceapi.DbPoolWithSchema + dbPool serviceapi.DbPool listenerDB *pgxpool.Pool mu sync.RWMutex cache map[string]any @@ -105,7 +105,7 @@ func NewSyncConfigPG(cfg *Config) (serviceapi.SyncConfig, error) { // Create context with cancel for goroutine management ctx, cancel := context.WithCancel(context.Background()) - pool, err := dbpool_pg.NewPgxPostgresPool(ctx, dsn) + dbPool, err := dbpool_pg.NewPgxPostgresPool(dsn, schema, "") if err != nil { if listenerDB != nil { listenerDB.Close() @@ -114,7 +114,6 @@ func NewSyncConfigPG(cfg *Config) (serviceapi.SyncConfig, error) { return nil, fmt.Errorf("failed to create db pool: %w", err) } - dbPool := dbpool_pg.NewDbPoolWithSchema(pool, schema) service := &syncConfigPG{ cfg: cfg, dbPool: dbPool, @@ -557,7 +556,7 @@ func Service(cfg *Config) (serviceapi.SyncConfig, error) { // ServiceFactory creates a SyncConfig service from configuration map func ServiceFactory(mapCfg map[string]any) any { cfg := &Config{ - DbPoolName: utils.GetValueFromMap(mapCfg, "db_pool_name", "global-db"), + DbPoolName: utils.GetValueFromMap(mapCfg, "db_pool_name", "db_main"), TableName: utils.GetValueFromMap(mapCfg, "table_name", "sync_config"), Channel: utils.GetValueFromMap(mapCfg, "channel", "config_changes"), HeartbeatInterval: utils.GetValueFromMap(mapCfg, "heartbeat_interval", 5*time.Minute), @@ -575,20 +574,20 @@ func ServiceFactory(mapCfg map[string]any) any { } // Register registers the SyncConfig service type -func Register() { +func Register(dbPoolName string) { lokstra_registry.RegisterServiceType(SERVICE_TYPE, ServiceFactory) - SetDefaultSyncConfigPG() + SetDefaultSyncConfigPG(dbPoolName) } // registers the default SyncConfigPG service -func SetDefaultSyncConfigPG() { +func SetDefaultSyncConfigPG(syncDbPoolName string) { if lokstra_registry.HasService("sync-config") { return // Already registered } lokstra_registry.RegisterLazyService("sync-config", func() any { cfg := &Config{ - DbPoolName: "global-db", + DbPoolName: syncDbPoolName, TableName: "sync_config", Channel: "config_changes", SyncOnMismatch: true, diff --git a/services/sync_config_pg/module_test.go b/services/sync_config_pg/module_test.go index 9576f354..b16dc9af 100644 --- a/services/sync_config_pg/module_test.go +++ b/services/sync_config_pg/module_test.go @@ -24,7 +24,7 @@ func TestServiceFactory(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -79,7 +79,7 @@ func TestSyncConfig_Integration(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -167,7 +167,7 @@ func TestSubscribeAndCRC_Integration(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -241,7 +241,7 @@ func TestSync(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -267,7 +267,7 @@ func TestComplexDataTypes_Integration(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -366,7 +366,7 @@ func TestUpdateExistingKey_Integration(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -435,7 +435,7 @@ func TestUnsubscribe_Integration(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -504,7 +504,7 @@ func TestConcurrentOperations_Integration(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -562,7 +562,7 @@ func TestGetIntEdgeCases_Integration(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -602,7 +602,7 @@ func TestSyncReloadsData_Integration(t *testing.T) { loadConfig(t) cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 1 * time.Minute, @@ -659,7 +659,7 @@ func TestSingleton_Integration(t *testing.T) { ctx := context.Background() cfg := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config", Channel: "config_changes", HeartbeatInterval: 5 * time.Minute, @@ -704,7 +704,7 @@ func TestSingleton_Integration(t *testing.T) { // Create third instance with different config (different table) cfg3 := &sync_config_pg.Config{ - DbPoolName: "global-db", + DbPoolName: "db_main", TableName: "sync_config_different", // Different table Channel: "config_changes", HeartbeatInterval: 5 * time.Minute, diff --git a/services/sync_config_pg/test/config.yaml b/services/sync_config_pg/test/config.yaml index 24767390..7854b1b7 100644 --- a/services/sync_config_pg/test/config.yaml +++ b/services/sync_config_pg/test/config.yaml @@ -3,12 +3,12 @@ configs: dbpool-manager: use_sync: true - # dbpool_name: global-db + # dbpool_name: db_main # heartbeat_interval: ${DBPOOL_MANAGER_HEARTBEAT_INTERVAL:5} # in minutes # reconnect_interval: ${DBPOOL_MANAGER_RECONNECT_INTERVAL:5} # in seconds named-db-pools: - global-db: + db_main: dsn: ${GLOBAL_DB_DSN:postgres://postgres:adm1n@localhost:5432/lokstra_db} schema: ${GLOBAL_DB_SCHEMA:lokstra_auth} diff --git a/services/sync_config_pg/test/main.go b/services/sync_config_pg/test/main.go index f3e8f709..74647a2b 100644 --- a/services/sync_config_pg/test/main.go +++ b/services/sync_config_pg/test/main.go @@ -13,11 +13,15 @@ func main() { // 2. Load application config lokstra.LoadConfig("config.yaml") + lokstra.UsePgxDbPoolManager(true) + lokstra.UsePgxSyncConfig("db_main") + lokstra.LoadNamedDbPoolsFromConfig() + // 3. Register routers registerRouters() // 4. Run the server - if err := lokstra.InitAndRunServer(); err != nil { + if err := lokstra.RunConfiguredServer(); err != nil { panic(err) } } diff --git a/syncmap/syncmap.go b/syncmap/syncmap.go index 6a8b4538..ea5a87bc 100644 --- a/syncmap/syncmap.go +++ b/syncmap/syncmap.go @@ -156,6 +156,51 @@ func (sm *SyncMap[V]) Get(ctx context.Context, key string) (V, error) { return typedVal, nil } +func (sm *SyncMap[V]) Load(key string) (V, bool) { + var zero V + fullKey := sm.makeFullKey(key) + val, err := sm.config.Get(context.Background(), fullKey) + if err != nil { + return zero, false + } + typedVal, err := sm.convertValue(val) + if err != nil { + return zero, false + } + return typedVal, true +} + +func (sm *SyncMap[V]) Store(key string, value V) error { + fullKey := sm.makeFullKey(key) + return sm.config.Set(context.Background(), fullKey, value) +} + +func (sm *SyncMap[V]) LoadOrStore(key string, newFunc func() (V, error)) (V, bool, error) { + var zero V + fullKey := sm.makeFullKey(key) + val, err := sm.config.Get(context.Background(), fullKey) + if err == nil { + typedVal, err := sm.convertValue(val) + if err != nil { + return zero, false, err + } + return typedVal, true, nil + } + + // If not found, call newFunc to create a new value + newVal, err := newFunc() + if err != nil { + return zero, false, err + } + + // Store the new value + if err := sm.Store(key, newVal); err != nil { + return zero, false, err + } + + return newVal, false, nil +} + // deletes a key from the SyncMap func (sm *SyncMap[V]) Delete(ctx context.Context, key string) error { fullKey := sm.makeFullKey(key) diff --git a/tools/migration_runner/example/example_multi_db.go b/tools/migration_runner/example/example_multi_db.go index 721079d1..98a6b510 100644 --- a/tools/migration_runner/example/example_multi_db.go +++ b/tools/migration_runner/example/example_multi_db.go @@ -17,6 +17,11 @@ func main() { log.Fatalf("Failed to load config: %v", err) } + lokstra.UsePgxDbPoolManager(true) + + lokstra.UsePgxSyncConfig("db_main") + lokstra.LoadNamedDbPoolsFromConfig() + // OPTION 1: Auto-scan all migration folders (RECOMMENDED) // Scans multi_db/ for subdirectories, runs them in alphabetical order // Each folder loads its own migration.yaml configuration @@ -52,7 +57,7 @@ func main() { // lokstra migration status -dir multi_db/ledger-db // Start your application servers - if err := lokstra.InitAndRunServer(); err != nil { + if err := lokstra.RunConfiguredServer(); err != nil { log.Fatalf("Failed to start server: %v", err) } } diff --git a/tools/migration_runner/example/main.go b/tools/migration_runner/example/main.go index 5b213cc5..434114d7 100644 --- a/tools/migration_runner/example/main.go +++ b/tools/migration_runner/example/main.go @@ -44,19 +44,18 @@ func MainTest() { lokstra_registry.LoadConfigFromFolder("config") // Get database pool - dbPool, ok := lokstra_registry.GetServiceAny(*dbName) + pool, ok := lokstra_registry.GetServiceAny(*dbName) if !ok { log.Fatalf("❌ Database pool '%s' not found. Check your config.yaml named-db-pools section", *dbName) } - dbPoolWithSchema, ok := dbPool.(serviceapi.DbPoolWithSchema) + dbPool, ok := pool.(serviceapi.DbPool) if !ok { - log.Fatalf("❌ Service '%s' is not a DbPoolWithSchema", *dbName) + log.Fatalf("❌ Service '%s' is not a DbPool", *dbName) } // Create migration runner - runner := migration_runner.New(dbPoolWithSchema, *migrationsDir) - + runner := migration_runner.New(dbPool, *migrationsDir) ctx := context.Background() // Execute command diff --git a/tools/migration_runner/migration.go b/tools/migration_runner/migration.go index e816f33f..26e16424 100644 --- a/tools/migration_runner/migration.go +++ b/tools/migration_runner/migration.go @@ -23,14 +23,14 @@ type Migration struct { // Runner manages database migrations type Runner struct { - dbPool serviceapi.DbPoolWithSchema + dbPool serviceapi.DbPool migrationsDir string migrations []*Migration schemaTable string } // New creates a new migration runner -func New(dbPool serviceapi.DbPoolWithSchema, migrationsDir string) *Runner { +func New(dbPool serviceapi.DbPool, migrationsDir string) *Runner { return &Runner{ dbPool: dbPool, migrationsDir: migrationsDir, diff --git a/tools/migration_runner/migration_test.go b/tools/migration_runner/migration_test.go index 28a8b892..5f70f5ad 100644 --- a/tools/migration_runner/migration_test.go +++ b/tools/migration_runner/migration_test.go @@ -12,7 +12,7 @@ type MockDbPoolWithSchema struct { executions []string // Track executed SQL } -var _ serviceapi.DbPoolWithSchema = (*MockDbPoolWithSchema)(nil) +var _ serviceapi.DbPool = (*MockDbPoolWithSchema)(nil) func (m *MockDbPoolWithSchema) Acquire(ctx context.Context) (serviceapi.DbConn, error) { return &MockDbConn{pool: m}, nil