From f85d42a709deaa9e1a269685360712c504bfd3ef Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Mon, 6 Jul 2026 18:02:20 +0530 Subject: [PATCH 1/4] Enhance logging and configuration in platform API - Integrated a custom logger into the configuration loading process to ensure consistent log formatting across the application. - Updated the configuration file to enable SQLite database by default. - Improved source file path handling in the logger for better clarity in log messages. - Changed log output format for server startup to provide clearer information on the running mode. --- platform-api/src/config/config.go | 7 +++++++ platform-api/src/config/config.toml | 4 ++-- platform-api/src/internal/logger/logger.go | 7 +++++-- platform-api/src/internal/server/server.go | 13 ++++--------- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/platform-api/src/config/config.go b/platform-api/src/config/config.go index cd18d9ec18..791b7c172d 100644 --- a/platform-api/src/config/config.go +++ b/platform-api/src/config/config.go @@ -36,6 +36,8 @@ import ( kenv "github.com/knadh/koanf/providers/env" "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/v2" + + "platform-api/src/internal/logger" ) // FileBasedUser represents a built-in user for file-based auth mode. @@ -331,6 +333,11 @@ func LoadConfig(configPath string) (*Server, error) { return nil, fmt.Errorf("failed to unmarshal config: %w", err) } + // Install the configured logger as the slog default so the warnings/info logs + // emitted below (and any package-level slog.* call in this file) use the same + // format as the rest of the application, instead of slog's default handler. + slog.SetDefault(logger.NewLogger(logger.Config{Level: cfg.LogLevel, Format: cfg.LogFormat})) + if err := validateDefaultDevPortalConfig(&cfg.DefaultDevPortal); err != nil { return nil, err } diff --git a/platform-api/src/config/config.toml b/platform-api/src/config/config.toml index 0b87e375f3..3d164740ab 100644 --- a/platform-api/src/config/config.toml +++ b/platform-api/src/config/config.toml @@ -28,8 +28,8 @@ port = "9243" # Database # --------------------------------------------------------------------------- [database] -# driver = "sqlite3" # "sqlite3" or "postgres" -# path = "/app/data/api_platform.db" # SQLite file path (ignored for postgres) +driver = "sqlite3" # "sqlite3" or "postgres" +path = "/app/data/api_platform.db" # SQLite file path (ignored for postgres) # PostgreSQL — uncomment when driver = "postgres" # host = "localhost" diff --git a/platform-api/src/internal/logger/logger.go b/platform-api/src/internal/logger/logger.go index 7c49f0463f..c52babd505 100644 --- a/platform-api/src/internal/logger/logger.go +++ b/platform-api/src/internal/logger/logger.go @@ -43,12 +43,15 @@ func NewLogger(cfg Config) *slog.Logger { if src, ok := a.Value.Any().(*slog.Source); ok { // Extract path with special handling: // - hide "internal" prefix for internal package logs - // - keep "cmd" prefix for main entrypoint logs + // - keep the package prefix (cmd, config, plugins, ...) for everything else under platform-api/src + // - fall back to the path relative to the repo root for code outside platform-api/src (e.g. common/) file := src.File if idx := strings.Index(src.File, "platform-api/src/internal/"); idx != -1 { file = src.File[idx+len("platform-api/src/internal/"):] - } else if idx := strings.Index(src.File, "platform-api/src/cmd/"); idx != -1 { + } else if idx := strings.Index(src.File, "platform-api/src/"); idx != -1 { file = src.File[idx+len("platform-api/src/"):] + } else if idx := strings.LastIndex(src.File, "api-platform/"); idx != -1 { + file = src.File[idx+len("api-platform/"):] } return slog.String("source", fmt.Sprintf("%s:%d", file, src.Line)) } diff --git a/platform-api/src/internal/server/server.go b/platform-api/src/internal/server/server.go index 05e5577e3c..bd0086312f 100644 --- a/platform-api/src/internal/server/server.go +++ b/platform-api/src/internal/server/server.go @@ -804,17 +804,12 @@ func (s *Server) Start(port string, certDir string) error { errCh <- httpServer.ListenAndServeTLS("", "") }() - mode := "Production" + mode := "PRODUCTION" if demoMode() { - mode = "Demo" + mode = "DEMO" } - const termWidth = 80 - msg := fmt.Sprintf("=== Platform API started [%s] ===", mode) - pad := (termWidth - len(msg)) / 2 - if pad < 0 { - pad = 0 - } - fmt.Printf("\n%*s%s\n\n", pad, "", msg) + s.logger.Info("Platform API started", "mode=", mode) + fmt.Printf("\n >>> Platform API started mode=%s <<<\n\n", mode) quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) From 70b5f1ec4f88a4f19c333da13a6176ea802d9bd4 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Mon, 6 Jul 2026 18:25:07 +0530 Subject: [PATCH 2/4] Fix merge conflicts --- platform-api/config/config.go | 2 +- platform-api/internal/server/server.go | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/platform-api/config/config.go b/platform-api/config/config.go index 791b7c172d..8c65bb60e9 100644 --- a/platform-api/config/config.go +++ b/platform-api/config/config.go @@ -37,7 +37,7 @@ import ( "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/v2" - "platform-api/src/internal/logger" + "github.com/wso2/api-platform/platform-api/internal/logger" ) // FileBasedUser represents a built-in user for file-based auth mode. diff --git a/platform-api/internal/server/server.go b/platform-api/internal/server/server.go index be32381013..89c852753b 100644 --- a/platform-api/internal/server/server.go +++ b/platform-api/internal/server/server.go @@ -808,8 +808,9 @@ func (s *Server) Start(port string, certDir string) error { if demoMode() { mode = "DEMO" } - s.logger.Info("Platform API started", "mode=", mode) - fmt.Printf("\n >>> Platform API started mode=%s <<<\n\n", mode) + s.logger.Info("Platform API started", "mode", mode) + + fmt.Printf("\n >>> Platform API started [%s] <<<\n\n", mode) quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) From 48926639c77d62c713d3eb167ef9f6e8dba0375d Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Mon, 6 Jul 2026 23:52:27 +0530 Subject: [PATCH 3/4] Refactor startup banner for platform API and AI Workspace BFF - Replaced the console output for server startup with a new function `printStartedMarker` to provide a consistent and decorative banner style. - Updated the logging to include the mode of operation (PRODUCTION or DEMO) for better clarity during startup. - Removed the previous `printBanner` function in favor of the new implementation. --- platform-api/internal/server/server.go | 16 +++++++++- portals/ai-workspace/bff/main.go | 43 ++++++++++++-------------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/platform-api/internal/server/server.go b/platform-api/internal/server/server.go index 89c852753b..f3d12b8649 100644 --- a/platform-api/internal/server/server.go +++ b/platform-api/internal/server/server.go @@ -810,7 +810,7 @@ func (s *Server) Start(port string, certDir string) error { } s.logger.Info("Platform API started", "mode", mode) - fmt.Printf("\n >>> Platform API started [%s] <<<\n\n", mode) + printStartedMarker(mode) quit := make(chan os.Signal, 1) signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) @@ -847,6 +847,20 @@ func (s *Server) Start(port string, certDir string) error { } } +// printStartedMarker writes a large, prominent banner for humans watching +// the console, matching the gateway controller's startup banner style. It's +// purely decorative — the structured "Platform API started" slog line is the +// source of truth for log parsing. +func printStartedMarker(mode string) { + fmt.Print("\n\n" + + "========================================================================\n" + + "\n" + + " Platform API Started mode=" + mode + "\n" + + "\n" + + "========================================================================\n" + + "\n\n") +} + // GetMux returns the raw ServeMux for testing purposes. func (s *Server) GetMux() *http.ServeMux { return s.mux diff --git a/portals/ai-workspace/bff/main.go b/portals/ai-workspace/bff/main.go index 24c4fa8b1b..537f9585cb 100644 --- a/portals/ai-workspace/bff/main.go +++ b/portals/ai-workspace/bff/main.go @@ -37,28 +37,20 @@ import ( "ai-workspace-bff/internal/tlsutil" ) -// printBanner writes a multi-line startup banner horizontally centered in an -// 80-column terminal, with blank-line padding above and below the title. -func printBanner() { - const termWidth = 80 - lines := []string{ - "========================================", - "", - "", - "AI Workspace started", - "", - "", - "========================================", - } - fmt.Println() - for _, line := range lines { - pad := (termWidth - len(line)) / 2 - if pad < 0 { - pad = 0 - } - fmt.Printf("%*s%s\n", pad, "", line) - } - fmt.Println() +// printStartedMarker writes a large, prominent banner for humans watching +// the console, matching the gateway controller's startup banner style. It's +// purely decorative — the structured "AI Workspace BFF started" slog.Info +// line is the source of truth for log parsing. +func printStartedMarker(mode string) { + fmt.Print("\n\n" + + "========================================================================\n" + + "\n" + + "\n" + + " AI Workspace Started mode=" + mode + "\n" + + "\n" + + "\n" + + "========================================================================\n" + + "\n\n") } func main() { @@ -97,13 +89,18 @@ func main() { httpServer.TLSConfig = tlsConfig go func() { - printBanner() + mode := "PRODUCTION" + if cfg.DemoMode { + mode = "DEMO" + } slog.Info("AI Workspace BFF started", "addr", cfg.Addr, + "mode", mode, "auth_mode", cfg.AuthMode, "platform_api", cfg.PlatformAPIURL, "oidc_enabled", cfg.OIDC.Enabled, ) + printStartedMarker(mode) var serveErr error if tlsConfig != nil { serveErr = httpServer.ListenAndServeTLS("", "") From 988a0456a9e6a91544e053da5213de1d8d9645a5 Mon Sep 17 00:00:00 2001 From: Thushani Jayasekera Date: Tue, 7 Jul 2026 00:06:48 +0530 Subject: [PATCH 4/4] Enhance logging configuration for AI Workspace BFF - Introduced a new logger package to standardize logging across the application. - Updated configuration to include log level and format options. - Modified main application to utilize the new logger and load logging settings from the configuration. - Adjusted TOML mapping to support logging configuration from environment variables. --- platform-api/config/config.toml | 2 +- .../bff/internal/config/config.go | 6 + .../bff/internal/config/toml_mapping.go | 4 + .../bff/internal/logger/logger.go | 103 ++++++++++++++++++ portals/ai-workspace/bff/main.go | 5 +- .../ai-workspace/configs/config-template.toml | 4 + 6 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 portals/ai-workspace/bff/internal/logger/logger.go diff --git a/platform-api/config/config.toml b/platform-api/config/config.toml index 3d164740ab..2436dc2717 100644 --- a/platform-api/config/config.toml +++ b/platform-api/config/config.toml @@ -29,7 +29,7 @@ port = "9243" # --------------------------------------------------------------------------- [database] driver = "sqlite3" # "sqlite3" or "postgres" -path = "/app/data/api_platform.db" # SQLite file path (ignored for postgres) +# path = "/app/data/api_platform.db" # SQLite file path (ignored for postgres) # PostgreSQL — uncomment when driver = "postgres" # host = "localhost" diff --git a/portals/ai-workspace/bff/internal/config/config.go b/portals/ai-workspace/bff/internal/config/config.go index ef8209ab5c..de292287fd 100644 --- a/portals/ai-workspace/bff/internal/config/config.go +++ b/portals/ai-workspace/bff/internal/config/config.go @@ -34,6 +34,10 @@ type Config struct { Addr string // host:port to listen on, e.g. ":5380" StaticDir string // directory containing the built SPA (index.html + assets) + // Logging + LogLevel string // "debug" | "info" | "warn" | "error" (default "info") + LogFormat string // "text" | "json" (default "text") + // TLS for the BFF listener TLS TLSConfig @@ -201,6 +205,8 @@ func Load() (*Config, error) { cfg := &Config{ Addr: getenv("BFF_ADDR", ":5380"), StaticDir: getenv("STATIC_DIR", "/app"), + LogLevel: strings.ToLower(getenv("LOG_LEVEL", "info")), + LogFormat: strings.ToLower(getenv("LOG_FORMAT", "text")), TLS: TLSConfig{ SelfSigned: selfSigned, // Convention matches the legacy entrypoint.sh mount path. buildTLS diff --git a/portals/ai-workspace/bff/internal/config/toml_mapping.go b/portals/ai-workspace/bff/internal/config/toml_mapping.go index 33b0969e0a..023ea1a234 100644 --- a/portals/ai-workspace/bff/internal/config/toml_mapping.go +++ b/portals/ai-workspace/bff/internal/config/toml_mapping.go @@ -57,6 +57,10 @@ var tomlKeyToEnv = map[string]string{ "tls_cert_file": "BFF_TLS_CERT_FILE", "tls_key_file": "BFF_TLS_KEY_FILE", + // --- Logging --- + "log_level": "LOG_LEVEL", + "log_format": "LOG_FORMAT", + // --- Upstream Platform API --- "platform_api_url": "PLATFORM_API_URL", "platform_api_tls_skip_verify": "PLATFORM_API_TLS_SKIP_VERIFY", diff --git a/portals/ai-workspace/bff/internal/logger/logger.go b/portals/ai-workspace/bff/internal/logger/logger.go new file mode 100644 index 0000000000..ae04022573 --- /dev/null +++ b/portals/ai-workspace/bff/internal/logger/logger.go @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2026, WSO2 LLC. (https://www.wso2.com). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the + * License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +// Package logger builds the BFF's process-wide slog logger. It mirrors +// platform-api's internal/logger package so both services produce logs of +// the same shape; keep the two in sync if one changes. +package logger + +import ( + "fmt" + "log/slog" + "os" + "strings" +) + +// Config holds logger configuration. +type Config struct { + Level string // "debug", "info", "warn", "error" + Format string // "text" (default) or "json" +} + +// NewLogger creates a new slog logger with configurable log level and format. +func NewLogger(cfg Config) *slog.Logger { + level := ParseLevel(cfg.Level) + + opts := &slog.HandlerOptions{ + Level: level, + AddSource: true, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.SourceKey { + if src, ok := a.Value.Any().(*slog.Source); ok { + return slog.String("source", fmt.Sprintf("%s:%d", shortenSource(src.File), src.Line)) + } + } + return a + }, + } + + var handler slog.Handler + if strings.ToLower(cfg.Format) == "json" { + handler = slog.NewJSONHandler(os.Stdout, opts) + } else { + // Default to text. + handler = slog.NewTextHandler(os.Stdout, opts) + } + + return slog.New(handler) +} + +// shortenSource turns an absolute compile-time file path into a compact, +// module-relative one so logs read e.g. "server/server.go" instead of the full +// filesystem path. It handles both a local build (".../ai-workspace-bff/internal/...") +// and the BFF imported as a dependency (".../ai-workspace-bff@v1.2.3/internal/..."). +// The "internal/" prefix is dropped; other roots are kept. Paths outside the +// ai-workspace-bff module (e.g. other modules) are returned as-is. +func shortenSource(file string) string { + const mod = "ai-workspace-bff" + idx := strings.LastIndex(file, mod) + if idx == -1 { + return file + } + rest := file[idx+len(mod):] + // Module-cache paths carry an "@version" segment right after the module dir. + if strings.HasPrefix(rest, "@") { + if slash := strings.IndexByte(rest, '/'); slash != -1 { + rest = rest[slash:] + } + } + rest = strings.TrimPrefix(rest, "/") + if rest == "" { + return file + } + return strings.TrimPrefix(rest, "internal/") +} + +// ParseLevel converts a log level string to slog.Level. +func ParseLevel(level string) slog.Level { + switch strings.ToLower(level) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn", "warning": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} diff --git a/portals/ai-workspace/bff/main.go b/portals/ai-workspace/bff/main.go index 537f9585cb..d69ef9f1a6 100644 --- a/portals/ai-workspace/bff/main.go +++ b/portals/ai-workspace/bff/main.go @@ -33,6 +33,7 @@ import ( "time" "ai-workspace-bff/internal/config" + "ai-workspace-bff/internal/logger" "ai-workspace-bff/internal/server" "ai-workspace-bff/internal/tlsutil" ) @@ -54,13 +55,13 @@ func printStartedMarker(mode string) { } func main() { - slog.SetDefault(slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))) - cfg, err := config.Load() if err != nil { + slog.SetDefault(logger.NewLogger(logger.Config{Level: "info", Format: "text"})) slog.Error("configuration error", "err", err) os.Exit(1) } + slog.SetDefault(logger.NewLogger(logger.Config{Level: cfg.LogLevel, Format: cfg.LogFormat})) ctx, cancel := context.WithCancel(context.Background()) defer cancel() diff --git a/portals/ai-workspace/configs/config-template.toml b/portals/ai-workspace/configs/config-template.toml index c709ab2820..412b164501 100644 --- a/portals/ai-workspace/configs/config-template.toml +++ b/portals/ai-workspace/configs/config-template.toml @@ -98,6 +98,10 @@ platform_gateway_versions = '[{"version":"1.2","latestVersion":"v1.2.0-M1","chan # tls_cert_file = "/etc/ai-workspace/tls/tls.crt" # tls_key_file = "/etc/ai-workspace/tls/tls.key" +# --- Logging --- +# log_level = "info" # debug | info | warn | error +# log_format = "text" # text | json + # --- Upstream Platform API --- # Base URL the BFF uses to reach the Platform API (server-to-server, NOT the # browser path above). REQUIRED — set here or via the PLATFORM_API_URL env var.