diff --git a/README.md b/README.md index f853417..7318ebe 100644 --- a/README.md +++ b/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" -``` - +``` \ No newline at end of file diff --git a/config.go b/config.go index 35eb8c4..9627042 100644 --- a/config.go +++ b/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) { diff --git a/gotify.go b/gotify.go new file mode 100644 index 0000000..f20fc37 --- /dev/null +++ b/gotify.go @@ -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 +} diff --git a/jobResult.go b/jobResult.go new file mode 100644 index 0000000..63de95d --- /dev/null +++ b/jobResult.go @@ -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 +} diff --git a/main.go b/main.go index afa4da4..79f6a5e 100644 --- a/main.go +++ b/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 }