diff --git a/client.go b/client.go index 2e0d5aa..d5f29c8 100644 --- a/client.go +++ b/client.go @@ -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 diff --git a/cloudns.go b/cloudns.go index bf7265d..7130b4c 100644 --- a/cloudns.go +++ b/cloudns.go @@ -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 } diff --git a/cmd/monitor/cmd_monitor.go b/cmd/monitor/cmd_monitor.go index 9ce4ab2..7b144bf 100644 --- a/cmd/monitor/cmd_monitor.go +++ b/cmd/monitor/cmd_monitor.go @@ -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) } diff --git a/cmd/prod/cmd_dns.go b/cmd/prod/cmd_dns.go index 4f362d4..d15325e 100644 --- a/cmd/prod/cmd_dns.go +++ b/cmd/prod/cmd_dns.go @@ -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 { diff --git a/cmd/prod/cmd_lb.go b/cmd/prod/cmd_lb.go index 0c6c935..29d8b9d 100644 --- a/cmd/prod/cmd_lb.go +++ b/cmd/prod/cmd_lb.go @@ -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 { diff --git a/cmd/prod/cmd_setup.go b/cmd/prod/cmd_setup.go index 3e54442..de7c0cc 100644 --- a/cmd/prod/cmd_setup.go +++ b/cmd/prod/cmd_setup.go @@ -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) diff --git a/cmd/prod/cmd_ssh.go b/cmd/prod/cmd_ssh.go index 7760836..0f53189 100644 --- a/cmd/prod/cmd_ssh.go +++ b/cmd/prod/cmd_ssh.go @@ -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 diff --git a/cmd/prod/cmd_status.go b/cmd/prod/cmd_status.go index b69b66c..15d9d29 100644 --- a/cmd/prod/cmd_status.go +++ b/cmd/prod/cmd_status.go @@ -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 diff --git a/config.go b/config.go index 9ac9df0..a75a2b3 100644 --- a/config.go +++ b/config.go @@ -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. diff --git a/hetzner.go b/hetzner.go index de0336c..c9ff370 100644 --- a/hetzner.go +++ b/hetzner.go @@ -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 {