Skip to content

Commit da7ccb2

Browse files
committed
refactor: centralize check execution and classify error kinds
1 parent e1b4048 commit da7ccb2

File tree

9 files changed

+173
-50
lines changed

9 files changed

+173
-50
lines changed

internal/checker/dns/request.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
mdns "github.com/miekg/dns"
1111

12+
"github.com/pixel365/pulse/internal/e"
13+
1214
"github.com/pixel365/pulse/internal/config"
1315
)
1416

@@ -34,11 +36,14 @@ func (c *Checker) request(ctx context.Context) error {
3436
}
3537

3638
if res == nil {
37-
return fmt.Errorf("empty dns response")
39+
return e.NewError(e.ErrProtocol, "empty dns response")
3840
}
3941

4042
if res.Rcode != mdns.RcodeSuccess {
41-
return fmt.Errorf("unexpected dns response code: %s", mdns.RcodeToString[res.Rcode])
43+
return e.NewError(
44+
e.ErrProtocol,
45+
fmt.Sprintf("unexpected dns response code: %s", mdns.RcodeToString[res.Rcode]),
46+
)
4247
}
4348

4449
values, err := collectAnswers(res.Answer, c.config.Spec.RecordType)
@@ -47,10 +52,9 @@ func (c *Checker) request(ctx context.Context) error {
4752
}
4853

4954
if len(values) == 0 {
50-
return fmt.Errorf(
51-
"no %s records found for %s",
52-
c.config.Spec.RecordType,
53-
c.config.Spec.Name,
55+
return e.NewError(
56+
e.ErrConstraint,
57+
fmt.Sprintf("no %s records found for %s", c.config.Spec.RecordType, c.config.Spec.Name),
5458
)
5559
}
5660

@@ -202,7 +206,10 @@ func checkAnswers(expect *config.DNSExpect, recordType config.RecordType, actual
202206
for _, want := range expect.Contains {
203207
value := normalizeValue(recordType, want)
204208
if _, ok := actualSet[value]; !ok {
205-
return fmt.Errorf("expected dns answer to contain %q", want)
209+
return e.NewError(
210+
e.ErrConstraint,
211+
fmt.Sprintf("expected dns answer to contain %q", want),
212+
)
206213
}
207214
}
208215

@@ -217,12 +224,18 @@ func checkAnswers(expect *config.DNSExpect, recordType config.RecordType, actual
217224

218225
expected = uniqueSorted(expected)
219226
if len(expected) != len(actual) {
220-
return fmt.Errorf("dns answers mismatch: expected %v, got %v", expected, actual)
227+
return e.NewError(
228+
e.ErrConstraint,
229+
fmt.Sprintf("dns answers mismatch: expected %v, got %v", expected, actual),
230+
)
221231
}
222232

223233
for i := range expected {
224234
if expected[i] != actual[i] {
225-
return fmt.Errorf("dns answers mismatch: expected %v, got %v", expected, actual)
235+
return e.NewError(
236+
e.ErrConstraint,
237+
fmt.Sprintf("dns answers mismatch: expected %v, got %v", expected, actual),
238+
)
226239
}
227240
}
228241

internal/checker/grpc/request.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"google.golang.org/grpc/credentials/insecure"
1010
healthpb "google.golang.org/grpc/health/grpc_health_v1"
1111
"google.golang.org/grpc/metadata"
12+
13+
"github.com/pixel365/pulse/internal/e"
1214
)
1315

1416
func (c *Checker) request(ctx context.Context) error {
@@ -21,7 +23,10 @@ func (c *Checker) request(ctx context.Context) error {
2123
grpc.WithTransportCredentials(insecure.NewCredentials()),
2224
)
2325
if err != nil {
24-
return fmt.Errorf("could not create grpc client: %w", err)
26+
return e.NewError(
27+
e.ErrInternal,
28+
fmt.Sprintf("could not create grpc client: %v", err),
29+
)
2530
}
2631

2732
defer func() {
@@ -46,10 +51,13 @@ func (c *Checker) request(ctx context.Context) error {
4651
}
4752

4853
if got := resp.GetStatus().String(); got != string(c.config.Spec.ExpectedHealthStatus) {
49-
return fmt.Errorf(
50-
"unexpected grpc health status: expected %s, got %s",
51-
c.config.Spec.ExpectedHealthStatus,
52-
got,
54+
return e.NewError(
55+
e.ErrConstraint,
56+
fmt.Sprintf(
57+
"unexpected grpc health status: expected %s, got %s",
58+
c.config.Spec.ExpectedHealthStatus,
59+
got,
60+
),
5361
)
5462
}
5563

internal/checker/http/request.go

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import (
44
"bytes"
55
"context"
66
"encoding/json"
7-
"errors"
87
"fmt"
98
"io"
109
h "net/http"
1110
"net/url"
1211
"strings"
1312

1413
"github.com/pixel365/pulse/internal/config"
14+
"github.com/pixel365/pulse/internal/e"
1515
)
1616

1717
func (c *Checker) request(ctx context.Context) error {
@@ -97,7 +97,10 @@ func makeRequest(ctx context.Context, config Alias) (*h.Request, error) {
9797
}
9898
req = rq
9999
default:
100-
return nil, fmt.Errorf("unsupported method: %s", config.Spec.Method)
100+
return nil, e.NewError(
101+
e.ErrInternal,
102+
fmt.Sprintf("unsupported method: %s", config.Spec.Method),
103+
)
101104
}
102105

103106
return req, nil
@@ -113,7 +116,7 @@ func checkCode(statusCode int, codes []int) error {
113116
}
114117

115118
if !success {
116-
return fmt.Errorf("unsuccess code %d", statusCode)
119+
return e.NewError(e.ErrProtocol, fmt.Sprintf("unsuccess code %d", statusCode))
117120
}
118121

119122
return nil
@@ -135,7 +138,10 @@ func checkBody(expect *config.StringExpect, res io.ReadCloser) error {
135138
}
136139

137140
if !bodyOk {
138-
return errors.New("response body does not contain expected value")
141+
return e.NewError(
142+
e.ErrConstraint,
143+
"response body does not contain expected value",
144+
)
139145
}
140146

141147
if equals != "" {
@@ -144,7 +150,7 @@ func checkBody(expect *config.StringExpect, res io.ReadCloser) error {
144150
}
145151

146152
if !bodyOk {
147-
return errors.New("response body does not equal expected value")
153+
return e.NewError(e.ErrConstraint, "response body does not equal expected value")
148154
}
149155

150156
return nil

internal/checker/tcp/request.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/pixel365/pulse/internal/config"
12+
"github.com/pixel365/pulse/internal/e"
1213
)
1314

1415
func (c *Checker) request(ctx context.Context) error {
@@ -70,11 +71,14 @@ func checkEquals(conn net.Conn, expect *config.StringExpect) error {
7071

7172
response := string(body)
7273
if expect.Contains != "" && !strings.Contains(response, expect.Contains) {
73-
return fmt.Errorf("tcp response does not contain %q", expect.Contains)
74+
return e.NewError(
75+
e.ErrConstraint,
76+
fmt.Sprintf("tcp response does not contain %q", expect.Contains),
77+
)
7478
}
7579

7680
if response != expect.Equals {
77-
return fmt.Errorf("tcp response does not equal expected value")
81+
return e.NewError(e.ErrConstraint, "tcp response does not equal expected value")
7882
}
7983

8084
return nil
@@ -104,5 +108,8 @@ func checkContains(conn net.Conn, contains string) error {
104108
return fmt.Errorf("could not read tcp response: %w", err)
105109
}
106110

107-
return fmt.Errorf("tcp response does not contain %q", contains)
111+
return e.NewError(
112+
e.ErrConstraint,
113+
fmt.Sprintf("tcp response does not contain %q", contains),
114+
)
108115
}

internal/checker/tls/request.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"fmt"
77
"net"
88
"time"
9+
10+
"github.com/pixel365/pulse/internal/e"
911
)
1012

1113
func (c *Checker) request(ctx context.Context) error {
@@ -33,7 +35,10 @@ func (c *Checker) request(ctx context.Context) error {
3335
tlsConn, ok := conn.(*ctls.Conn)
3436
if !ok {
3537
_ = conn.Close()
36-
return fmt.Errorf("unexpected connection type: %T", conn)
38+
return e.NewError(
39+
e.ErrInternal,
40+
fmt.Sprintf("unexpected connection type: %T", conn),
41+
)
3742
}
3843

3944
defer func() {
@@ -42,16 +47,19 @@ func (c *Checker) request(ctx context.Context) error {
4247

4348
state := tlsConn.ConnectionState()
4449
if len(state.PeerCertificates) == 0 {
45-
return fmt.Errorf("no peer certificates presented")
50+
return e.NewError(e.ErrProtocol, "no peer certificates presented")
4651
}
4752

4853
leaf := state.PeerCertificates[0]
4954
validityLeft := time.Until(leaf.NotAfter)
5055
if validityLeft < c.config.Spec.MinValidity {
51-
return fmt.Errorf(
52-
"certificate validity %s is below required minimum %s",
53-
validityLeft.Truncate(time.Second),
54-
c.config.Spec.MinValidity,
56+
return e.NewError(
57+
e.ErrConstraint,
58+
fmt.Sprintf(
59+
"certificate validity %s is below required minimum %s",
60+
validityLeft.Truncate(time.Second),
61+
c.config.Spec.MinValidity,
62+
),
5563
)
5664
}
5765

internal/e/errors.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package e
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net"
7+
)
8+
9+
type ErrorKind string
10+
11+
const (
12+
// ErrNone means the execution completed without an error.
13+
ErrNone ErrorKind = ""
14+
15+
// ErrTimeout covers context deadlines and operation timeouts.
16+
ErrTimeout ErrorKind = "timeout"
17+
18+
// ErrNetwork covers connection establishment and transport-level failures.
19+
ErrNetwork ErrorKind = "network"
20+
21+
// ErrProtocol means the endpoint responded, but the protocol-level response was invalid.
22+
ErrProtocol ErrorKind = "protocol"
23+
24+
// ErrConstraint means the endpoint responded, but the observed result did not satisfy check expectations.
25+
ErrConstraint ErrorKind = "constraint"
26+
27+
// ErrInternal is used for local checker/runtime errors unrelated to the target system.
28+
ErrInternal ErrorKind = "internal"
29+
30+
// ErrUnknown is a fallback for errors that were not classified more precisely.
31+
ErrUnknown ErrorKind = "unknown"
32+
)
33+
34+
type KindError struct {
35+
Err error
36+
Kind ErrorKind
37+
}
38+
39+
func (e *KindError) Error() string {
40+
if e == nil || e.Err == nil {
41+
return ""
42+
}
43+
44+
return e.Err.Error()
45+
}
46+
47+
func (e *KindError) Unwrap() error {
48+
if e == nil {
49+
return nil
50+
}
51+
52+
return e.Err
53+
}
54+
55+
func NewError(kind ErrorKind, msg string) error {
56+
return &KindError{
57+
Kind: kind,
58+
Err: errors.New(msg),
59+
}
60+
}
61+
62+
func ResolveError(err error) (ErrorKind, string) {
63+
if err == nil {
64+
return ErrNone, ""
65+
}
66+
67+
if kindErr, ok := errors.AsType[*KindError](err); ok && kindErr != nil {
68+
return kindErr.Kind, err.Error()
69+
}
70+
71+
switch {
72+
case errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
73+
return ErrTimeout, err.Error()
74+
}
75+
76+
if netErr, ok := errors.AsType[net.Error](err); ok {
77+
if netErr.Timeout() {
78+
return ErrTimeout, err.Error()
79+
}
80+
81+
return ErrNetwork, err.Error()
82+
}
83+
84+
if _, ok := errors.AsType[*net.OpError](err); ok {
85+
return ErrNetwork, err.Error()
86+
}
87+
88+
return ErrUnknown, err.Error()
89+
}

internal/executor.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"time"
77

88
"github.com/pixel365/pulse/internal/config"
9+
"github.com/pixel365/pulse/internal/e"
910
"github.com/pixel365/pulse/internal/model"
1011
)
1112

@@ -51,12 +52,14 @@ func (c *CheckExec) execute(
5152
var err error
5253

5354
result := model.CheckExecutionResult{
54-
ExecutionID: rand.Text(),
55-
CheckID: c.cfg.ID,
56-
ServiceID: c.cfg.Service,
57-
CheckType: c.cfg.Type,
58-
Status: model.Success,
59-
StartedAt: time.Now().UTC(),
55+
ExecutionID: rand.Text(),
56+
CheckID: c.cfg.ID,
57+
ServiceID: c.cfg.Service,
58+
CheckType: c.cfg.Type,
59+
Status: model.Success,
60+
StartedAt: time.Now().UTC(),
61+
ErrorKind: e.ErrNone,
62+
ErrorMessage: "",
6063
}
6164

6265
attempts := c.cfg.Retries + 1
@@ -82,8 +85,7 @@ func (c *CheckExec) execute(
8285

8386
if err != nil {
8487
result.Status = model.Failure
85-
} else {
86-
result.Status = model.Success
88+
result.ErrorKind, result.ErrorMessage = e.ResolveError(err)
8789
}
8890

8991
return result

internal/model/errors.go

Lines changed: 0 additions & 12 deletions
This file was deleted.

0 commit comments

Comments
 (0)