forked from abohmeed/cronmanager
-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
154 lines (144 loc) · 4.39 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
package main
import (
"bufio"
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"log"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"syscall"
"time"
"github.com/juju/fslock"
)
const exporterPath string = "/opt/prometheus/exporters/dist/textfile/crons.prom"
//isDelayed: Used to signal that the cron job delay was triggered
var (
isDelayed = false
jobStartTime time.Time
jobDuration float64
flgVersion bool
version string
)
func main() {
version = "1.1.18"
cmdPtr := flag.String("c", "", "[Required] The `cron job` command")
jobnamePtr := flag.String("n", "", "[Required] The `job name` to appear in the alarm")
logfilePtr := flag.String("l", "", "[Optional] The `log file` to store the cron output")
flag.BoolVar(&flgVersion, "version", false, "if true print version and exit")
flag.Parse()
if flgVersion {
fmt.Println("CronManager version " + version)
os.Exit(0)
}
flag.Usage = func() {
fmt.Printf("Usage: cronmanager -c command -n jobname [ -l log file ]\nExample: cronmanager \"/usr/bin/php /var/www/app.zlien.com/console broadcast:entities:updated -e project -l 20000\" -n update_entitites_cron -t 3600 -l /path/to/log\n")
flag.PrintDefaults()
}
if *cmdPtr == "" || *jobnamePtr == "" {
flag.Usage()
os.Exit(1)
}
// Parse the command by extracting the first token as the command and the rest as its args
cmdArr := strings.Split(*cmdPtr, " ")
cmdBin := cmdArr[0]
cmdArgs := cmdArr[1:]
cmd := exec.Command(cmdBin, cmdArgs...)
var buf bytes.Buffer
// If we have a log file specified, use it
if *logfilePtr != "" {
outfile, err := os.Create(*logfilePtr)
if err != nil {
panic(err)
}
defer outfile.Close()
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
panic(err)
}
writer := bufio.NewWriter(outfile)
defer writer.Flush()
go io.Copy(writer, stdoutPipe)
} else {
cmd.Stdout = &buf
}
err := cmd.Start()
if err != nil {
log.Fatal(err)
}
//Record the start time of the job
jobStartTime = time.Now()
//Start a ticker in a goroutine that will write an alarm metric if the job exceeds the time
go func() {
for range time.Tick(time.Second) {
jobDuration = time.Since(jobStartTime).Seconds()
writeToExporter(*jobnamePtr, "duration", strconv.FormatFloat(jobDuration, 'f', 0, 64))
}
}()
// Execute the command
if err := cmd.Wait(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if _, ok := exiterr.Sys().(syscall.WaitStatus); ok {
writeToExporter(*jobnamePtr, "failed", "1")
// Bting the duration to zero to denote that the job is no longer running
writeToExporter(*jobnamePtr, "duration", "0")
}
} else {
log.Fatalf("cmd.Wait: %v", err)
}
} else {
// The job had no errors
writeToExporter(*jobnamePtr, "failed", "0")
// Bting the duration to zero to denote that the job is no longer running
writeToExporter(*jobnamePtr, "duration", "0")
// In all cases, unlock the file
}
}
func writeToExporter(jobName string, label string, metric string) {
jobNeedle := "cronjob{name=\"" + jobName + "\",dimension=\"" + label + "\"}"
typeData := "# TYPE cron_job gauge"
jobData := jobNeedle + " " + metric
// Lock filepath to prevent race conditions
lock := fslock.New(exporterPath)
err := lock.Lock()
if err != nil {
log.Println("Error locking file " + exporterPath)
}
defer lock.Unlock()
input, err := ioutil.ReadFile(exporterPath)
if err != nil {
// We're not sure why we can't read from the file. Let's try creating it and fail if that didn't work either
if _, err := os.Create(exporterPath); err != nil {
log.Fatal("Couldn't read or write to the exporter file. Check parent directory permissions")
}
}
re := regexp.MustCompile(jobNeedle + `.*\n`)
// If we have the job data alrady, just replace it and that's it
if re.Match(input) {
input = re.ReplaceAll(input, []byte(jobData+"\n"))
} else {
// If the job is not there then either there is no TYPE header at all and this is the first job
if re := regexp.MustCompile(typeData); !re.Match(input) {
// Add the TYPE and the job data
input = append(input, typeData+"\n"...)
input = append(input, jobData+"\n"...)
} else {
// Or there is a TYPE header with one or more other jobs. Just append the job to the TYPE header
input = re.ReplaceAll(input, []byte(typeData+"\n"+jobData))
}
}
f, err := os.Create(exporterPath)
if err != nil {
log.Fatal(err)
}
defer f.Close()
if _, err = f.Write(input); err != nil {
log.Fatal(err)
}
f.Sync()
}