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
7 changes: 7 additions & 0 deletions internal/adapter/discovery/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,13 @@ func (s *ModelDiscoveryService) discoverConcurrently(ctx context.Context, endpoi

// handleDiscoveryError processes discovery errors and manages endpoint disabling
func (s *ModelDiscoveryService) handleDiscoveryError(endpoint *domain.Endpoint, err error) {
// Discovery requests are canceled during coordinated shutdown/restart.
// Treat these as expected lifecycle events, not endpoint failures.
if errors.Is(err, context.Canceled) {
s.logger.InfoWithEndpoint("Model discovery canceled during shutdown", endpoint.Name)
return
}

// Create user-friendly message for console output and detailed error for logs
userMsg := GetUserFriendlyMessage(err)

Expand Down
34 changes: 34 additions & 0 deletions internal/adapter/discovery/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,40 @@ func TestEndpointDisabledAfterMaxFailures(t *testing.T) {
}
}

func TestDiscoverEndpointContextCanceledDoesNotCountAsFailure(t *testing.T) {
endpoint := createMockEndpoint("http://localhost:11434", "test-endpoint")

client := &mockDiscoveryClient{
discoveryErrors: map[string]error{
endpoint.URLString: &DiscoveryError{
EndpointURL: endpoint.URLString,
ProfileType: domain.ProfileOMLX,
Operation: "http_request",
Err: &NetworkError{
URL: endpoint.URLString + "/v1/models",
Err: context.Canceled,
},
},
},
}
endpointRepo := &mockEndpointRepository{}
modelRegistry := &mockModelRegistry{registeredModels: make([]*domain.ModelInfo, 0)}
service := NewModelDiscoveryService(client, endpointRepo, modelRegistry, DiscoveryConfig{Timeout: 5 * time.Second}, createTestLogger())

err := service.DiscoverEndpoint(context.Background(), endpoint)
if err == nil {
t.Fatalf("expected context cancellation error, got nil")
}

if got := service.getFailureCount(endpoint.URLString); got != 0 {
t.Fatalf("expected no failure count increment for context cancellation, got %d", got)
}

if service.isEndpointDisabled(endpoint.URLString) {
t.Fatalf("endpoint should not be disabled for context cancellation")
}
}

func TestFilterActiveEndpoints(t *testing.T) {
endpoints := []*domain.Endpoint{
createMockEndpoint("http://localhost:11434", "enabled-1"),
Expand Down
11 changes: 9 additions & 2 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
type Config struct {
Level string
LogDir string
OutputPath string
Theme string
MaxSize int // megabytes
MaxBackups int
Expand Down Expand Up @@ -129,12 +130,18 @@ func createJSONHandler(level slog.Level, writer *os.File) slog.Handler {
// createFileHandler creates a file-based JSON handler with rotation
// wraps with ansiStripHandler to ensure clean JSON output without ANSI codes
func createFileHandler(cfg *Config, level slog.Level) (slog.Handler, func(), error) {
if err := os.MkdirAll(cfg.LogDir, 0755); err != nil {
logFilePath := filepath.Join(cfg.LogDir, DefaultLogOutputName)
if cfg.OutputPath != "" {
logFilePath = cfg.OutputPath
}

logDir := filepath.Dir(logFilePath)
if err := os.MkdirAll(logDir, 0755); err != nil {
return nil, nil, err
}

rotator := &lumberjack.Logger{
Filename: filepath.Join(cfg.LogDir, DefaultLogOutputName),
Filename: logFilePath,
MaxSize: cfg.MaxSize,
MaxBackups: cfg.MaxBackups,
MaxAge: cfg.MaxAge,
Expand Down
50 changes: 41 additions & 9 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"log/slog"
"os"
"os/signal"
"path/filepath"
"runtime"
"strings"
"syscall"
"time"

Expand Down Expand Up @@ -77,8 +79,15 @@ func main() {
vlog.Printf(theme.ColourProfiler("Profiling server started at http://%s/debug/pprof/\n"), profileAddress)
}

// Load configuration before logger setup so logging.output is respected.
cfg, err := config.Load(configFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to load configuration: %v\n", err)
os.Exit(1)
}

// Setup logging
lcfg := buildLoggerConfig()
lcfg := buildLoggerConfig(cfg)
logInstance, styledLogger, cleanup, err := logger.NewWithTheme(lcfg)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to initialise logger: %v\n", err)
Expand Down Expand Up @@ -106,12 +115,6 @@ func main() {
cancel()
}()

// Load configuration
cfg, err := config.Load(configFile)
if err != nil {
logger.FatalWithLogger(logInstance, "Failed to load configuration", "error", err)
}

// Validate model alias configuration at startup
if err = cfg.ValidateModelAliases(); err != nil {
logger.FatalWithLogger(logInstance, "Invalid model alias configuration", "error", err)
Expand Down Expand Up @@ -202,8 +205,8 @@ func reportProcessStats(logger logger.StyledLogger, startTime time.Time) {
)
}

func buildLoggerConfig() *logger.Config {
return &logger.Config{
func buildLoggerConfig(cfg *config.Config) *logger.Config {
lcfg := &logger.Config{
Level: env.GetEnvOrDefault("OLLA_LOG_LEVEL", DefaultLoggerLevel),
PrettyLogs: env.GetEnvBoolOrDefault("OLLA_PRETTY_LOGS", DefaultPrettyLogs),
FileOutput: env.GetEnvBoolOrDefault("OLLA_FILE_OUTPUT", DefaultFileOutput),
Expand All @@ -213,4 +216,33 @@ func buildLoggerConfig() *logger.Config {
MaxAge: env.GetEnvIntOrDefault("OLLA_LOG_MAX_AGE_DAYS", DefaultLogMaxAgeDays),
Theme: env.GetEnvOrDefault("OLLA_THEME", DefaultTheme),
}

if cfg == nil {
return lcfg
}

if cfg.Logging.Level != "" {
lcfg.Level = cfg.Logging.Level
}

output := strings.TrimSpace(cfg.Logging.Output)
switch strings.ToLower(output) {
case "", "stdout":
lcfg.FileOutput = false
lcfg.OutputPath = ""
case "stderr":
lcfg.FileOutput = false
lcfg.OutputPath = ""
default:
lcfg.FileOutput = true
if filepath.IsAbs(output) {
lcfg.OutputPath = output
lcfg.LogDir = filepath.Dir(output)
} else {
lcfg.OutputPath = filepath.Clean(output)
lcfg.LogDir = filepath.Dir(lcfg.OutputPath)
}
}

return lcfg
}
Loading