Skip to content

Commit

Permalink
feat: support shell notification (megaease#130)
Browse files Browse the repository at this point in the history
* suuport shell notification

* add the EASEPROBE_ prefix for all of env vairables

* correct the unit test case

* better way to set the env variables

* update the README.md and the example script

* go fmt

* add the csv title

* rename the csv head

* fix the sla cvs head

* go fmt

* fix the cvs format issue

* using the use encoding/csv to generate the csv format
  • Loading branch information
haoel committed Jun 8, 2022
1 parent 6f566db commit 01c6138
Show file tree
Hide file tree
Showing 15 changed files with 313 additions and 33 deletions.
32 changes: 29 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,15 @@ Ease Probe supports the following notifications:
- **Slack**. Using Webhook for notification
- **Discord**. Using Webhook for notification
- **Telegram**. Using Telegram Bot for notification
- **Teams**. Support the [Microsoft Teams](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#setting-up-a-custom-incoming-webhook) notification.
- **Email**. Support multiple email addresses.
- **AWS SNS**. Support AWS Simple Notification Service.
- **WeChat Work**. Support Enterprise WeChat Work notification.
- **DingTalk**. Support the DingTalk notification.
- **Lark**. Support the Lark(Feishu) notification.
- **Log**. Write the notification into a log file or syslog.
- **SMS**. Support SMS notification with multiple SMS service providers - [Twilio](https://www.twilio.com/sms), [Vonage(Nexmo)](https://developer.vonage.com/messaging/sms/overview), [YunPain](https://www.yunpian.com/doc/en/domestic/list.html)
- **Teams**. Support the [Microsoft Teams](https://docs.microsoft.com/en-us/microsoftteams/platform/webhooks-and-connectors/how-to/connectors-using?tabs=cURL#setting-up-a-custom-incoming-webhook) notification.
- **Log**. Write the notification into a log file or syslog.
- **Shell**. Run a shell command to notify the result. (see [example](resources/scripts/notify/notify.sh))

> **Note**:
>
Expand Down Expand Up @@ -212,6 +213,13 @@ notify:
teams:
- name: "teams alert service"
webhook: "https://outlook.office365.com/webhook/a1269812-6d10-44b1-abc5-b84f93580ba0@9e7b80c7-d1eb-4b52-8582-76f921e416d9/IncomingWebhook/3fdd6767bae44ac58e5995547d66a4e4/f332c8d9-3397-4ac5-957b-b8e3fc465a8c" # see https://docs.microsoft.com/en-us/outlook/actionable-messages/send-via-connectors
shell: # EaseProbe set the environment variables -
# (see the example: resources/scripts/notify/notify.sh)
- name: "shell alert service"
command: "/bin/bash"
args:
- "-c"
- "/path/to/script.sh"
```

Check the [Notification Configuration](#38-notification-configuration) to see how to configure it.
Expand Down Expand Up @@ -777,6 +785,24 @@ notify:
mobile: 123456789,987654321 # mobile phone number, multi phone number joint by `,`
sign: "xxxxx" # get this from yunpian

# EaseProbe set the following environment variables
# - TYPE: "Status" or "SLA"
# - NAME: probe name
# - STATUS: "up" or "down"
# - RTT: round trip time in milliseconds
# - TIMESTAMP: timestamp of probe time
# - MESSAGE: probe message
# and offer two formats of string
# - JSON: the JSON format
# - CSV: the CSV format
# The CVS format would be set for STDIN for the shell command.
# (see the example: resources/scripts/notify/notify.sh)
shell:
- name: "shell alert service"
command: "/bin/bash"
args:
- "-c"
- "/path/to/script.sh"
```

**Note**: All of the notifications can have the following optional configuration.
Expand All @@ -797,7 +823,7 @@ notify:
settings:

# The customized name and icon
name: "Easeprobe" # the name of the probe: default: "EaseProbe"
name: "EaseProbe" # the name of the probe: default: "EaseProbe"
icon: "https://path/to/icon.png" # the icon of the probe. default: "https://megaease.com/favicon.png"
# Daemon settings

Expand Down
9 changes: 9 additions & 0 deletions global/global.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,3 +214,12 @@ func MakeDirectory(filename string) string {

return filepath.Join(dir, file)
}

// CommandLine will return the whole command line which includes command and all arguments
func CommandLine(cmd string, args []string) string {
result := cmd
for _, arg := range args {
result += " " + arg
}
return result
}
8 changes: 8 additions & 0 deletions global/global_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -317,3 +317,11 @@ func TestMakeDirectoryFail(t *testing.T) {
monkey.Unpatch(os.MkdirAll)

}

func TestCommandLine(t *testing.T) {
s := CommandLine("echo", []string{"hello", "world"})
assert.Equal(t, "echo hello world", s)

s = CommandLine("kubectl", []string{"get", "pod", "--all-namespaces", "-o", "json"})
assert.Equal(t, "kubectl get pod --all-namespaces -o json", s)
}
2 changes: 2 additions & 0 deletions notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/megaease/easeprobe/notify/email"
"github.com/megaease/easeprobe/notify/lark"
"github.com/megaease/easeprobe/notify/log"
"github.com/megaease/easeprobe/notify/shell"
"github.com/megaease/easeprobe/notify/slack"
"github.com/megaease/easeprobe/notify/sms"
"github.com/megaease/easeprobe/notify/teams"
Expand All @@ -46,6 +47,7 @@ type Config struct {
Lark []lark.NotifyConfig `yaml:"lark"`
Sms []sms.NotifyConfig `yaml:"sms"`
Teams []teams.NotifyConfig `yaml:"teams"`
Shell []shell.NotifyConfig `yaml:"shell"`
}

// Notify is the configuration of the Notify
Expand Down
79 changes: 79 additions & 0 deletions notify/shell/shell.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (c) 2022, MegaEase
* All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http:https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package shell

import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"

"github.com/megaease/easeprobe/global"
"github.com/megaease/easeprobe/notify/base"
"github.com/megaease/easeprobe/report"

log "github.com/sirupsen/logrus"
)

// NotifyConfig is the config for shell notify
type NotifyConfig struct {
base.DefaultNotify `yaml:",inline"`

Cmd string `yaml:"cmd"`
Args []string `yaml:"args"`
}

// Config is the config for shell probe
func (c *NotifyConfig) Config(gConf global.NotifySettings) error {
c.NotifyKind = "shell"
c.NotifyFormat = report.Shell
c.NotifySendFunc = c.RunShell
c.DefaultNotify.Config(gConf)

return nil
}

// RunShell is the shell for shell notify
func (c *NotifyConfig) RunShell(title, msg string) error {
ctx, cancel := context.WithTimeout(context.Background(), c.Timeout)
defer cancel()

var envMap map[string]string
err := json.Unmarshal([]byte(msg), &envMap)
if err != nil {
return err
}

cmd := exec.CommandContext(ctx, c.Cmd, c.Args...)
var env []string
for k, v := range envMap {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
cmd.Stdin = strings.NewReader(envMap["EASEPROBE_CSV"])
cmd.Env = append(os.Environ(), env...)
output, err := cmd.CombinedOutput()
if err != nil {
return err
}
log.Debugf("[%s / %s] - %s", c.NotifyKind, c.NotifyName, global.CommandLine(c.Cmd, c.Args))
log.Debugf("input: \n%s", msg)
log.Debugf("output:\n%s", string(output))
return nil
}
9 changes: 0 additions & 9 deletions probe/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,6 @@ import (
"strings"
)

// CommandLine will return the whole command line which includes command and all arguments
func CommandLine(cmd string, args []string) string {
result := cmd
for _, arg := range args {
result += " " + arg
}
return result
}

// CheckOutput checks the output text,
// - if it contains a configured string then return nil
// - if it does not contain a configured string then return nil
Expand Down
8 changes: 0 additions & 8 deletions probe/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,6 @@ import (
"github.com/stretchr/testify/assert"
)

func TestCommandLine(t *testing.T) {
s := CommandLine("echo", []string{"hello", "world"})
assert.Equal(t, "echo hello world", s)

s = CommandLine("kubectl", []string{"get", "pod", "--all-namespaces", "-o", "json"})
assert.Equal(t, "kubectl get pod --all-namespaces -o json", s)
}

func TestCheckOutput(t *testing.T) {
err := CheckOutput("hello", "good", "easeprobe hello world")
assert.Nil(t, err)
Expand Down
2 changes: 1 addition & 1 deletion probe/host/host.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (s *Server) DoProbe() (bool, string) {
return false, err.Error() + " - " + output
}

log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, probe.CommandLine(s.Command, s.Args))
log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, global.CommandLine(s.Command, s.Args))
log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, probe.CheckEmpty(string(output)))

info, err := s.ParseHostInfo(string(output))
Expand Down
11 changes: 3 additions & 8 deletions probe/shell/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"fmt"
"os"
"os/exec"
"strings"

"github.com/megaease/easeprobe/global"
"github.com/megaease/easeprobe/probe"
Expand Down Expand Up @@ -52,7 +51,7 @@ func (s *Shell) Config(gConf global.ProbeSettings) error {
tag := ""
name := s.ProbeName
s.DefaultProbe.Config(gConf, kind, tag, name,
probe.CommandLine(s.Command, s.Args), s.DoProbe)
global.CommandLine(s.Command, s.Args), s.DoProbe)

s.metrics = newMetrics(kind, tag)

Expand All @@ -66,12 +65,8 @@ func (s *Shell) DoProbe() (bool, string) {
ctx, cancel := context.WithTimeout(context.Background(), s.ProbeTimeout)
defer cancel()

for _, e := range s.Env {
v := strings.Split(e, "=")
os.Setenv(v[0], v[1])
}

cmd := exec.CommandContext(ctx, s.Command, s.Args...)
cmd.Env = append(os.Environ(), s.Env...)
output, err := cmd.CombinedOutput()

status := true
Expand All @@ -91,7 +86,7 @@ func (s *Shell) DoProbe() (bool, string) {
log.Errorf(message)
status = false
}
log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, probe.CommandLine(s.Command, s.Args))
log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, global.CommandLine(s.Command, s.Args))
log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, probe.CheckEmpty(string(output)))

s.ExportMetrics()
Expand Down
6 changes: 3 additions & 3 deletions probe/ssh/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ func (s *Server) Config(gConf global.ProbeSettings) error {
kind := "ssh"
tag := ""
name := s.ProbeName
endpoint := probe.CommandLine(s.Command, s.Args)
endpoint := global.CommandLine(s.Command, s.Args)

s.metrics = newMetrics(kind, tag)

Expand Down Expand Up @@ -148,7 +148,7 @@ func (s *Server) DoProbe() (bool, string) {
}
}

log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, probe.CommandLine(s.Command, s.Args))
log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, global.CommandLine(s.Command, s.Args))
log.Debugf("[%s / %s] - %s", s.ProbeKind, s.ProbeName, probe.CheckEmpty(string(output)))

s.ExportMetrics()
Expand Down Expand Up @@ -246,7 +246,7 @@ func (s *Server) RunSSHCmd() (string, error) {
var stdoutBuf, stderrBuf bytes.Buffer
session.Stdout = &stdoutBuf
session.Stderr = &stderrBuf
if err := session.Run(env + probe.CommandLine(s.Command, s.Args)); err != nil {
if err := session.Run(env + global.CommandLine(s.Command, s.Args)); err != nil {
return stderrBuf.String(), err
}

Expand Down
51 changes: 51 additions & 0 deletions report/result.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@
package report

import (
"bytes"
"encoding/csv"
"encoding/json"
"fmt"
"time"

"github.com/megaease/easeprobe/global"
"github.com/megaease/easeprobe/probe"

log "github.com/sirupsen/logrus"
)

Expand Down Expand Up @@ -253,3 +256,51 @@ func ToLark(r probe.Result) string {
footer := global.FooterString() + " probed at " + r.StartTime.Format(r.TimeFormat)
return fmt.Sprintf(json, headerColor, title, content, footer)
}

// ToCSV convert the object to CSV
func ToCSV(r probe.Result) string {
rtt := fmt.Sprintf("%d", r.RoundTripTime.Round(time.Millisecond))
time := r.StartTime.UTC().Format(r.TimeFormat)
timestamp := fmt.Sprintf("%d", r.StartTimestamp)
data := [][]string{
{"Title", "Name", "Endpoint", "Status", "PreStatus", "RoundTripTime", "Time", "Timestamp", "Message"},
{r.Title(), r.Name, r.Endpoint, r.Status.String(), r.PreStatus.String(), rtt, time, timestamp, r.Message},
}

buf := new(bytes.Buffer)
w := csv.NewWriter(buf)

if err := w.WriteAll(data); err != nil {
log.Errorf("ToCSV(): Failed to write to csv buffer: %v", err)
return ""
}
return buf.String()
}

// ToShell convert the result object to shell variables
func ToShell(r probe.Result) string {
env := make(map[string]string)

// set the notify type variable
env["EASEPROBE_TYPE"] = "Status"

// set individual variables
env["EASEPROBE_TITLE"] = r.Title()
env["EASEPROBE_NAME"] = r.Name
env["EASEPROBE_ENDPOINT"] = r.Endpoint
env["EASEPROBE_STATUS"] = r.Status.String()
env["EASEPROBE_TIMESTAMP"] = fmt.Sprintf("%d", r.StartTimestamp)
env["EASEPROBE_RTT"] = fmt.Sprintf("%d", r.RoundTripTime.Round(time.Millisecond))
env["EASEPROBE_MESSAGE"] = r.Message

// set JSON and CVS format
env["EASEPROBE_JSON"] = ToJSON(r)
env["EASEPROBE_CSV"] = ToCSV(r)

buf, err := json.Marshal(env)
if err != nil {
log.Errorf("ToShell(): Failed to marshal env to json: %s", err)
return ""
}
return string(buf)
}
Loading

0 comments on commit 01c6138

Please sign in to comment.