Skip to content
Open
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ hyperfleet-api/
│ ├── db/ # Database session factory, migrations, transaction middleware
│ ├── errors/ # RFC 9457 Problem Details error model
│ ├── handlers/ # HTTP handler pattern, validation and error handling
│ ├── logger/ # Structured logging (slog-based)
│ ├── logger/ # API logging integration (request ID, HTTP attrs, GORM adapter)
│ ├── presenters/ # Response presenters (DAO models → API responses)
│ └── services/ # Business logic layer (status aggregation, validation)
├── openapi/ # API specification source
Expand Down
3 changes: 2 additions & 1 deletion cmd/hyperfleet-api/container/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package container
import (
"context"
"fmt"
"log/slog"

"github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/validators"
Expand All @@ -16,7 +17,7 @@ func (c *Container) SchemaValidator() *validators.SchemaValidator {
panic(fmt.Sprintf("create schema validator: %v", err))
}
c.schemaValidator = schemaValidator
logger.With(context.Background(), logger.FieldSchemaPath, schemaPath).Info("Schema validation enabled")
slog.InfoContext(context.Background(), "Schema validation enabled", logger.FieldSchemaPath, schemaPath)
}
return c.schemaValidator
}
33 changes: 10 additions & 23 deletions cmd/hyperfleet-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"log/slog"
"os"

hfl "github.com/openshift-hyperfleet/hyperfleet-logger"
"github.com/spf13/cobra"

"github.com/openshift-hyperfleet/hyperfleet-api/cmd/hyperfleet-api/migrate"
Expand Down Expand Up @@ -36,56 +37,42 @@ func main() {
rootCmd.AddCommand(migrateCmd, serveCmd, versionCmd)

if err := rootCmd.Execute(); err != nil {
logger.WithError(ctx, err).Error("Error running command")
slog.ErrorContext(ctx, "Error running command", "error", err)
os.Exit(1)
}
}

// initDefaultLogger initializes a default logger with INFO level
// This ensures logging works before environment/config is loaded
// Reads HYPERFLEET_LOGGING_* from environment variables if set
// Reads HYPERFLEET_LOGGING_* variables if set.
func initDefaultLogger() {
// Read log level from environment with default fallback
level := slog.LevelInfo
if levelStr := os.Getenv("HYPERFLEET_LOGGING_LEVEL"); levelStr != "" {
if parsed, err := logger.ParseLogLevel(levelStr); err == nil {
if parsed, err := hfl.ParseLevel(levelStr); err == nil {
level = parsed
}
}

// Read log format from environment with default fallback
format := logger.FormatJSON
format := hfl.FormatJSON
if formatStr := os.Getenv("HYPERFLEET_LOGGING_FORMAT"); formatStr != "" {
if parsed, err := logger.ParseLogFormat(formatStr); err == nil {
if parsed, err := hfl.ParseFormat(formatStr); err == nil {
format = parsed
}
}

// Read log output from environment with default fallback
var output io.Writer = os.Stdout
if outputStr := os.Getenv("HYPERFLEET_LOGGING_OUTPUT"); outputStr != "" {
if parsed, err := logger.ParseLogOutput(outputStr); err == nil {
if parsed, err := hfl.ParseOutput(outputStr); err == nil {
output = parsed
}
}

cfg := &logger.LogConfig{
Level: level,
Format: format,
Output: output,
Component: "hyperfleet-api",
Version: api.Version,
Hostname: getHostname(),
}
logger.InitGlobalLogger(cfg)
}

func getHostname() string {
hostname, err := os.Hostname()
if err != nil {
return "unknown"
}
return hostname
slog.SetDefault(logger.NewLogger(api.Version, logger.HandlerConfig{
Level: level, Format: format, Output: output,
}))
}

func newVersionCommand() *cobra.Command {
Expand Down
28 changes: 25 additions & 3 deletions cmd/hyperfleet-api/migrate/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package migrate

import (
"context"
"log/slog"
"os"

hfl "github.com/openshift-hyperfleet/hyperfleet-logger"
"github.com/spf13/cobra"

"github.com/openshift-hyperfleet/hyperfleet-api/pkg/api"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/config"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/db"
"github.com/openshift-hyperfleet/hyperfleet-api/pkg/db/db_session"
Expand Down Expand Up @@ -40,27 +43,46 @@ func runMigrate(cmd *cobra.Command, _ []string) {
loader := config.NewConfigLoader()
appConfig, err := loader.Load(ctx, cmd)
if err != nil {
logger.WithError(ctx, err).Error("Failed to load configuration")
slog.ErrorContext(ctx, "Failed to load configuration", "error", err)
os.Exit(1)
}
initLogger(appConfig.Logging)

// Run migration with the loaded configuration
if err := runMigrateWithError(ctx, appConfig.Database); err != nil {
os.Exit(1)
}
}

func initLogger(loggingCfg *config.LoggingConfig) {
level, err := hfl.ParseLevel(loggingCfg.Level)
if err != nil {
level = slog.LevelInfo
}
format, err := hfl.ParseFormat(loggingCfg.Format)
if err != nil {
format = hfl.FormatJSON
}
output, err := hfl.ParseOutput(loggingCfg.Output)
if err != nil {
output = os.Stdout
}
slog.SetDefault(logger.NewLogger(api.Version, logger.HandlerConfig{
Level: level, Format: format, Output: output,
}))
}

func runMigrateWithError(ctx context.Context, dbConfig *config.DatabaseConfig) error {
connection := db_session.NewProdFactory(dbConfig)
defer func() {
if closeErr := connection.Close(); closeErr != nil {
logger.WithError(ctx, closeErr).Error("Failed to close database connection")
slog.ErrorContext(ctx, "Failed to close database connection", "error", closeErr)
}
}()

// Use MigrateWithLock to prevent concurrent migrations from multiple pods
if err := db.MigrateWithLock(ctx, connection); err != nil {
logger.WithError(ctx, err).Error("Migration failed")
slog.ErrorContext(ctx, "Migration failed", "error", err)
return err
}

Expand Down
71 changes: 29 additions & 42 deletions cmd/hyperfleet-api/servecmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"syscall"
"time"

hfl "github.com/openshift-hyperfleet/hyperfleet-logger"
"github.com/spf13/cobra"

"github.com/openshift-hyperfleet/hyperfleet-api/cmd/hyperfleet-api/container"
Expand Down Expand Up @@ -57,8 +58,9 @@ func runServe(cmd *cobra.Command, args []string) (runErr error) {
// container.Container's accessors panic by design; log the stack here since main.go can't see it.
defer func() {
if r := recover(); r != nil {
logger.With(context.Background(), "panic_stack", string(debug.Stack())).
Error(fmt.Sprintf("recovered from panic in runServe: %v", r))
slog.ErrorContext(context.Background(),
fmt.Sprintf("recovered from panic in runServe: %v", r), "panic_stack", string(debug.Stack()),
)
Comment thread
kuudori marked this conversation as resolved.
runErr = fmt.Errorf("%v", r)
}
}()
Expand All @@ -82,49 +84,48 @@ func runServe(cmd *cobra.Command, args []string) (runErr error) {
closeErr := c.Close()
runErr = errors.Join(runErr, closeErr)
if runErr == nil {
logger.Info(context.Background(), "Graceful shutdown completed")
slog.InfoContext(context.Background(), "Graceful shutdown completed")
}
}()

ctr := container.NewContainer(cfg, c)

initLogger(cfg)

ctr := container.NewContainer(cfg, c)

sf := ctr.SessionFactory()
configureDBLogger(cfg, sf)

logger.Info(ctx, "Starting HyperFleet API with configuration (sensitive values redacted):")
logger.Info(ctx, config.DumpConfig(cfg))
slog.InfoContext(ctx, "Starting HyperFleet API with configuration (sensitive values redacted):")
slog.InfoContext(ctx, config.DumpConfig(cfg))

// OTel registered first so it flushes last - teardown spans are preserved.
if cfg.Tracing.Enabled {
traceProvider, traceErr := telemetry.InitTraceProvider(ctx, cfg.Tracing.ServiceName, api.Version)
if traceErr != nil {
logger.WithError(ctx, traceErr).Warn("Failed to initialize OpenTelemetry")
slog.WarnContext(ctx, "Failed to initialize OpenTelemetry", "error", traceErr)
} else {
logger.With(ctx, logger.FieldServiceName, cfg.Tracing.ServiceName).Info("OpenTelemetry initialized")
slog.InfoContext(ctx, "OpenTelemetry initialized", logger.FieldServiceName, cfg.Tracing.ServiceName)
c.Add(func() error {
flushCtx, cancel := context.WithTimeout(context.Background(), otelFlushTimeout)
defer cancel()
return telemetry.Shutdown(flushCtx, traceProvider)
})
}
} else {
logger.With(ctx, logger.FieldOTelEnabled, false).Info("OpenTelemetry disabled")
slog.InfoContext(ctx, "OpenTelemetry disabled", logger.FieldOTelEnabled, false)
}

logger.With(ctx,
"log_level", cfg.Logging.Level,
slog.InfoContext(ctx,
"Logger initialized", "log_level", cfg.Logging.Level,
"log_format", cfg.Logging.Format,
"log_output", cfg.Logging.Output,
"masking_enabled", cfg.Logging.Masking.Enabled,
).Info("Logger initialized")
)

if collectorErr := metrics.RegisterReconciliationCollector(
ctr.SessionFactory().DirectDB(),
cfg.Metrics.ReconciliationStuckThreshold,
); collectorErr != nil {
logger.WithError(ctx, collectorErr).Error("Failed to register reconciliation collector")
slog.ErrorContext(ctx, "Failed to register reconciliation collector", "error", collectorErr)
}

apiServer, err := BuildAPIServer(
Expand All @@ -151,7 +152,7 @@ func runServe(cmd *cobra.Command, args []string) (runErr error) {
// Readyz registered last so it runs first - immediately fails the probe.
c.Add(func() error {
health.GetReadinessState().SetShuttingDown()
logger.Info(context.Background(), "Marked as not ready, draining in-flight requests...")
slog.InfoContext(context.Background(), "Marked as not ready, draining in-flight requests...")
return nil
})

Expand Down Expand Up @@ -191,15 +192,15 @@ func runServe(cmd *cobra.Command, args []string) (runErr error) {
}
if triggerErr == nil && !shutdown {
health.GetReadinessState().SetReady()
logger.Info(ctx, "Application ready to receive traffic")
slog.InfoContext(ctx, "Application ready to receive traffic")
select {
case <-ctx.Done():
case <-signals:
case triggerErr = <-serverResults:
}
}

logger.Info(context.Background(), "Shutdown requested, starting graceful shutdown...")
slog.InfoContext(context.Background(), "Shutdown requested, starting graceful shutdown...")
runErr = triggerErr
return runErr
}
Expand All @@ -219,41 +220,27 @@ func initLogger(cfg *config.ApplicationConfig) {
ctx := context.Background()
loggingCfg := cfg.Logging

level, err := logger.ParseLogLevel(loggingCfg.Level)
level, err := hfl.ParseLevel(loggingCfg.Level)
if err != nil {
logger.With(ctx, logger.FieldLogLevel, loggingCfg.Level).WithError(err).Warn("Invalid log level, using default")
slog.WarnContext(ctx, "Invalid log level, using default", logger.FieldLogLevel, loggingCfg.Level, "error", err)
level = slog.LevelInfo
}

format, err := logger.ParseLogFormat(loggingCfg.Format)
format, err := hfl.ParseFormat(loggingCfg.Format)
if err != nil {
logger.With(ctx, logger.FieldLogFormat, loggingCfg.Format).WithError(err).Warn("Invalid log format, using default")
format = logger.FormatJSON
slog.WarnContext(ctx, "Invalid log format, using default", logger.FieldLogFormat, loggingCfg.Format, "error", err)
format = hfl.FormatJSON
}

output, err := logger.ParseLogOutput(loggingCfg.Output)
output, err := hfl.ParseOutput(loggingCfg.Output)
if err != nil {
logger.With(ctx, logger.FieldLogOutput, loggingCfg.Output).WithError(err).Warn("Invalid log output, using default")
slog.WarnContext(ctx, "Invalid log output, using default", logger.FieldLogOutput, loggingCfg.Output, "error", err)
output = os.Stdout
}

hostname := cfg.Server.Hostname
if hostname == "" {
hostname, _ = os.Hostname() //nolint:errcheck // empty string is acceptable fallback
}

logConfig := &logger.LogConfig{
Level: level,
Format: format,
Output: output,
Component: "api",
Version: api.Version,
Hostname: hostname,
}

// Use ReconfigureGlobalLogger instead of InitGlobalLogger because
// InitGlobalLogger was already called in main() with default config
logger.ReconfigureGlobalLogger(logConfig)
slog.SetDefault(logger.NewLogger(api.Version, logger.HandlerConfig{
Level: level, Format: format, Output: output,
}))
}

func configureDBLogger(cfg *config.ApplicationConfig, sessionFactory db.SessionFactory) {
Expand Down
5 changes: 2 additions & 3 deletions cmd/hyperfleet-api/server/compress.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package server

import (
"compress/gzip"
"log/slog"
"net/http"
"strconv"
"strings"

"github.com/openshift-hyperfleet/hyperfleet-api/pkg/logger"
)

// gzipResponseWriter wraps an http.ResponseWriter, transparently gzip-encoding
Expand Down Expand Up @@ -73,7 +72,7 @@ func CompressMiddleware(next http.Handler) http.Handler {
gz := gzip.NewWriter(w)
defer func() {
if err := gz.Close(); err != nil {
logger.WithError(r.Context(), err).Warn("failed to finalize gzip response, response may be incomplete")
slog.WarnContext(r.Context(), "failed to finalize gzip response, response may be incomplete", "error", err)
}
}()

Expand Down
14 changes: 6 additions & 8 deletions cmd/hyperfleet-api/server/logging/request_logging_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,13 @@ func RequestLoggingMiddleware(masker *middleware.MaskingMiddleware) func(http.Ha
} else {
maskedHeaders = r.Header
}

logger.With(ctx,
logger.HTTPMethod(r.Method),
slog.InfoContext(ctx,
"HTTP request received", logger.HTTPMethod(r.Method),
logger.HTTPPath(r.URL.Path),
slog.String("remote_addr", r.RemoteAddr),
logger.HTTPUserAgent(r.UserAgent()),
slog.Any("headers", maskedHeaders),
).Info("HTTP request received")
)

rw := &responseWriter{ResponseWriter: w}

Expand All @@ -48,15 +47,14 @@ func RequestLoggingMiddleware(masker *middleware.MaskingMiddleware) func(http.Ha
if rw.statusCode == 0 {
rw.statusCode = http.StatusOK
}

logger.With(ctx,
logger.HTTPMethod(r.Method),
slog.InfoContext(ctx,
"HTTP request completed", logger.HTTPMethod(r.Method),
logger.HTTPPath(r.URL.Path),
logger.HTTPStatusCode(rw.statusCode),
logger.HTTPDuration(duration),
slog.String("remote_addr", r.RemoteAddr),
logger.HTTPUserAgent(r.UserAgent()),
).Info("HTTP request completed")
)
})
}
}
Expand Down
Loading