Skip to content

Commit 88cbd30

Browse files
committed
rewrite sentry to support info messages
1 parent e319cc9 commit 88cbd30

File tree

4 files changed

+194
-98
lines changed

4 files changed

+194
-98
lines changed

guard.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ type (
3131
Config *Config
3232

3333
Status *CmdStatus
34+
Reporter *Reporter
3435
}
3536

3637
// CmdStatus is the commands status
@@ -69,7 +70,7 @@ func main() {
6970

7071
r := chained(
7172
runner, timeout, validateStdout, validateStderr, quietIgnore,
72-
sentryHandler, lockfile, headerize, combineLogs, insertUUID,
73+
lockfile, sentryHandler, headerize, combineLogs, insertUUID,
7374
writeSyslog, setupLogs,
7475
)
7576
err := r(context.Background(), &cr)

helpers.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"io/ioutil"
9+
"os"
10+
"strconv"
811
"strings"
12+
"syscall"
913
"time"
1014

1115
"github.com/robfig/cron"
@@ -79,3 +83,37 @@ func isQuiet(cr *CmdRequest) (bool, error) {
7983
}
8084
return false, nil
8185
}
86+
87+
// handleLockfile validates the lockfile and checks if the command should be run
88+
func handleExistingLockfile(cr *CmdRequest) (bool, error) {
89+
_, statErr := os.Stat(cr.Lockfile)
90+
if statErr == nil {
91+
pidBytes, err := ioutil.ReadFile(cr.Lockfile)
92+
if err != nil {
93+
return false, fmt.Errorf("unable to read lockfile: %s", err)
94+
}
95+
pid, err := strconv.Atoi(string(pidBytes))
96+
if err != nil {
97+
return false, fmt.Errorf("unable to read pidfile: %s", err)
98+
}
99+
proc, err := os.FindProcess(pid)
100+
if err != nil {
101+
return false, fmt.Errorf("process(%d) from pidfile missing: %s", pid, err)
102+
}
103+
err = proc.Signal(syscall.Signal(0))
104+
if err == nil {
105+
_, _ = fmt.Fprintf(cr.Status.Combined, "cron is still running, pid: %d", pid)
106+
return false, nil
107+
} else {
108+
// if we have an orphaned pid, we try to report that to our reporter and continue
109+
logErr := fmt.Errorf("process(%d) from pidfile missing: %s", pid, err)
110+
if cr.Reporter != nil {
111+
cr.Reporter.Info(logErr)
112+
}
113+
return true, nil
114+
}
115+
} else if !os.IsNotExist(statErr) {
116+
return false, fmt.Errorf("unable to handle lockfile: %s", statErr)
117+
}
118+
return true, nil
119+
}

middlewares.go

Lines changed: 13 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,14 @@ import (
44
"bufio"
55
"bytes"
66
"context"
7-
"crypto/sha256"
8-
"encoding/hex"
97
"errors"
108
"fmt"
119
"io"
12-
"io/ioutil"
1310
"log"
1411
"log/syslog"
1512
"os"
16-
"strconv"
17-
"strings"
18-
"syscall"
1913
"time"
2014

21-
"github.com/getsentry/sentry-go"
2215
"golang.org/x/sync/errgroup"
2316
)
2417

@@ -70,7 +63,7 @@ func insertUUID(g GuardFunc) GuardFunc {
7063
if cr.ErrFileHideUUID {
7164
return g(ctx, cr)
7265
}
73-
combined := newUUIDPrefixer(cr.Status.Combined)
66+
combined := newUUIDPrefixer(cr.Status.Combined)
7467
cr.Status.Combined = combined
7568
return g(ctx, cr)
7669
}
@@ -117,31 +110,16 @@ func headerize(g GuardFunc) GuardFunc {
117110
func lockfile(g GuardFunc) GuardFunc {
118111
return func(ctx context.Context, cr *CmdRequest) (err error) {
119112
if cr.Lockfile != "" {
120-
_, statErr := os.Stat(cr.Lockfile)
121-
if statErr == nil {
122-
pidBytes, err := ioutil.ReadFile(cr.Lockfile)
123-
if err != nil {
124-
return fmt.Errorf("unable to read lockfile: %s", err)
125-
}
126-
pid, err := strconv.Atoi(string(pidBytes))
127-
if err != nil {
128-
return fmt.Errorf("unable to read pidfile: %s", err)
129-
}
130-
proc, err := os.FindProcess(pid)
131-
if err != nil {
132-
return fmt.Errorf("process(%d) from pidfile missing: %s", pid, err)
133-
}
134-
err = proc.Signal(syscall.Signal(0))
135-
if err != nil {
136-
return fmt.Errorf("process(%d) from pidfile missing: %s", pid, err)
137-
}
138-
_, _ = fmt.Fprintf(cr.Status.Combined, "cron is still running, pid: %d", pid)
113+
run, err := handleExistingLockfile(cr)
114+
if err != nil {
115+
return err
116+
}
117+
if !run {
139118
return nil
140-
} else if !os.IsNotExist(statErr) {
141-
return fmt.Errorf("unable to handle lockfile: %s", statErr)
142119
}
120+
143121
pid := os.Getpid()
144-
lockfile, err := os.OpenFile(cr.Lockfile, os.O_CREATE|os.O_RDWR, 0600)
122+
lockfile, err := os.OpenFile(cr.Lockfile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0600)
145123
if err != nil {
146124
return fmt.Errorf("unable to open lockfile: %s", err)
147125
}
@@ -157,80 +135,18 @@ func lockfile(g GuardFunc) GuardFunc {
157135
}
158136
}
159137

138+
// sentryHandler redirects all errors to a sentry if configured
160139
func sentryHandler(g GuardFunc) GuardFunc {
161140
return func(ctx context.Context, cr *CmdRequest) (err error) {
162-
// check if envar is set
163-
sentryDSN, ok := os.LookupEnv("CRONGUARD_SENTRY_DSN")
164-
if !ok && cr.Config != nil {
165-
sentryDSN = cr.Config.SentryDSN
166-
}
167-
if sentryDSN == "" {
141+
reporter, reporterErr := newReporter(cr)
142+
if reporterErr != nil {
168143
return g(ctx, cr)
169144
}
170145

171-
// wrap buffers
172-
start := time.Now()
173-
combined := bytes.NewBuffer([]byte{})
174-
stderr := bytes.NewBuffer([]byte{})
175-
cr.Status.Stderr = io.MultiWriter(stderr, combined, cr.Status.Stderr)
176-
cr.Status.Stdout = io.MultiWriter(combined, cr.Status.Stdout)
177-
178-
// prepare sentry
179-
sentryErr := sentry.Init(sentry.ClientOptions{
180-
Dsn: sentryDSN,
181-
Transport: sentry.NewHTTPSyncTransport(),
182-
})
183-
if sentryErr != nil {
184-
fmt.Fprintf(cr.Status.Stderr, "cronguard: unable to connect to sentry: %s\n", sentryErr)
185-
fmt.Fprintf(cr.Status.Stderr, "cronguard: running cron anyways\n")
186-
}
187-
146+
cr.Reporter = reporter
188147
err = g(ctx, cr)
189-
190-
// try to log to sentry
191-
if err != nil && sentryErr == nil {
192-
// gather data
193-
hostname, _ := os.Hostname()
194-
if hostname == "" {
195-
hostname = "no-hostname"
196-
}
197-
hostname = strings.SplitN(hostname, ".", 2)[0]
198-
cmd := cr.Command
199-
if len(cmd) > 32 {
200-
cmd = fmt.Sprintf("%s%s", cmd[0:30], "...")
201-
}
202-
cmdHash := sha256.New()
203-
cmdHash.Write([]byte(cr.Command))
204-
cmdHash.Write([]byte(hostname))
205-
hash := hex.EncodeToString(cmdHash.Sum(nil))
206-
207-
// add data to message
208-
sentry.ConfigureScope(func(scope *sentry.Scope) {
209-
scope.SetExtra("time_start", start)
210-
scope.SetExtra("time_end", time.Now())
211-
scope.SetExtra("time_duration", time.Since(start).String())
212-
scope.SetExtra("out_combined", combined.String())
213-
scope.SetExtra("out_stderr", stderr.String())
214-
scope.SetExtra("command", cr.Command)
215-
scope.SetFingerprint([]string{hash})
216-
})
217-
name := fmt.Sprintf(
218-
"%s: %s (%s)",
219-
hostname,
220-
cmd,
221-
err.Error(),
222-
)
223-
_ = sentry.CaptureMessage(name)
224-
225-
// hide error if messages are successfully flushed to sentry
226-
flushed := sentry.Flush(30 * time.Second)
227-
if flushed {
228-
return nil
229-
}
230-
}
231-
return err
148+
return reporter.Finish(err)
232149
}
233-
234150
}
235151

236152
// quietIgnore allows to ignore errors on lower settings if flag is set

sentry.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"crypto/sha256"
6+
"encoding/hex"
7+
"fmt"
8+
"hash"
9+
"io"
10+
"os"
11+
"strings"
12+
"time"
13+
14+
"github.com/getsentry/sentry-go"
15+
)
16+
17+
type (
18+
Reporter struct {
19+
sentryDSN string
20+
start time.Time
21+
hostname string
22+
cmd string
23+
hash hash.Hash
24+
25+
combined *bytes.Buffer
26+
stderr *bytes.Buffer
27+
}
28+
)
29+
30+
// newReporter creates a new Sentry client
31+
func newReporter(cr *CmdRequest) (*Reporter, error) {
32+
sentryDSN, ok := os.LookupEnv("CRONGUARD_SENTRY_DSN")
33+
if !ok && cr.Config != nil {
34+
sentryDSN = cr.Config.SentryDSN
35+
}
36+
if sentryDSN == "" {
37+
return nil, fmt.Errorf("no config provided")
38+
}
39+
40+
// data
41+
hostname, _ := os.Hostname()
42+
if hostname == "" {
43+
hostname = "no-hostname"
44+
}
45+
hostname = strings.SplitN(hostname, ".", 2)[0]
46+
hash := sha256.New()
47+
hash.Write([]byte(cr.Command))
48+
hash.Write([]byte(hostname))
49+
cmd := cr.Command
50+
if len(cmd) > 32 {
51+
cmd = fmt.Sprintf("%s%s", cmd[0:30], "...")
52+
}
53+
54+
// setup sentry
55+
sentryErr := sentry.Init(sentry.ClientOptions{
56+
Dsn: sentryDSN,
57+
Transport: sentry.NewHTTPSyncTransport(),
58+
})
59+
if sentryErr != nil {
60+
fmt.Fprintf(cr.Status.Stderr, "cronguard: unable to connect to sentry: %s\n", sentryErr)
61+
fmt.Fprintf(cr.Status.Stderr, "cronguard: running cron anyways\n")
62+
return nil, fmt.Errorf("unable to connect to sentry")
63+
}
64+
65+
// wrap buffers
66+
start := time.Now()
67+
combined := bytes.NewBuffer([]byte{})
68+
stderr := bytes.NewBuffer([]byte{})
69+
cr.Status.Stderr = io.MultiWriter(stderr, combined, cr.Status.Stderr)
70+
cr.Status.Stdout = io.MultiWriter(combined, cr.Status.Stdout)
71+
72+
// set known sentry extras
73+
sentry.ConfigureScope(func(scope *sentry.Scope) {
74+
scope.SetExtra("time_start", start)
75+
scope.SetExtra("command", cr.Command)
76+
})
77+
78+
return &Reporter{
79+
sentryDSN: sentryDSN,
80+
start: start,
81+
hostname: hostname,
82+
cmd: cmd,
83+
hash: hash,
84+
combined: combined,
85+
stderr: stderr,
86+
}, nil
87+
}
88+
89+
// Finish reports the final status to sentry if err != nil
90+
func (r *Reporter) Finish(err error) error {
91+
if err == nil {
92+
return nil
93+
}
94+
return r.report(err, finishLevel)
95+
}
96+
97+
// Info reports a Info status to sentry
98+
func (r *Reporter) Info(err error) error {
99+
return r.report(err, infoLevel)
100+
}
101+
102+
// reportLevel is used by reporter to disingques
103+
type reportLevel = string
104+
105+
const (
106+
// infoLevel is an information that will be send to sentry
107+
infoLevel reportLevel = "info"
108+
// finishLevel is used to tell the reporter that the cron has finished
109+
finishLevel = "finish"
110+
)
111+
112+
// report reports any error message to sentry
113+
func (r *Reporter) report(err error, level reportLevel) error {
114+
// prepare sentry information
115+
name := ""
116+
extra := map[string]interface{}{}
117+
if level == finishLevel {
118+
name = fmt.Sprintf("%s: %s (%s)", r.hostname, r.cmd, err.Error())
119+
extra["time_end"] = time.Now()
120+
extra["time_duration"] = time.Since(r.start).String()
121+
extra["out_combined"] = r.combined.String()
122+
extra["out_stderr"] = r.stderr.String()
123+
} else {
124+
name = fmt.Sprintf("%s (%s): %s (%s)", r.hostname, level, r.cmd, err.Error())
125+
}
126+
127+
// sentry
128+
hash := hex.EncodeToString(r.hash.Sum([]byte(level)))
129+
sentry.ConfigureScope(func(scope *sentry.Scope) {
130+
scope.SetFingerprint([]string{hash})
131+
scope.SetExtras(extra)
132+
})
133+
_ = sentry.CaptureMessage(name)
134+
135+
// hide error if messages are successfully flushed to sentry
136+
flushed := sentry.Flush(30 * time.Second)
137+
if !flushed {
138+
return err
139+
}
140+
return nil
141+
}

0 commit comments

Comments
 (0)