feat: gotify notification
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
This commit is contained in:
parent
660fc317ab
commit
01e283f96b
5 changed files with 173 additions and 28 deletions
44
README.md
44
README.md
|
|
@ -9,6 +9,7 @@ A lightweight, efficient cron-like scheduler written in Go that executes shell c
|
|||
- **Comprehensive Logging**: Detailed execution logs using `zerolog` with configurable levels
|
||||
- **Error Handling**: Robust error handling with exit code tracking and output capture
|
||||
- **Shell Command Support**: Execute any shell command via bash
|
||||
- **Notifications**: Send job status notifications via Gotify
|
||||
|
||||
## Installation
|
||||
|
||||
|
|
@ -46,12 +47,40 @@ jobs:
|
|||
|
||||
### Configuration Fields
|
||||
|
||||
| Field | Description | Format | Examples |
|
||||
|-------|-------------|--------|----------|
|
||||
| `name` | Descriptive job name for logging | String | "Daily Backup", "Health Check" |
|
||||
| `minute` | Minute pattern (0-59) | String | `"0"`, `"*/5"`, `"15,30,45"`, `"*"` |
|
||||
| `hour` | Hour pattern (0-23) | String | `"2"`, `"*/2"`, `"9-17"`, `"*"` |
|
||||
| `command` | Shell command to execute | String | `"echo 'Hello'"`, `"backup.sh"` |
|
||||
|| Field | Description | Format | Examples |
|
||||
||-------|-------------|--------|----------|
|
||||
|| `name` | Descriptive job name for logging | String | "Daily Backup", "Health Check" |
|
||||
|| `minute` | Minute pattern (0-59) | String | `"0"`, `"*/5"`, `"15,30,45"`, `"*"` |
|
||||
|| `hour` | Hour pattern (0-23) | String | `"2"`, `"*/2"`, `"9-17"`, `"*"` |
|
||||
|| `command` | Shell command to execute | String | `"echo 'Hello'"`, `"backup.sh"` |
|
||||
|| `notification` | Name of notification configuration to use | String | `"default"` |
|
||||
|
||||
### Notifications
|
||||
|
||||
You can configure notifications for job success or failure using Gotify:
|
||||
|
||||
```yaml
|
||||
notification:
|
||||
- name: default
|
||||
success:
|
||||
gotify:
|
||||
url: https://gotify.example.com/message?token=YOUR_TOKEN
|
||||
error:
|
||||
gotify:
|
||||
url: https://gotify.example.com/message?token=YOUR_TOKEN
|
||||
|
||||
jobs:
|
||||
- name: "Daily Backup"
|
||||
minute: "0"
|
||||
hour: "2"
|
||||
command: "backup.sh /data /backup"
|
||||
notification: default
|
||||
```
|
||||
|
||||
Notifications support:
|
||||
- Multiple named notification configurations
|
||||
- Separate success and error notification URLs
|
||||
- Optional configuration per job
|
||||
|
||||
### Pattern Syntax
|
||||
- `*` - Every minute/hour
|
||||
|
|
@ -98,5 +127,4 @@ jobs:
|
|||
minute: "0"
|
||||
hour: "3"
|
||||
command: "cleanup.sh"
|
||||
```
|
||||
|
||||
```
|
||||
32
config.go
32
config.go
|
|
@ -12,14 +12,36 @@ import (
|
|||
)
|
||||
|
||||
type config struct {
|
||||
Jobs []jobconfig `yaml:"jobs"`
|
||||
Jobs []jobconfig `yaml:"jobs"`
|
||||
Notification []notificationConfig `yaml:"notification"`
|
||||
}
|
||||
|
||||
type jobconfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Hour string `yaml:"hour"`
|
||||
Minute string `yaml:"minute"`
|
||||
Command string `yaml:"command"`
|
||||
Name string `yaml:"name"`
|
||||
Hour string `yaml:"hour"`
|
||||
Minute string `yaml:"minute"`
|
||||
Command string `yaml:"command"`
|
||||
Notification string `yaml:"notification,omitempty"`
|
||||
}
|
||||
|
||||
type notificationConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
Success notification `yaml:"success,omitempty"`
|
||||
Error notification `yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
type notification struct {
|
||||
Gotify gotifyConfig `yaml:"gotify,omitempty"`
|
||||
}
|
||||
|
||||
type gotifyConfig struct {
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
func (n notification) SendNotification(job jobconfig, execCode int, outString string, err error) error {
|
||||
if n.Gotify.URL != "" {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReadFromFile(path string) (config, error) {
|
||||
|
|
|
|||
40
gotify.go
Normal file
40
gotify.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Title string `json:"title,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Priority int `json:"priority,omitempty"`
|
||||
Extras map[string]interface{} `json:"extras,omitempty"`
|
||||
}
|
||||
|
||||
func SendGotifyNotification(url string, msg Message) error {
|
||||
bodyBytes, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encode payload: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("HTTP request error: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status: %s", resp.Status)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
15
jobResult.go
Normal file
15
jobResult.go
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
package main
|
||||
|
||||
type jobResult struct {
|
||||
job jobconfig
|
||||
outputString string
|
||||
execCode int
|
||||
err error
|
||||
}
|
||||
|
||||
func (j jobResult) isSuccess() bool {
|
||||
if j.execCode == 0 && j.err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
70
main.go
70
main.go
|
|
@ -28,7 +28,7 @@ func main() {
|
|||
for {
|
||||
select {
|
||||
case t := <-cronTicker:
|
||||
execucteJobs(t, currentConfig.Jobs)
|
||||
execucteJobs(t, currentConfig)
|
||||
case <-configTicker:
|
||||
log.Debug().Msg("Reload Config")
|
||||
currentConfig, err = ReadFromFile("config.yml")
|
||||
|
|
@ -39,8 +39,8 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
func execucteJobs(t time.Time, jobs []jobconfig) {
|
||||
for _, job := range jobs {
|
||||
func execucteJobs(t time.Time, c config) {
|
||||
for _, job := range c.Jobs {
|
||||
execute, err := job.MatchCurrentTime(t)
|
||||
log.Debug().Bool("execution", execute).Time("t", t).Msg("check cron execution")
|
||||
if err != nil {
|
||||
|
|
@ -50,34 +50,74 @@ func execucteJobs(t time.Time, jobs []jobconfig) {
|
|||
continue
|
||||
}
|
||||
|
||||
go executeCommand(job)
|
||||
go executeJob(t, job, c)
|
||||
}
|
||||
}
|
||||
|
||||
func executeCommand(job jobconfig) error {
|
||||
l := log.With().Str("name", job.Name).Str("command", job.Command).Logger()
|
||||
func executeJob(t time.Time, job jobconfig, c config) {
|
||||
result, err := executeCommand(job)
|
||||
log.Info().Str("name", job.Name).Str("command", job.Command).Time("execute-for", t).Int("execCode", result.execCode).Str("output", result.outputString).Err(result.err).Msg("Done execute task")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Error while Execute command")
|
||||
}
|
||||
nc := notificationConfig{}
|
||||
for _, n := range c.Notification {
|
||||
if n.Name == job.Notification {
|
||||
nc = n
|
||||
}
|
||||
}
|
||||
sendNotification(result, nc)
|
||||
}
|
||||
|
||||
func sendNotification(result jobResult, notification notificationConfig) error {
|
||||
title := ""
|
||||
msg := Message{}
|
||||
msg.Priority = 10
|
||||
|
||||
if result.isSuccess() {
|
||||
title = "✅ " + result.job.Name
|
||||
msg.Priority = -1
|
||||
} else {
|
||||
title = "❌ " + result.job.Name
|
||||
}
|
||||
|
||||
msg.Message = result.outputString
|
||||
msg.Title = title
|
||||
|
||||
if result.isSuccess() {
|
||||
if notification.Success.Gotify.URL != "" {
|
||||
SendGotifyNotification(notification.Success.Gotify.URL, msg)
|
||||
}
|
||||
} else {
|
||||
if notification.Error.Gotify.URL != "" {
|
||||
SendGotifyNotification(notification.Error.Gotify.URL, msg)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func executeCommand(job jobconfig) (jobResult, error) {
|
||||
j := jobResult{}
|
||||
j.job = job
|
||||
var output bytes.Buffer
|
||||
cmd := exec.Command("bash", "-c", job.Command)
|
||||
cmd.Stdout = &output
|
||||
cmd.Stderr = &output
|
||||
l.Debug().Msg("Start Execution")
|
||||
err := cmd.Run()
|
||||
|
||||
outString := output.String()
|
||||
j.outputString = output.String()
|
||||
|
||||
exitCode := 0
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
||||
exitCode = status.ExitStatus()
|
||||
j.execCode = status.ExitStatus()
|
||||
}
|
||||
} else {
|
||||
l.Error().Err(err).Str("output", outString).Msg("Faild Execution")
|
||||
return err
|
||||
j.err = err
|
||||
return j, err
|
||||
}
|
||||
}
|
||||
|
||||
l.Debug().Err(err).Str("output", outString).Int("exitcode", exitCode).Msg("Success Execution")
|
||||
|
||||
return nil
|
||||
return j, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue