From 32b50a0e94b51f661ac4f0be693294f8936a01da Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Sat, 13 Jun 2026 09:55:43 +1000 Subject: [PATCH 1/3] feat(daemon): route daemon-kernel logging through log/slog Introduce a log/slog seam in the daemon ahead of the pkg/daemonkit extraction (#499). Bump automa-saga/logx to v0.5.0 and install its zerolog-backed slog.Handler via slog.SetDefault in the daemon bootstrap, so slog records reach the same journald + rotating-file sinks logx configures. Convert the 8 daemon-kernel logx call sites (5 in core/monitor.go, 3 in server.go) to slog, preserving the same reason keys, messages, levels, and field names. CLI/workflows stay on logx. Closes #691 Signed-off-by: Lenin Mehedy --- cmd/daemon/main.go | 9 +++++++ go.mod | 2 +- go.sum | 4 +-- internal/daemon/core/monitor.go | 48 +++++++++++++++------------------ internal/daemon/server.go | 8 +++--- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 1ee7c5a5..a91b07d3 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -5,6 +5,7 @@ package main import ( "context" "fmt" + "log/slog" "os" "os/signal" "path" @@ -201,6 +202,14 @@ func initConfig(ctx context.Context) { doctor.CheckErr(ctx, err) } + // Install the slog→logx bridge so the daemon kernel (which logs via the + // stdlib log/slog seam, in preparation for the pkg/daemonkit extraction) + // emits to the same zerolog sinks logx just configured — console, the + // rotating file, and journald. Must run after logx.Initialize so the + // bridge resolves the fully configured logger. CLI/workflows keep using + // logx directly. + slog.SetDefault(slog.New(logx.NewSlogHandler())) + activateProxy(ctx) } diff --git a/go.mod b/go.mod index 41a1b4f9..45f58407 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/BurntSushi/toml v1.6.0 github.com/Masterminds/semver/v3 v3.5.0 github.com/automa-saga/automa v0.10.0 - github.com/automa-saga/logx v0.4.0 + github.com/automa-saga/logx v0.5.0 github.com/bluet/syspkg v0.1.7 github.com/charmbracelet/bubbles v1.0.0 github.com/charmbracelet/bubbletea v1.3.10 diff --git a/go.sum b/go.sum index 9d291e7c..b8e7ebc1 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/automa-saga/automa v0.10.0 h1:K3cBRwVl7MgHFre4FYmnLZMYsphoUOnoGJ50hjDe4NY= github.com/automa-saga/automa v0.10.0/go.mod h1:AVzNGE+ci5sAJ7ao3ZkmC+CMe4kpwJhE6AloBJv3Udc= -github.com/automa-saga/logx v0.4.0 h1:5KPFX1vfnJtFvm18a9pwwDlqwgALimKjVmR3G1zjytM= -github.com/automa-saga/logx v0.4.0/go.mod h1:inlus7bMGEUD5wIEgtoKcGO5Ulhl93CHkzUBlqJJK6I= +github.com/automa-saga/logx v0.5.0 h1:yuFC7JlEQRbEKr+6t9eg1kXAvRIIgzdu9Bz7a0gSJYo= +github.com/automa-saga/logx v0.5.0/go.mod h1:8S9d499t8uro8yO3blI1hruXLEYWzrBp9ndCI/Ik/88= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= diff --git a/internal/daemon/core/monitor.go b/internal/daemon/core/monitor.go index 6cb7b1e8..89cd652c 100644 --- a/internal/daemon/core/monitor.go +++ b/internal/daemon/core/monitor.go @@ -5,10 +5,9 @@ package core import ( "context" "fmt" + "log/slog" "sync" "time" - - "github.com/automa-saga/logx" ) // Back-off and degradation parameters for SupervisedMonitor. Declared as @@ -164,42 +163,40 @@ func SupervisedMonitor(ctx context.Context, m MonitorRunner, tracker *StatusTrac // ctx cancelled → clean shutdown, do not restart. if ctx.Err() != nil { setState("stopped") - logx.As().Info(). - Str("reason", "MonitorStopped"). - Str("monitor", m.Name()). - Msg("Monitor stopped cleanly") + slog.Info("Monitor stopped cleanly", + "reason", "MonitorStopped", + "monitor", m.Name()) return } // nil return without ctx cancellation → also clean exit. if err == nil { setState("stopped") - logx.As().Info(). - Str("reason", "MonitorExited"). - Str("monitor", m.Name()). - Msg("Monitor exited without error and without context cancellation — not restarting") + slog.Info("Monitor exited without error and without context cancellation — not restarting", + "reason", "MonitorExited", + "monitor", m.Name()) return } // Crash path. consecutiveCrashes++ - logx.As().Error().Err(err). - Str("reason", "MonitorCrash"). - Str("monitor", m.Name()). - Int("consecutive_crashes", consecutiveCrashes). - Dur("backoff", backoff). - Msg("Monitor crashed — restarting after back-off") + slog.Error("Monitor crashed — restarting after back-off", + "error", err, + "reason", "MonitorCrash", + "monitor", m.Name(), + "consecutive_crashes", consecutiveCrashes, + "backoff", backoff) // Emit MonitorDegraded at every supervisedDegradedThreshold consecutive // crashes so ops is alerted at crash #5, #10, #15, … if consecutiveCrashes%supervisedDegradedThreshold == 0 { - logx.As().Error().Err(err). - Str("reason", "MonitorDegraded"). - Str("monitor", m.Name()). - Int("consecutive_crashes", consecutiveCrashes). - Dur("current_backoff", backoff). - Msg("Monitor is crashing repeatedly — operator intervention may be required") + slog.Error("Monitor is crashing repeatedly — operator intervention may be required", + "error", err, + "reason", "MonitorDegraded", + "monitor", m.Name(), + "consecutive_crashes", consecutiveCrashes, + "current_backoff", backoff) } // A stable run resets both the back-off and the consecutive-crash counter. @@ -213,10 +210,9 @@ func SupervisedMonitor(ctx context.Context, m MonitorRunner, tracker *StatusTrac select { case <-ctx.Done(): setState("stopped") - logx.As().Info(). - Str("reason", "MonitorStopped"). - Str("monitor", m.Name()). - Msg("Monitor restart cancelled — context done") + slog.Info("Monitor restart cancelled — context done", + "reason", "MonitorStopped", + "monitor", m.Name()) return case <-time.After(backoff): } diff --git a/internal/daemon/server.go b/internal/daemon/server.go index 95977ba2..916c842d 100644 --- a/internal/daemon/server.go +++ b/internal/daemon/server.go @@ -5,13 +5,13 @@ package daemon import ( "context" "errors" + "log/slog" "net" "net/http" "os" "path/filepath" "time" - "github.com/automa-saga/logx" "github.com/hashgraph/solo-weaver/internal/daemon/core" ) @@ -102,12 +102,12 @@ func (s *Server) Start(ctx context.Context) error { return err } - logx.As().Info().Str("reason", "ServerStarted").Str("sock", sockPath).Msg("Daemon socket server listening") + slog.Info("Daemon socket server listening", "reason", "ServerStarted", "sock", sockPath) serveErr := make(chan error, 1) go func() { if err := s.srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { - logx.As().Error().Err(err).Str("reason", "ServerStopped").Msg("Daemon socket server exited with error") + slog.Error("Daemon socket server exited with error", "error", err, "reason", "ServerStopped") serveErr <- err } close(serveErr) @@ -115,7 +115,7 @@ func (s *Server) Start(ctx context.Context) error { select { case <-ctx.Done(): - logx.As().Info().Str("reason", "ServerStopped").Msg("Daemon socket server shutting down") + slog.Info("Daemon socket server shutting down", "reason", "ServerStopped") shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() shutdownErr := s.srv.Shutdown(shutdownCtx) From 3c8a98fa7933a490d54def70e4b662335ace89af Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Sat, 13 Jun 2026 09:55:53 +1000 Subject: [PATCH 2/3] fix(workflows): use errorx in consensus migration client Convert the 7 fmt.Errorf sites in consensus_migration_client.go to the errorx standard (forbidigo lint). Network calls use ExternalError, request marshal/build use InternalError, response decode uses IllegalFormat, and remote error statuses use ExternalError. Signed-off-by: Lenin Mehedy --- .../steps/consensus_migration_client.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/workflows/steps/consensus_migration_client.go b/internal/workflows/steps/consensus_migration_client.go index 923cfce5..529cec7d 100644 --- a/internal/workflows/steps/consensus_migration_client.go +++ b/internal/workflows/steps/consensus_migration_client.go @@ -5,11 +5,11 @@ package steps import ( "bytes" "encoding/json" - "fmt" "net/http" "time" "github.com/hashgraph/solo-weaver/internal/daemon/consensus" + "github.com/joomcode/errorx" ) // SoakStart sends POST /consensus_node/migration/soak/start to the daemon socket @@ -18,7 +18,7 @@ import ( func SoakStart(sockPath string, req consensus.SoakStartRequest) (*consensus.SoakStartResponse, error) { body, err := json.Marshal(req) if err != nil { - return nil, fmt.Errorf("marshal soak start request: %w", err) + return nil, errorx.InternalError.Wrap(err, "marshal soak start request") } resp, err := socketClient(sockPath).Post( @@ -27,7 +27,7 @@ func SoakStart(sockPath string, req consensus.SoakStartRequest) (*consensus.Soak bytes.NewReader(body), ) if err != nil { - return nil, fmt.Errorf("soak start: %w", err) + return nil, errorx.ExternalError.Wrap(err, "soak start") } defer resp.Body.Close() @@ -37,7 +37,7 @@ func SoakStart(sockPath string, req consensus.SoakStartRequest) (*consensus.Soak var out consensus.SoakStartResponse if err := json.NewDecoder(resp.Body).Decode(&out); err != nil { - return nil, fmt.Errorf("decode soak start response: %w", err) + return nil, errorx.IllegalFormat.Wrap(err, "decode soak start response") } return &out, nil } @@ -54,12 +54,12 @@ func SoakStop(sockPath string, keepState bool) error { req, err := http.NewRequest(http.MethodDelete, url, nil) if err != nil { - return fmt.Errorf("build soak stop request: %w", err) + return errorx.InternalError.Wrap(err, "build soak stop request") } resp, err := soakSocketClient(sockPath).Do(req) if err != nil { - return fmt.Errorf("soak stop: %w", err) + return errorx.ExternalError.Wrap(err, "soak stop") } defer resp.Body.Close() @@ -93,9 +93,9 @@ func decodeAPIError(resp *http.Response) error { } _ = json.NewDecoder(resp.Body).Decode(&body) if body.Error != "" { - return fmt.Errorf("daemon returned %d: %s", resp.StatusCode, body.Error) + return errorx.ExternalError.New("daemon returned %d: %s", resp.StatusCode, body.Error) } - return fmt.Errorf("daemon returned unexpected status %d", resp.StatusCode) + return errorx.ExternalError.New("daemon returned unexpected status %d", resp.StatusCode) } // soakClientTimeout is used by the soak client calls. Longer than the default From 29cc47328511589a516048fa29f06235eb5c1dee Mon Sep 17 00:00:00 2001 From: Lenin Mehedy Date: Sat, 13 Jun 2026 10:21:50 +1000 Subject: [PATCH 3/3] fix: remove incorrect catalog entry for daemon until daemon is released to support auto download Signed-off-by: Lenin Mehedy --- internal/workflows/steps/step_daemon.go | 6 ++++-- pkg/software/config_it_test.go | 23 +++++++++++++++++++++++ pkg/software/infrastructure-catalog.yaml | 15 --------------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/internal/workflows/steps/step_daemon.go b/internal/workflows/steps/step_daemon.go index 4e9deb0c..f29a8dea 100644 --- a/internal/workflows/steps/step_daemon.go +++ b/internal/workflows/steps/step_daemon.go @@ -89,8 +89,10 @@ func InstallDaemonBinaryStep(src DaemonBinarySource, paths models.WeaverPaths) * return automa.StepFailureReport(stp.Id(), automa.WithError(errorx.InternalError.Wrap(err, "failed to initialise daemon installer"). WithProperty(models.ErrPropertyResolution, []string{ - "The provisioner binary may be built without a catalog entry for solo-provisioner-daemon", - "Re-install the provisioner: sudo solo-provisioner install", + "Auto-download is unavailable until an official solo-provisioner-daemon release is published", + "Build the daemon locally and install it with --daemon-bin:", + " task build:daemon GOOS=linux GOARCH=", + " sudo solo-provisioner daemon service install --daemon-bin=", }))) } diff --git a/pkg/software/config_it_test.go b/pkg/software/config_it_test.go index 83b4275c..b6de2cc4 100644 --- a/pkg/software/config_it_test.go +++ b/pkg/software/config_it_test.go @@ -130,6 +130,29 @@ func Test_Config_GetSoftwareByName_Integration(t *testing.T) { require.Error(t, err, "Should return error for non-existent software") } +// Test_Config_DaemonAutoDownloadUnavailable_Integration locks in the contract that +// the solo-provisioner-daemon binary has no embedded catalog entry. There is no +// published daemon release yet, so the auto-download path (used when no --daemon-bin +// is supplied) must fail rather than attempt a broken download. NewDaemonInstaller +// resolves the catalog entry up front, so a missing entry surfaces as an error here. +// +// If/when a real daemon release with checksums is published and re-added to the +// catalog, this test should be replaced with one asserting the entry resolves. +func Test_Config_DaemonAutoDownloadUnavailable_Integration(t *testing.T) { + config, err := LoadInfrastructureCatalog() + require.NoError(t, err) + + _, err = config.GetHostArtifact(DaemonBinaryName) + require.Error(t, err, + "solo-provisioner-daemon must NOT have a catalog entry until a real release exists; "+ + "its presence would re-enable a broken auto-download path") + + _, err = NewDaemonInstaller() + require.Error(t, err, + "NewDaemonInstaller must fail without a catalog entry so the install step fails "+ + "cleanly when no --daemon-bin is supplied") +} + // Test_Config_GetDefaultVersion_Integration tests getting the default version from actual artifacts func Test_Config_GetDefaultVersion_Integration(t *testing.T) { config, err := LoadInfrastructureCatalog() diff --git a/pkg/software/infrastructure-catalog.yaml b/pkg/software/infrastructure-catalog.yaml index 853af966..3b3c7d64 100644 --- a/pkg/software/infrastructure-catalog.yaml +++ b/pkg/software/infrastructure-catalog.yaml @@ -244,21 +244,6 @@ host: algorithm: 'sha256' checksum: '9bb6c7e85c3166ad698ee11042706b2ffbce4b9d017ef96bb1ced3962d88256a' -- name: solo-provisioner-daemon - default: "0.0.0" - versions: - 0.0.0: - binaries: - - name: 'solo-provisioner-daemon' - url: 'https://github.com/hashgraph/solo-weaver/releases/download/daemon-v{{.VERSION}}/solo-provisioner-daemon-{{.OS}}-{{.ARCH}}' - linux: - amd64: - algorithm: 'sha256' - checksum: '' - arm64: - algorithm: 'sha256' - checksum: '' - - name: teleport default: "18.6.4" versions: