feat: gotify notification
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
kekskurse 2025-07-31 18:43:03 +02:00
parent 660fc317ab
commit 01e283f96b
5 changed files with 173 additions and 28 deletions

View file

@ -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
@ -99,4 +128,3 @@ jobs:
hour: "3"
command: "cleanup.sh"
```

View file

@ -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
View 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
View 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
View file

@ -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
}