Skip to content

Commit

Permalink
sentry support
Browse files Browse the repository at this point in the history
  • Loading branch information
foosinn committed May 18, 2020
1 parent 8f43b53 commit 3e7ac0c
Show file tree
Hide file tree
Showing 74 changed files with 17,175 additions and 69 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# project binaries
# project binaries and files
cronguard
cronguard-linux-386
cronguard-linux-amd64
cronguard-linux-arm
cronguard-linux-arm64
cronguard.yaml
cronguard.yml
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,32 @@ Simple wrapper to log and handle cron errors.

Example:

```
```sh
cronguard -name cron.example "command"
```

The command is executed with `bash -c`. You can use bash features like pipes.

**Note**: Bash is required.

### Sentry Support

To enable sentry you can either create a `/etc/cronguard.yaml`, create `./cronguard.yaml` or use the environment
variable `CRONGUARD_SENTRY_DSN`.

If one of these is set cronguard will try to send events to sentry. If thats not possible it will fallback to default
behavior.

Config Example:

```yaml
sentry_dsn: https://[email protected]/2
```
### Quiet-Times
Using `-quiet-times` one can setup time ranges during which errors are ignored. Useful to disable error handling, for example, if there is a database backup running.
Using `-quiet-times` one can setup time ranges during which errors are ignored. Useful to disable error handling,
for example, if there is a database backup running.

Example:

Expand All @@ -55,6 +70,6 @@ Golang time duration documentation: https://golang.org/pkg/time/#ParseDuration

Via go:

```
```sh
go get -u github.com/bitsbeats/cronguard
```
39 changes: 39 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"os"

"gopkg.in/yaml.v2"
)

type (
// Config holds the optional and global configuration
Config struct {
SentryDSN string `yaml:"sentry_dsn"`
}
)

// ParseConfig loads the Configfile if there is one or uses defaults
func ParseConfig() *Config {
c := Config{}
file, err := open("cronguard.yml", "cronguard.yaml", "/etc/cronguard.yml", "/etc/cronguard.yaml")
if err != nil {
return &c
}
_ = yaml.NewDecoder(file).Decode(&c)
return &c
}

func open(files ...string) (*os.File, error) {
err := error(nil)
for _, file := range files {
file, err := os.Open(file)
if err == nil {
return file, nil
}
if !os.IsNotExist(err) {
return nil, err
}
}
return nil, err
}
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@ module github.com/bitsbeats/cronguard
go 1.12

require (
github.com/google/go-cmp v0.3.0
github.com/getsentry/sentry-go v0.5.1
github.com/google/go-cmp v0.4.0
github.com/robfig/cron v1.2.0
github.com/rs/xid v1.2.1
golang.org/x/sync v0.0.0-20190423024810-112230192c58
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
gopkg.in/yaml.v2 v2.2.4
)
184 changes: 182 additions & 2 deletions go.sum

Large diffs are not rendered by default.

25 changes: 19 additions & 6 deletions guard.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,17 @@ type (

Regex *regexp.Regexp

Config *Config

Status *CmdStatus
}

// CmdStatus is the commands status
CmdStatus struct {
Stdout io.Writer
Stderr io.Writer
Combined io.Writer
ExitCode int
Stdout io.Writer // captures stdout
Stderr io.Writer // captures stderr
Combined io.Writer // captures stdout and stderr
ExitCode int // captures the exitcode
}

// GuardFunc is a middleware function
Expand All @@ -45,6 +47,7 @@ type (

func main() {
cr := CmdRequest{}
cr.Config = ParseConfig()
cr.Status = &CmdStatus{}
f := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
f.StringVar(&cr.Name, "name", "guard", "cron name in syslog")
Expand All @@ -64,7 +67,11 @@ func main() {
}
cr.Command = f.Arg(0)

r := chained(runner, timeout, validateStdout, validateStderr, quietIgnore, lockfile, headerize, combineLogs, insertUUID, writeSyslog, setupLogs)
r := chained(
runner, timeout, validateStdout, validateStderr, quietIgnore,
sentryHandler, lockfile, headerize, combineLogs, insertUUID,
writeSyslog, setupLogs,
)
err := r(context.Background(), &cr)
if err != nil {
log.Fatalf("execution failed: %s", err)
Expand Down Expand Up @@ -94,7 +101,13 @@ func runner() GuardFunc {

err = cmd.Wait()
if err != nil {
cr.Status.ExitCode = err.(*exec.ExitError).ExitCode()
switch casted := err.(type) {
case *exec.ExitError:
cr.Status.ExitCode = casted.ExitCode()
default:
cr.Status.ExitCode = 1
err = fmt.Errorf("unable to execute command: %w", err)
}
return err
}
return err
Expand Down
3 changes: 2 additions & 1 deletion guard_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,8 @@ func TestOutput(t *testing.T) {
{"sleep 1", []string{"-timeout", "2s"}, ""},
{"sleep 2", []string{"-timeout", "500ms"}, "// error: context deadline exceeded\n"},
}
for _, c := range cases {
for i, c := range cases {
t.Logf("running case %d: %+v", i+1, c)
err = guard(t, c.additionalArgs, c.command, c.content)
if err != nil {
t.Error(err)
Expand Down
88 changes: 85 additions & 3 deletions middlewares.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"bufio"
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
Expand All @@ -12,9 +14,11 @@ import (
"log/syslog"
"os"
"strconv"
"strings"
"syscall"
"time"

"github.com/getsentry/sentry-go"
"github.com/rs/xid"
"golang.org/x/sync/errgroup"
)
Expand Down Expand Up @@ -166,6 +170,82 @@ func lockfile(g GuardFunc) GuardFunc {
}
}

func sentryHandler(g GuardFunc) GuardFunc {
return func(ctx context.Context, cr *CmdRequest) (err error) {
// check if envar is set
sentryDSN, ok := os.LookupEnv("CRONGUARD_SENTRY_DSN")
if !ok && cr.Config != nil {
sentryDSN = cr.Config.SentryDSN
}
if sentryDSN == "" {
return g(ctx, cr)
}

// wrap buffers
start := time.Now()
combined := bytes.NewBuffer([]byte{})
stderr := bytes.NewBuffer([]byte{})
cr.Status.Stderr = io.MultiWriter(stderr, combined, cr.Status.Stderr)
cr.Status.Stdout = io.MultiWriter(combined, cr.Status.Stdout)

// prepare sentry
sentryErr := sentry.Init(sentry.ClientOptions{
Dsn: sentryDSN,
Transport: sentry.NewHTTPSyncTransport(),
})
if sentryErr != nil {
fmt.Fprintf(cr.Status.Stderr, "cronguard: unable to connect to sentry: %s\n", sentryErr)
fmt.Fprintf(cr.Status.Stderr, "cronguard: running cron anyways\n")
}

err = g(ctx, cr)

// try to log to sentry
if err != nil && sentryErr == nil {
// gather data
hostname, _ := os.Hostname()
if hostname == "" {
hostname = "no-hostname"
}
hostname = strings.SplitN(hostname, ".", 2)[0]
cmd := cr.Command
if len(cmd) > 32 {
cmd = fmt.Sprintf("%s%s", cmd[0:30], "...")
}
cmdHash := sha256.New()
cmdHash.Write([]byte(cr.Command))
cmdHash.Write([]byte(hostname))
hash := hex.EncodeToString(cmdHash.Sum(nil))

// add data to message
sentry.ConfigureScope(func(scope *sentry.Scope) {
scope.SetExtra("time_start", start)
scope.SetExtra("time_end", time.Now())
scope.SetExtra("time_duration", time.Since(start).String())
scope.SetExtra("out_combined", combined.String())
scope.SetExtra("out_stderr", stderr.String())
scope.SetExtra("command", cr.Command)
scope.SetFingerprint([]string{hash})
})
name := fmt.Sprintf(
"%s: %s (%s)",
hostname,
cmd,
err.Error(),
)
_ = sentry.CaptureMessage(name)

// hide error if messages are successfully flushed to sentry
flushed := sentry.Flush(30 * time.Second)
if flushed {
return nil
}
}
return err
}

}

// quietIgnore allows to ignore errors on lower settings if flag is set
func quietIgnore(g GuardFunc) GuardFunc {
return func(ctx context.Context, cr *CmdRequest) (err error) {
Expand Down Expand Up @@ -208,17 +288,19 @@ func validateStdout(g GuardFunc) GuardFunc {

s := bufio.NewScanner(out)
errGrp := errgroup.Group{}
errGrp.Go(func() (err error) {
errGrp.Go(func() error {
var err error
for s.Scan() {
line := s.Bytes()
if readErr := s.Err(); readErr != nil {
return readErr
}
if cr.Regex.Match(line) {
match := cr.Regex.Match(line)
if match {
err = fmt.Errorf("bad keyword in command output: %s", line)
}
}
return
return err
})

err = g(ctx, cr)
Expand Down
Loading

0 comments on commit 3e7ac0c

Please sign in to comment.