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
153 changes: 153 additions & 0 deletions internal/boot/boot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package boot

import (
"io"
"log/slog"
"maps"
"reflect"
"slices"
"sync"

"github.com/BeamFoundry/rays/internal/boot/config"
"github.com/BeamFoundry/rays/internal/boot/logger"
"github.com/BeamFoundry/rays/internal/services"
. "github.com/BeamFoundry/rays/pkg/core"
. "github.com/BeamFoundry/rays/pkg/lang"
)

type BootLoader struct {
*config.Config
loggingProperties *LoggingProperties
handler func(io.Writer, *slog.HandlerOptions) slog.Handler
logger *logger.Logger
loggers []string
lock sync.Mutex
}

func NewBootLoader() *BootLoader {
cfg := config.NewConfig()
loggingDefaults := map[string]any{
"handler": LogHandler{}.New().DEFAULT.String(),
"level": LogLevel{}.New().INFO.String(),
"source": true,
"format": logger.DefaultFormat,
"color": LogColor{}.New().AUTO.String(),
}
//nolint:errcheck // default config load at path cannot fail
cfg.LoadDefaultConfigAt(loggingDefaults, "rays.logging")

quietLogger := logger.NewLogger()
quietLogger.SetLogLevel(slog.LevelError)
cfg.SetLogger(quietLogger)
services.Register(&services.Registry{
Logger: quietLogger,
})

return &BootLoader{
Config: cfg,
logger: quietLogger,
loggers: []string{},
}
}

func (this *BootLoader) SetLoggingHandler(handler func(io.Writer, *slog.HandlerOptions) slog.Handler) {
this.lock.Lock()
defer this.lock.Unlock()
this.handler = handler
}

func (this *BootLoader) LoggingProperties() *LoggingProperties {
return this.loggingProperties
}

func (this *BootLoader) Boot() Error {
this.lock.Lock()
defer this.lock.Unlock()

// 1. Initializing configuration in Phase 1 (quiet)
if err := this.InitConfig(); err != nil {
return err
}

// 2. Initializing logging properties from parsed config
if cfg, err := this.GetSubConfig("rays.logging"); err == nil {
this.loggingProperties = &LoggingProperties{}
if err := cfg.Unmarshal(this.loggingProperties); err != nil {
return err
}
level := this.loggingProperties.LogLevel()
this.logger.SetLogLevel(level.Level())
if this.loggingProperties.AddSource() {
this.logger.AddSource()
}
if this.handler != nil {
this.logger.SetHandler(this.handler)
} else if handler := this.loggingProperties.LogHandler(); handler.Handler(this.loggingProperties.Format()) != nil {
this.logger.SetHandler(handler.Handler(this.loggingProperties.Format()))
}
this.loggingProperties.LogColor().Apply(this.logger)
this.loggers = slices.Sorted(maps.Keys(this.loggingProperties.MapLevels()))
slices.Reverse(this.loggers)
}
this.logger.SetDefault()

// 3. Register configured logger into services
services.Register(&services.Registry{
Logger: this.logger,
})

// 4. Primary functionality hooks (future extensions)

// 5. Reinitialize Config with the correct parameters (Phase 4)
this.Clear()
this.SetLogger(this.logger)
//nolint:errcheck // Phase 1 has already validated configuration loading
_ = this.InitConfig()

this.logger.Debug("Config & Logging initialized")
return nil
}

func (this *BootLoader) Logger() Logging {
return &logging{this.logger}
}

func (this *BootLoader) LoggerForClass(class any) Logging {
rtype := reflect.TypeOf(class)
return this.LoggerForType(rtype)
}

func (this *BootLoader) LoggerForType(rtype reflect.Type) Logging {
if this.loggingProperties == nil || rtype.Kind() != reflect.Pointer || rtype.Elem().Kind() != reflect.Struct {
return this.Logger()
}

pkg := rtype.Elem().PkgPath()
name := pkg + "/" + rtype.Elem().Name()
for _, level := range this.loggers {
if (!String(level).EndsWith("/") || !String(pkg).StartsWith(level)) && pkg != level && name != level {
continue
}
this.logger.Trace("Creating specific new logger", "pkg", pkg, "name", name)
lg := this.logger.Clone()
logLevel := this.loggingProperties.MapLevels()[level]
lg.SetLogLevel(logLevel.Level())
return &logging{lg}
}
return this.Logger()
}

func (this *BootLoader) Reset() {
this.lock.Lock()
defer this.lock.Unlock()
this.Config.Reset()
this.handler = nil
this.loggingProperties = nil
this.loggers = []string{}
this.logger.Reset()
this.logger.SetLogLevel(slog.LevelError)
this.SetLogger(this.logger)
services.Register(&services.Registry{
Logger: this.logger,
})
}
13 changes: 13 additions & 0 deletions internal/boot/boot_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package boot_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestBoot(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Internal/Boot Suite")
}
176 changes: 176 additions & 0 deletions internal/boot/boot_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package boot_test

import (
"bytes"
"io"
"log/slog"
"reflect"

"github.com/BeamFoundry/rays/internal/boot"
"github.com/BeamFoundry/rays/internal/boot/logger"
"github.com/BeamFoundry/rays/internal/services"
. "github.com/BeamFoundry/rays/pkg/core"
"github.com/BeamFoundry/rays/pkg/lang"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

type DummyStruct struct{}

var _ = BeforeSuite(func() {
services.Get().Logger.SetOutput(GinkgoWriter)
})

var _ = Describe("BootLoader", func() {
var bl *boot.BootLoader

BeforeEach(func() {
bl = boot.NewBootLoader()
services.Get().Logger.SetOutput(GinkgoWriter)
})

AfterEach(func() {
bl.Reset()
})

It("creates a BootLoader with default quiet logger and default config", func() {
Expect(bl).NotTo(BeNil())
Expect(bl.Logger()).NotTo(BeNil())
Expect(bl.Config).NotTo(BeNil())
})

It("can boot successfully with default settings", func() {
Expect(bl.Boot()).NotTo(HaveOccurred())
Expect(bl.LoggingProperties()).NotTo(BeNil())
Expect(bl.LoggingProperties().LogLevel().Enum).To(Equal(boot.LogLevel{}.New().INFO))
Expect(bl.LoggingProperties().LogHandler().Enum).To(Equal(boot.LogHandler{}.New().DEFAULT))
Expect(bl.LoggingProperties().LogColor().Enum).To(Equal(boot.LogColor{}.New().AUTO))
Expect(bl.LoggingProperties().Color().Enum).To(Equal(boot.LogColor{}.New().AUTO))
Expect(bl.LoggingProperties().AddSource()).To(BeTrue())
Expect(bl.LoggingProperties().Format()).To(Equal(logger.DefaultFormat))
Expect(bl.LoggingProperties().MapLevels()).To(BeEmpty())
})

It("can boot with custom logging properties and package-specific loggers", func() {
cfg := map[string]any{
"level": "DEBUG",
"handler": "JSON",
"color": "ON",
"source": true,
"loggers": map[string]any{
"github.com/BeamFoundry/rays/internal/boot/": "TRACE",
"github.com/BeamFoundry/rays/internal/boot_test": "DEBUG",
"DummyStruct": "WARN",
},
}
Expect(bl.LoadConfigAt(cfg, "rays.logging")).NotTo(HaveOccurred())
Expect(bl.Boot()).NotTo(HaveOccurred())

Expect(bl.Logger()).NotTo(BeNil())
Expect(services.Get().Logger).NotTo(BeNil())

// Test LoggerForType and LoggerForClass
lg1 := bl.LoggerForClass(&DummyStruct{})
Expect(lg1).NotTo(BeNil())

lg2 := bl.LoggerForType(reflect.TypeFor[*DummyStruct]())
Expect(lg2).NotTo(BeNil())

// Test non-struct pointer and non-pointer
Expect(bl.LoggerForType(reflect.TypeFor[string]())).NotTo(BeNil())
Expect(bl.LoggerForClass("not a struct")).NotTo(BeNil())

// Test struct that does not match any configured logger
Expect(bl.LoggerForClass(&bytes.Buffer{})).NotTo(BeNil())

// Test With and WithGroup on logging wrapper
withLg := lg1.With("key", "val")
Expect(withLg).NotTo(BeNil())
withGrp := lg1.WithGroup("testgroup")
Expect(withGrp).NotTo(BeNil())
})

It("supports custom logging handler set via SetLoggingHandler", func() {
called := false
bl.SetLoggingHandler(func(w io.Writer, opts *slog.HandlerOptions) slog.Handler {
called = true
return slog.NewTextHandler(w, opts)
})
Expect(bl.Boot()).NotTo(HaveOccurred())
Expect(called).To(BeTrue())
})

It("supports CUSTOM log handler with custom format", func() {
cfg := map[string]any{
"handler": "CUSTOM",
"format": "%msg(%v)",
}
Expect(bl.LoadConfigAt(cfg, "rays.logging")).NotTo(HaveOccurred())
Expect(bl.Boot()).NotTo(HaveOccurred())
})

It("supports all LogHandler enum values", func() {
h := boot.LogHandler{}.New()
for _, enumVal := range []lang.Enum[boot.LogHandler]{
h.DEFAULT,
h.JSON,
h.TEXT,
h.CUSTOM,
h.ECS,
h.GELF,
h.LOGSTASH,
} {
handlerObj := boot.LogHandler{Enum: enumVal}
fn := handlerObj.Handler("custom-format")
Expect(fn).NotTo(BeNil())
handler := fn(io.Discard, nil)
Expect(handler).NotTo(BeNil())
}
// Default / invalid
invalidHandler := boot.LogHandler{}
Expect(invalidHandler.Handler()).To(BeNil())
})

It("supports all LogLevel enum values", func() {
l := boot.LogLevel{}.New()
Expect((&boot.LogLevel{Enum: l.TRACE}).Level()).To(Equal(LevelTrace))
Expect((&boot.LogLevel{Enum: l.DEBUG}).Level()).To(Equal(LevelDebug))
Expect((&boot.LogLevel{Enum: l.WARN}).Level()).To(Equal(LevelWarn))
Expect((&boot.LogLevel{Enum: l.ERROR}).Level()).To(Equal(LevelError))
Expect((&boot.LogLevel{Enum: l.FATAL}).Level()).To(Equal(LevelFatal))
Expect((&boot.LogLevel{Enum: l.INFO}).Level()).To(Equal(LevelInfo))
invalidLevel := boot.LogLevel{}
Expect(invalidLevel.Level()).To(Equal(LevelInfo))
})

It("supports all LogColor enum values", func() {
lg := logger.NewLogger()
lg.SetOutput(GinkgoWriter)
c := boot.LogColor{}.New()
boot.LogColor{Enum: c.ON}.Apply(lg)
boot.LogColor{Enum: c.OFF}.Apply(lg)
boot.LogColor{Enum: c.AUTO}.Apply(lg)
})

It("returns error when phase 1 config initialization fails", func() {
// Calling InitConfig beforehand so Boot()'s InitConfig fails with already initialized
Expect(bl.InitConfig()).NotTo(HaveOccurred())
err := bl.Boot()
Expect(err).To(HaveOccurred())
})

It("returns error when logging properties unmarshal fails", func() {
cfg := map[string]any{
"source": "invalid_bool",
}
Expect(bl.LoadConfigAt(cfg, "rays.logging")).NotTo(HaveOccurred())
err := bl.Boot()
Expect(err).To(HaveOccurred())
})

It("can reset properly", func() {
Expect(bl.Boot()).NotTo(HaveOccurred())
bl.Reset()
Expect(bl.LoggingProperties()).To(BeNil())
})
})
Loading
Loading