Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions platform-api/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ import (
kenv "github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"

"github.com/wso2/api-platform/platform-api/internal/logger"
)

// FileBasedUser represents a built-in user for file-based auth mode.
Expand Down Expand Up @@ -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
}
Expand Down
2 changes: 1 addition & 1 deletion platform-api/config/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ port = "9243"
# Database
# ---------------------------------------------------------------------------
[database]
# driver = "sqlite3" # "sqlite3" or "postgres"
driver = "sqlite3" # "sqlite3" or "postgres"
# path = "/app/data/api_platform.db" # SQLite file path (ignored for postgres)

# PostgreSQL — uncomment when driver = "postgres"
Expand Down
28 changes: 19 additions & 9 deletions platform-api/internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -804,17 +804,13 @@ 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)

printStartedMarker(mode)
Comment thread
Thushani-Jayasekera marked this conversation as resolved.

quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
Expand Down Expand Up @@ -851,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
Expand Down
6 changes: 6 additions & 0 deletions portals/ai-workspace/bff/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: new configs for ai workspace


// TLS for the BFF listener
TLS TLSConfig

Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions portals/ai-workspace/bff/internal/config/toml_mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
103 changes: 103 additions & 0 deletions portals/ai-workspace/bff/internal/logger/logger.go
Original file line number Diff line number Diff line change
@@ -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
}
}
48 changes: 23 additions & 25 deletions portals/ai-workspace/bff/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,42 +33,35 @@ import (
"time"

"ai-workspace-bff/internal/config"
"ai-workspace-bff/internal/logger"
"ai-workspace-bff/internal/server"
"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" +
Comment thread
Thushani-Jayasekera marked this conversation as resolved.
"========================================================================\n" +
"\n\n")
}

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()
Expand Down Expand Up @@ -97,13 +90,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("", "")
Expand Down
4 changes: 4 additions & 0 deletions portals/ai-workspace/configs/config-template.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading