1
0
mirror of https://github.com/nxshock/gron.git synced 2024-11-27 03:41:00 +05:00

Compare commits

...

3 Commits

Author SHA1 Message Date
b118d18516 Update info about config directory 2022-11-20 14:59:39 +05:00
efa20ae0f0 Move job examples to separate dir 2022-11-20 14:56:40 +05:00
d9d9f0dcf0 Add JS for online data update 2022-11-20 14:44:03 +05:00
11 changed files with 199 additions and 73 deletions

View File

@ -4,46 +4,8 @@
## Usage
1. Create `gron.d` directory
2. Create job config in `gron.d/job1.conf` ([TOML](https://en.wikipedia.org/wiki/TOML) format):
```toml
Type = "cmd" # command execution
Category = "Test jobs" # jobs category name
Cron = "* * * * *" # cron instructions
1. Create `gron.d` directory (`JobConfigsPath` variable from `gron.conf` variable).
2. Create job config in `gron.d/job1.conf` ([TOML](https://en.wikipedia.org/wiki/TOML) format). See examples in [_jobExamples](_jobExamples).
Command = "echo Hello" # command to execute
```
SQL job:
```toml
Type = "sql" # sql execution
Cron = "* * * * *" # cron instructions
Description = "execute procedure every minute" # job description
Driver = "pgx" # "pgx" for Postgresql, "oracle" for Oracle, "sqlserver" for Microsoft SQL Server
ConnectionString = "postgres://login:password@host:port/database?sslmode=disable" # each driver has different syntax
SqlText = "CALL procedure" # command to execute
```
Add other options if needed:
```toml
Description = "print Hello every minute" # job description
NumberOfRestartAttemts = 3 # number of restart attemts
RestartSec = 5 # the time to sleep before restarting a job (seconds)
RestartRule = "on-error" # Configures whether the job shall be restarted when the job process exits
OnSuccessCmd = "echo 'Job finished.'" # execute cmd on job success
OnErrorCmd = "echo 'Error occurred: {{.Error}}'" # execute cmd on job error
OnSuccessHttpGetUrl = ""
OnErrorHttpGetUrl = "http://127.0.0.1/alerts?title={{.JobName}}%20failed&message={{.Error}}&tags=warning"
OnSuccessHttpPostUrl = "http://127.0.0.1/alerts"
OnSuccessMessageFmt = "Job {{.JobName}} finished."
OnErrorHttpPostUrl = "http://127.0.0.1/alerts"
OnErrorMessageFmt = "Job {{.JobName}} failed:\n\n{{.Error}}"
```
3. Launch `gron` binary
4. HTTP interface available on http://127.0.0.1:9876
3. Launch `gron` binary.
4. HTTP interface available on http://127.0.0.1:9876.

View File

@ -0,0 +1,23 @@
Type = "cmd" # command execution
Category = "Test jobs" # jobs category name
Description = "print 'Hello' every minute" # job description
Cron = "* * * * *" # cron instructions
Command = "echo Hello" # command to execute
NumberOfRestartAttemts = 3 # number of restart attemts
RestartSec = 5 # the time to sleep before restarting a job (seconds)
RestartRule = "on-error" # Configures whether the job shall be restarted when the job process exits
OnSuccessCmd = "echo 'Job finished.'" # execute cmd on job success
OnErrorCmd = "echo 'Error occurred: {{.Error}}'" # execute cmd on job error
# HTTP client callbacks
OnSuccessHttpGetUrl = "http://127.0.0.1/alerts?title={{.JobName}}%20finished"
OnErrorHttpGetUrl = "http://127.0.0.1/alerts?title={{.JobName}}%20failed&message={{.Error}}&tags=warning"
OnSuccessHttpPostUrl = "http://127.0.0.1/alerts"
OnSuccessMessageFmt = "Job {{.JobName}} finished."
OnErrorHttpPostUrl = "http://127.0.0.1/alerts"
OnErrorMessageFmt = "Job {{.JobName}} failed:\n\n{{.Error}}"

6
_jobExamples/basic.conf Normal file
View File

@ -0,0 +1,6 @@
Type = "cmd" # command execution
Category = "Test jobs" # jobs category name
Description = "print 'Hello' every minute" # job description
Cron = "* * * * *" # cron instructions
Command = "echo Hello" # command to execute

View File

@ -0,0 +1,7 @@
Type = "sql" # sql execution
Cron = "* * * * *" # cron instructions
Description = "execute procedure every minute" # job description
Driver = "pgx" # "pgx" for Postgresql, "oracle" for Oracle, "sqlserver" for Microsoft SQL Server
ConnectionString = "postgres://login:password@host:port/database?sslmode=disable" # each driver has different syntax
SqlText = "CALL procedure" # command to execute

View File

@ -1,8 +1,6 @@
package main
const (
programName = "gron"
defaultConfigFileName = "gron.conf"
defaultOnSuccessMessageFmt = "Job {{.JobName}} finished."

9
go.mod
View File

@ -3,14 +3,15 @@ module github.com/nxshock/gron
go 1.18
require (
github.com/BurntSushi/toml v1.2.0
github.com/BurntSushi/toml v1.2.1
github.com/antonfisher/nested-logrus-formatter v1.3.1
github.com/creasty/defaults v1.6.0
github.com/denisenkom/go-mssqldb v0.12.3
github.com/gorilla/websocket v1.5.0
github.com/jackc/pgx v3.6.2+incompatible
github.com/nxshock/logwriter v0.0.0-20220514172136-b1385d4106de
github.com/robfig/cron/v3 v3.0.1
github.com/sijms/go-ora/v2 v2.5.3
github.com/sijms/go-ora/v2 v2.5.6
github.com/sirupsen/logrus v1.9.0
github.com/stretchr/testify v1.8.0
)
@ -28,8 +29,8 @@ require (
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
golang.org/x/crypto v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/sys v0.2.0 // indirect
golang.org/x/text v0.4.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

18
go.sum
View File

@ -1,8 +1,8 @@
github.com/Azure/azure-sdk-for-go/sdk/azcore v0.19.0/go.mod h1:h6H6c8enJmmocHUbLiiGY6sx7f9i+X3m1CHdd5c6Rdw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v0.11.0/go.mod h1:HcM1YX14R7CJcghJGOYCgdezslRSVzqwLf/q+4Y2r/0=
github.com/Azure/azure-sdk-for-go/sdk/internal v0.7.0/go.mod h1:yqy467j36fJxcRV2TzfVZ1pCb5vxm4BtZPUdYWe/Xo8=
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak=
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1 h1:NFJIr+pzwv5QLHTPyKz9UMEoHck02Q9L0FP13b/xSbQ=
github.com/antonfisher/nested-logrus-formatter v1.3.1/go.mod h1:6WTfyWFkBc9+zyBaKIqRrg/KwMqBbodBjgbHjDz7zjA=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
@ -23,6 +23,8 @@ github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0kt
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
@ -53,8 +55,8 @@ github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBO
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sijms/go-ora/v2 v2.5.3 h1:klGKmhqRONVTtIzTdfYTvrW94kdJkdmZl93u2A3vchI=
github.com/sijms/go-ora/v2 v2.5.3/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk=
github.com/sijms/go-ora/v2 v2.5.6 h1:V53uhbcVpPrGBICnwBJx780+lcqfbTWLCi376B7Dr5A=
github.com/sijms/go-ora/v2 v2.5.6/go.mod h1:EHxlY6x7y9HAsdfumurRfTd+v8NrEOTR3Xl4FWlH6xk=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -66,8 +68,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
@ -78,8 +80,8 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0 h1:ljd4t30dBnAvMZaQCevtY0xLLD0A+bRZXbgLMLU1F/A=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@ -8,11 +8,49 @@ import (
"net"
"net/http"
"sort"
"sync"
"time"
"github.com/gorilla/websocket"
log "github.com/sirupsen/logrus"
)
type WsConnections struct {
connections map[*websocket.Conn]struct{}
mutex sync.Mutex
}
func (wc *WsConnections) Add(c *websocket.Conn) {
wc.mutex.Lock()
defer wc.mutex.Unlock()
wc.connections[c] = struct{}{}
}
func (wc *WsConnections) Delete(c *websocket.Conn) {
wc.mutex.Lock()
defer wc.mutex.Unlock()
delete(wc.connections, c)
}
func (wc *WsConnections) Send(message interface{}) {
for conn := range wc.connections {
go func(conn *websocket.Conn) { _ = conn.WriteJSON(message) }(conn)
}
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
var wsConnections = &WsConnections{
connections: make(map[*websocket.Conn]struct{})}
func httpServer(listenAddress string) {
if listenAddress == "none" {
return
@ -23,6 +61,7 @@ func httpServer(listenAddress string) {
http.HandleFunc("/shutdown", handleShutdown)
http.HandleFunc("/start", handleForceStart)
http.HandleFunc("/details", handleDetails)
http.HandleFunc("/ws", handleWebSocket)
log.WithField("job", "http_server").Fatal(http.ListenAndServe(listenAddress, nil))
}
@ -39,10 +78,9 @@ func handler(w http.ResponseWriter, r *http.Request) {
globalMutex.RLock()
buf := new(bytes.Buffer)
jobEntries := kernel.c.Entries()
jobs := make(map[string][]*Job)
for _, jobEntry := range jobEntries {
for _, jobEntry := range kernel.c.Entries() {
job := jobEntry.Job.(*Job)
job.NextLaunch = jobEntry.Next.Format(config.TimeFormat)
jobs[job.JobConfig.Category] = append(jobs[job.JobConfig.Category], job)
@ -73,6 +111,41 @@ func handler(w http.ResponseWriter, r *http.Request) {
_, _ = buf.WriteTo(w)
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
return
}
wsConnections.Add(conn)
defer wsConnections.Delete(conn)
var startMessage struct {
JobName string
}
for {
err := conn.ReadJSON(&startMessage)
if err != nil {
log.Println(err)
break
}
for _, jobEntry := range kernel.c.Entries() {
job := jobEntry.Job.(*Job)
if job.Name == startMessage.JobName {
host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
if err != nil {
host = r.RemoteAddr
}
log.WithField("job", "http_server").Printf("Forced start %s from %s.", job.Name, host)
go job.Run()
break
}
}
}
}
func handleForceStart(w http.ResponseWriter, r *http.Request) {
jobName := r.FormValue("jobName")
if jobName == "" {
@ -80,9 +153,7 @@ func handleForceStart(w http.ResponseWriter, r *http.Request) {
return
}
jobEntries := kernel.c.Entries()
for _, jobEntry := range jobEntries {
for _, jobEntry := range kernel.c.Entries() {
job := jobEntry.Job.(*Job)
if job.Name == jobName {
host, _, err := net.SplitHostPort(r.RemoteAddr)

23
job.go
View File

@ -128,6 +128,16 @@ func (j *Job) openAndMergeLog() (logEntry *log.Entry, jobLogFile *os.File) {
}
func (j *Job) Run() {
// TODO: переписать неоптимальный цикл
for _, jobEntry := range kernel.c.Entries() {
if jobEntry.Job.(*Job) != j {
continue
}
j.NextLaunch = jobEntry.Next.Format(config.TimeFormat)
break
}
log, jobLogFile := j.openAndMergeLog()
defer jobLogFile.Close()
@ -165,6 +175,8 @@ func (j *Job) runTry(log *log.Entry, jobLogFile *os.File) error {
j.LastStartTime = startTime.Format(config.TimeFormat)
globalMutex.Unlock()
wsConnections.Send(j)
var err error
switch j.JobConfig.Type {
case Cmd:
@ -196,6 +208,8 @@ func (j *Job) runTry(log *log.Entry, jobLogFile *os.File) error {
j.LastExecutionDuration = endTime.Sub(startTime).Truncate(time.Second).String()
globalMutex.Unlock()
wsConnections.Send(j)
return err
}
@ -318,12 +332,5 @@ func (j *Job) errorMessage(err error) string {
}
func runSimpleCmd(command string, args ...string) error {
log.Println(command)
log.Println(args)
err := exec.Command(command, args...).Run()
if err != nil {
log.Println(">>", err)
}
return err
return exec.Command(command, args...).Run()
}

View File

@ -8,6 +8,6 @@ import (
func format(fmt string, v interface{}) string {
t := new(template.Template)
b := new(strings.Builder)
template.Must(t.Parse(fmt)).Execute(b, v) // TODO: обработать возможные ошибки
_ = template.Must(t.Parse(fmt)).Execute(b, v) // TODO: обработать возможные ошибки
return b.String()
}

View File

@ -6,6 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<title>gron</title>
<link rel="stylesheet" href="/style.css">
<link rel="icon" href="data:,">
</head>
<body>
@ -35,10 +36,9 @@
<th>Details</th>
</tr>
{{range (index $.Jobs .)}}
<tr>
<tr id="{{.Name}}">
<td class="no-padding">
<form action="/start" method="get" id="form-{{.Name}}"></form>
<button{{if gt .CurrentRunningCount 0}} class="runningbg" {{else}}{{if .LastError}} class="errorbg" {{end}}{{end}} type="submit" form="form-{{.Name}}" name="jobName" value="{{.Name}}" {{if gt .CurrentRunningCount 0}} disabled{{end}}>{{.Name}}</button>
<button{{if gt .CurrentRunningCount 0}} class="runningbg" {{else}}{{if .LastError}} class="errorbg" {{end}}{{end}} name="jobName" value="{{.Name}}" {{if gt .CurrentRunningCount 0}} disabled{{end}} onclick='startJob("{{.Name}}")'>{{.Name}}</button>
</td>
<td class="smaller">{{.JobConfig.Description}}</td>
<td class="nowrap" align="right">
@ -54,5 +54,54 @@
</table>{{end}}
</main>
</body>
<script>
let socket = new WebSocket("ws://" + window.location.host + "/ws")
socket.onerror = function(error) {
console.log("WebSocket error: " + JSON.stringify(error))
socket.close()
}
socket.onmessage = function(event) {
message = JSON.parse(event.data);
html4 = "unknown"
if (message.Status == 0) {
html4 = "&#x2bc0; inactive"
} else if (message.Status == 1) {
html4 = '<span class="green">&#x2bc8; running</span>'
} else if (message.Status == 2) {
html4 = '<span class="red">&#x2bc1; error</span>'
} else if (message.Status == 3) {
html4 = '<span class="orange">&#x27f3; restarting</span>'
}
if (message.CurrentRunningCount > 0) {
document.querySelector("#" + message.Name + " > td:nth-child(1) > button").className = "runningbg"
} else if (message.LastError != "") {
document.querySelector("#" + message.Name + " > td:nth-child(1) > button").className = "errorbg"
} else {
document.querySelector("#" + message.Name + " > td:nth-child(1) > button").removeAttribute("class")
}
if (message.CurrentRunningCount > 0) {
document.querySelector("#" + message.Name + " > td:nth-child(1) > button").setAttribute("disabled", "true")
} else {
document.querySelector("#" + message.Name + " > td:nth-child(1) > button").removeAttribute("disabled")
}
document.querySelector("#" + message.Name + " > td:nth-child(4)").innerHTML = html4
document.querySelector("#" + message.Name + " > td:nth-child(5)").innerHTML = message.LastStartTime
document.querySelector("#" + message.Name + " > td:nth-child(6)").innerHTML = message.LastEndTime
document.querySelector("#" + message.Name + " > td:nth-child(7)").innerHTML = message.LastExecutionDuration
document.querySelector("#" + message.Name + " > td:nth-child(8)").innerHTML = message.NextLaunch
}
function startJob(jobName) {
socket.send(JSON.stringify({
jobName
}));
}
</script>
</html>