refactor: replace fmt.Errorf and errors.New with coreerr.E from go-log

Replace all remaining fmt.Errorf and errors.New calls in production code
with structured coreerr.E(op, msg, err) from forge.lthn.ai/core/go-log.
Covers 10 files across the infra package and cmd/prod and cmd/monitor.

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-16 20:34:42 +00:00
parent 6beb06686a
commit 9a24df3d5f
10 changed files with 55 additions and 50 deletions

View file

@ -10,6 +10,8 @@ import (
"strconv"
"sync"
"time"
coreerr "forge.lthn.ai/core/go-log"
)
// RetryConfig controls exponential backoff retry behaviour.
@ -105,7 +107,7 @@ func (a *APIClient) Do(req *http.Request, result any) error {
resp, err := a.client.Do(req)
if err != nil {
lastErr = fmt.Errorf("%s: %w", a.prefix, err)
lastErr = coreerr.E(a.prefix, "request failed", err)
if attempt < attempts-1 {
a.backoff(attempt, req)
}
@ -115,7 +117,7 @@ func (a *APIClient) Do(req *http.Request, result any) error {
data, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
lastErr = fmt.Errorf("read response: %w", err)
lastErr = coreerr.E("client.Do", "read response", err)
if attempt < attempts-1 {
a.backoff(attempt, req)
}
@ -129,7 +131,7 @@ func (a *APIClient) Do(req *http.Request, result any) error {
a.blockedUntil = time.Now().Add(retryAfter)
a.mu.Unlock()
lastErr = fmt.Errorf("%s %d: rate limited", a.prefix, resp.StatusCode)
lastErr = coreerr.E(a.prefix, fmt.Sprintf("rate limited: HTTP %d", resp.StatusCode), nil)
if attempt < attempts-1 {
select {
case <-req.Context().Done():
@ -142,7 +144,7 @@ func (a *APIClient) Do(req *http.Request, result any) error {
// Server errors are retryable.
if resp.StatusCode >= 500 {
lastErr = fmt.Errorf("%s %d: %s", a.prefix, resp.StatusCode, string(data))
lastErr = coreerr.E(a.prefix, fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(data)), nil)
if attempt < attempts-1 {
a.backoff(attempt, req)
}
@ -151,13 +153,13 @@ func (a *APIClient) Do(req *http.Request, result any) error {
// Client errors (4xx, except 429 handled above) are not retried.
if resp.StatusCode >= 400 {
return fmt.Errorf("%s %d: %s", a.prefix, resp.StatusCode, string(data))
return coreerr.E(a.prefix, fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(data)), nil)
}
// Success — decode if requested.
if result != nil {
if err := json.Unmarshal(data, result); err != nil {
return fmt.Errorf("decode response: %w", err)
return coreerr.E("client.Do", "decode response", err)
}
}
return nil
@ -191,7 +193,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
resp, err := a.client.Do(req)
if err != nil {
lastErr = fmt.Errorf("%s: %w", a.prefix, err)
lastErr = coreerr.E(a.prefix, "request failed", err)
if attempt < attempts-1 {
a.backoff(attempt, req)
}
@ -201,7 +203,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
data, err := io.ReadAll(resp.Body)
_ = resp.Body.Close()
if err != nil {
lastErr = fmt.Errorf("read response: %w", err)
lastErr = coreerr.E("client.DoRaw", "read response", err)
if attempt < attempts-1 {
a.backoff(attempt, req)
}
@ -214,7 +216,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
a.blockedUntil = time.Now().Add(retryAfter)
a.mu.Unlock()
lastErr = fmt.Errorf("%s %d: rate limited", a.prefix, resp.StatusCode)
lastErr = coreerr.E(a.prefix, fmt.Sprintf("rate limited: HTTP %d", resp.StatusCode), nil)
if attempt < attempts-1 {
select {
case <-req.Context().Done():
@ -226,7 +228,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
}
if resp.StatusCode >= 500 {
lastErr = fmt.Errorf("%s %d: %s", a.prefix, resp.StatusCode, string(data))
lastErr = coreerr.E(a.prefix, fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(data)), nil)
if attempt < attempts-1 {
a.backoff(attempt, req)
}
@ -234,7 +236,7 @@ func (a *APIClient) DoRaw(req *http.Request) ([]byte, error) {
}
if resp.StatusCode >= 400 {
return nil, fmt.Errorf("%s %d: %s", a.prefix, resp.StatusCode, string(data))
return nil, coreerr.E(a.prefix, fmt.Sprintf("HTTP %d: %s", resp.StatusCode, string(data)), nil)
}
return data, nil

View file

@ -3,10 +3,11 @@ package infra
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
coreerr "forge.lthn.ai/core/go-log"
)
const cloudnsBaseURL = "https://api.cloudns.net"
@ -81,7 +82,7 @@ func (c *CloudNSClient) ListRecords(ctx context.Context, domain string) (map[str
var records map[string]CloudNSRecord
if err := json.Unmarshal(data, &records); err != nil {
return nil, fmt.Errorf("parse records: %w", err)
return nil, coreerr.E("CloudNSClient.ListRecords", "parse records", err)
}
return records, nil
}
@ -108,11 +109,11 @@ func (c *CloudNSClient) CreateRecord(ctx context.Context, domain, host, recordTy
} `json:"data"`
}
if err := json.Unmarshal(data, &result); err != nil {
return "", fmt.Errorf("parse response: %w", err)
return "", coreerr.E("CloudNSClient.CreateRecord", "parse response", err)
}
if result.Status != "Success" {
return "", fmt.Errorf("cloudns: %s", result.StatusDescription)
return "", coreerr.E("CloudNSClient.CreateRecord", result.StatusDescription, nil)
}
return strconv.Itoa(result.Data.ID), nil
@ -138,11 +139,11 @@ func (c *CloudNSClient) UpdateRecord(ctx context.Context, domain, recordID, host
StatusDescription string `json:"statusDescription"`
}
if err := json.Unmarshal(data, &result); err != nil {
return fmt.Errorf("parse response: %w", err)
return coreerr.E("CloudNSClient.UpdateRecord", "parse response", err)
}
if result.Status != "Success" {
return fmt.Errorf("cloudns: %s", result.StatusDescription)
return coreerr.E("CloudNSClient.UpdateRecord", result.StatusDescription, nil)
}
return nil
@ -164,11 +165,11 @@ func (c *CloudNSClient) DeleteRecord(ctx context.Context, domain, recordID strin
StatusDescription string `json:"statusDescription"`
}
if err := json.Unmarshal(data, &result); err != nil {
return fmt.Errorf("parse response: %w", err)
return coreerr.E("CloudNSClient.DeleteRecord", "parse response", err)
}
if result.Status != "Success" {
return fmt.Errorf("cloudns: %s", result.StatusDescription)
return coreerr.E("CloudNSClient.DeleteRecord", result.StatusDescription, nil)
}
return nil
@ -179,7 +180,7 @@ func (c *CloudNSClient) DeleteRecord(ctx context.Context, domain, recordID strin
func (c *CloudNSClient) EnsureRecord(ctx context.Context, domain, host, recordType, value string, ttl int) (bool, error) {
records, err := c.ListRecords(ctx, domain)
if err != nil {
return false, fmt.Errorf("list records: %w", err)
return false, coreerr.E("CloudNSClient.EnsureRecord", "list records", err)
}
// Check if record already exists
@ -190,7 +191,7 @@ func (c *CloudNSClient) EnsureRecord(ctx context.Context, domain, host, recordTy
}
// Update existing record
if err := c.UpdateRecord(ctx, domain, id, host, recordType, value, ttl); err != nil {
return false, fmt.Errorf("update record: %w", err)
return false, coreerr.E("CloudNSClient.EnsureRecord", "update record", err)
}
return true, nil
}
@ -198,7 +199,7 @@ func (c *CloudNSClient) EnsureRecord(ctx context.Context, domain, host, recordTy
// Create new record
if _, err := c.CreateRecord(ctx, domain, host, recordType, value, ttl); err != nil {
return false, fmt.Errorf("create record: %w", err)
return false, coreerr.E("CloudNSClient.EnsureRecord", "create record", err)
}
return true, nil
}

View file

@ -582,5 +582,5 @@ func parseGitHubRepo(url string) (string, error) {
}
}
return "", fmt.Errorf("could not parse GitHub repo from URL: %s", url)
return "", log.E("monitor.parseGitHubRepo", "could not parse GitHub repo from URL: "+url, nil)
}

View file

@ -2,12 +2,11 @@ package prod
import (
"context"
"errors"
"fmt"
"os"
"time"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-infra"
)
@ -56,7 +55,7 @@ func getDNSClient() (*infra.CloudNSClient, error) {
authID := os.Getenv("CLOUDNS_AUTH_ID")
authPass := os.Getenv("CLOUDNS_AUTH_PASSWORD")
if authID == "" || authPass == "" {
return nil, errors.New("CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD required")
return nil, coreerr.E("prod.getDNSClient", "CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD required", nil)
}
return infra.NewCloudNSClient(authID, authPass), nil
}
@ -77,7 +76,7 @@ func runDNSList(cmd *cli.Command, args []string) error {
records, err := dns.ListRecords(ctx, zone)
if err != nil {
return fmt.Errorf("list records: %w", err)
return coreerr.E("prod.runDNSList", "list records", err)
}
cli.Print("%s DNS records for %s\n\n", cli.BoldStyle.Render("▶"), cli.TitleStyle.Render(zone))
@ -114,7 +113,7 @@ func runDNSSet(cmd *cli.Command, args []string) error {
changed, err := dns.EnsureRecord(ctx, dnsZone, host, recordType, value, dnsTTL)
if err != nil {
return fmt.Errorf("set record: %w", err)
return coreerr.E("prod.runDNSSet", "set record", err)
}
if changed {

View file

@ -2,12 +2,12 @@ package prod
import (
"context"
"errors"
"fmt"
"os"
"time"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-infra"
)
@ -39,7 +39,7 @@ func init() {
func getHCloudClient() (*infra.HCloudClient, error) {
token := os.Getenv("HCLOUD_TOKEN")
if token == "" {
return nil, errors.New("HCLOUD_TOKEN environment variable required")
return nil, coreerr.E("prod.getHCloudClient", "HCLOUD_TOKEN environment variable required", nil)
}
return infra.NewHCloudClient(token), nil
}
@ -55,7 +55,7 @@ func runLBStatus(cmd *cli.Command, args []string) error {
lbs, err := hc.ListLoadBalancers(ctx)
if err != nil {
return fmt.Errorf("list load balancers: %w", err)
return coreerr.E("prod.runLBStatus", "list load balancers", err)
}
if len(lbs) == 0 {

View file

@ -2,12 +2,11 @@ package prod
import (
"context"
"errors"
"fmt"
"os"
"time"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-infra"
)
@ -71,7 +70,7 @@ func runSetup(cmd *cli.Command, args []string) error {
if err := step.fn(ctx, cfg); err != nil {
cli.Print(" %s %s: %s\n", cli.ErrorStyle.Render("✗"), step.name, err)
return fmt.Errorf("step %s failed: %w", step.name, err)
return coreerr.E("prod.setup", "step "+step.name+" failed", err)
}
cli.Print(" %s %s complete\n", cli.SuccessStyle.Render("✓"), step.name)
@ -90,7 +89,7 @@ func stepDiscover(ctx context.Context, cfg *infra.Config) error {
hc := infra.NewHCloudClient(hcloudToken)
servers, err := hc.ListServers(ctx)
if err != nil {
return fmt.Errorf("list HCloud servers: %w", err)
return coreerr.E("prod.stepDiscover", "list HCloud servers", err)
}
for _, s := range servers {
@ -115,7 +114,7 @@ func stepDiscover(ctx context.Context, cfg *infra.Config) error {
hr := infra.NewHRobotClient(robotUser, robotPass)
servers, err := hr.ListServers(ctx)
if err != nil {
return fmt.Errorf("list Robot servers: %w", err)
return coreerr.E("prod.stepDiscover", "list Robot servers", err)
}
for _, s := range servers {
@ -141,7 +140,7 @@ func stepDiscover(ctx context.Context, cfg *infra.Config) error {
func stepLoadBalancer(ctx context.Context, cfg *infra.Config) error {
hcloudToken := os.Getenv("HCLOUD_TOKEN")
if hcloudToken == "" {
return errors.New("HCLOUD_TOKEN required for load balancer management")
return coreerr.E("prod.stepLoadBalancer", "HCLOUD_TOKEN required for load balancer management", nil)
}
hc := infra.NewHCloudClient(hcloudToken)
@ -149,7 +148,7 @@ func stepLoadBalancer(ctx context.Context, cfg *infra.Config) error {
// Check if LB already exists
lbs, err := hc.ListLoadBalancers(ctx)
if err != nil {
return fmt.Errorf("list load balancers: %w", err)
return coreerr.E("prod.stepLoadBalancer", "list load balancers", err)
}
for _, lb := range lbs {
@ -176,7 +175,7 @@ func stepLoadBalancer(ctx context.Context, cfg *infra.Config) error {
for _, b := range cfg.LoadBalancer.Backends {
host, ok := cfg.Hosts[b.Host]
if !ok {
return fmt.Errorf("backend host '%s' not found in config", b.Host)
return coreerr.E("prod.stepLoadBalancer", "backend host '"+b.Host+"' not found in config", nil)
}
targets = append(targets, infra.HCloudLBCreateTarget{
Type: "ip",
@ -224,7 +223,7 @@ func stepLoadBalancer(ctx context.Context, cfg *infra.Config) error {
lb, err := hc.CreateLoadBalancer(ctx, req)
if err != nil {
return fmt.Errorf("create load balancer: %w", err)
return coreerr.E("prod.stepLoadBalancer", "create load balancer", err)
}
cli.Print(" Created: %s (ID: %d, IP: %s)\n",
@ -237,7 +236,7 @@ func stepDNS(ctx context.Context, cfg *infra.Config) error {
authID := os.Getenv("CLOUDNS_AUTH_ID")
authPass := os.Getenv("CLOUDNS_AUTH_PASSWORD")
if authID == "" || authPass == "" {
return errors.New("CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD required")
return coreerr.E("prod.stepDNS", "CLOUDNS_AUTH_ID and CLOUDNS_AUTH_PASSWORD required", nil)
}
dns := infra.NewCloudNSClient(authID, authPass)

View file

@ -7,6 +7,7 @@ import (
"syscall"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
)
var sshCmd = &cli.Command{
@ -37,7 +38,7 @@ func runSSH(cmd *cli.Command, args []string) error {
for n, h := range cfg.Hosts {
cli.Print(" %s %s (%s)\n", cli.BoldStyle.Render(n), h.IP, h.Role)
}
return fmt.Errorf("host '%s' not found in infra.yaml", name)
return coreerr.E("prod.ssh", "host '"+name+"' not found in infra.yaml", nil)
}
sshArgs := []string{
@ -55,7 +56,7 @@ func runSSH(cmd *cli.Command, args []string) error {
sshPath, err := exec.LookPath("ssh")
if err != nil {
return fmt.Errorf("ssh not found: %w", err)
return coreerr.E("prod.ssh", "ssh not found", err)
}
// Replace current process with SSH

View file

@ -10,6 +10,7 @@ import (
"forge.lthn.ai/core/go-ansible"
"forge.lthn.ai/core/cli/pkg/cli"
coreerr "forge.lthn.ai/core/go-log"
"forge.lthn.ai/core/go-infra"
)
@ -114,14 +115,14 @@ func checkHost(ctx context.Context, name string, host *infra.Host) hostStatus {
client, err := ansible.NewSSHClient(sshCfg)
if err != nil {
s.Error = fmt.Errorf("create SSH client: %w", err)
s.Error = coreerr.E("prod.checkHost", "create SSH client", err)
return s
}
defer func() { _ = client.Close() }()
start := time.Now()
if err := client.Connect(ctx); err != nil {
s.Error = fmt.Errorf("SSH connect: %w", err)
s.Error = coreerr.E("prod.checkHost", "SSH connect", err)
return s
}
s.Connected = true

View file

@ -3,10 +3,10 @@
package infra
import (
"fmt"
"os"
"path/filepath"
coreerr "forge.lthn.ai/core/go-log"
coreio "forge.lthn.ai/core/go-io"
"gopkg.in/yaml.v3"
)
@ -232,12 +232,12 @@ type BackupJob struct {
func Load(path string) (*Config, error) {
data, err := coreio.Local.Read(path)
if err != nil {
return nil, fmt.Errorf("read infra config: %w", err)
return nil, coreerr.E("infra.Load", "read infra config", err)
}
var cfg Config
if err := yaml.Unmarshal([]byte(data), &cfg); err != nil {
return nil, fmt.Errorf("parse infra config: %w", err)
return nil, coreerr.E("infra.Load", "parse infra config", err)
}
// Expand SSH key paths
@ -269,7 +269,7 @@ func Discover(startDir string) (*Config, string, error) {
}
dir = parent
}
return nil, "", fmt.Errorf("infra.yaml not found (searched from %s)", startDir)
return nil, "", coreerr.E("infra.Discover", "infra.yaml not found (searched from "+startDir+")", nil)
}
// HostsByRole returns all hosts matching the given role.

View file

@ -6,6 +6,8 @@ import (
"fmt"
"net/http"
"strings"
coreerr "forge.lthn.ai/core/go-log"
)
const (
@ -210,7 +212,7 @@ func (c *HCloudClient) GetLoadBalancer(ctx context.Context, id int) (*HCloudLoad
func (c *HCloudClient) CreateLoadBalancer(ctx context.Context, req HCloudLBCreateRequest) (*HCloudLoadBalancer, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, fmt.Errorf("marshal request: %w", err)
return nil, coreerr.E("HCloudClient.CreateLoadBalancer", "marshal request", err)
}
var result struct {