Skip to content

Commit

Permalink
feat: supported probe plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
macrat committed May 1, 2021
1 parent d420081 commit 428a376
Show file tree
Hide file tree
Showing 11 changed files with 187 additions and 16 deletions.
48 changes: 44 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,27 @@ Easiest status monitoring service to check something service is dead or alive.
* [TCP connect](#tcp)
* [DNS resolve](#dns)
* [execute external command (or script file)](#exec)
* [plugin](#plugin)
- [view status page in browser, console, or program.](#status-page-and-endpoints)
- [kick alert if target failure.](#alerting)

### Good at
- Make a status page for temporary usage. (You can start it via one command! And, stop via just Ctrl-C!)
- Make a status page for a minimal system. (Single binary server, single log file, there is no database!)
- Make a status page for temporary usage.

You can start it via one command! And, stop via just Ctrl-C!

- Make a status page for a minimal system.

Single binary server, single log file, there is no database!

### Not good at
- Complex customization, extension. (There are nothing options for customizing.)
- Investigate more detail. (This is just for check dead or alive.)
- Complex customization, extension.

There is a few extension way, but extensibility is not the goal of this project.

- Investigate more detail.

This is just for check dead or alive.


## Quick start
Expand Down Expand Up @@ -170,6 +181,35 @@ This output is reporting latency is `123.456ms`, status is `FAILURE`, and messag

Ayd uses the last value if found multiple reports in single output.

#### plugin

Plugin for check target is almost the same as [`exec:` target](#exec).
The differences are below.

| |`exec: ` |plugin |
|--------------------------------------------|------------|--------------------------|
|scheme of URI |`exec:` only|anything |
|executable file place |anywhere |only in the PATH directory|
|set argument and environment variable in URI|can |can not |
|receive raw target URI |can not |can |

Plugin is the "plugin".
This is a good way to extend Ayd (you can use any URI!), but not good at writing a short script (you have to parse URI yourself).

Plugin is an executable file in the PATH directory.
Ayd looks for `ayd-XXX-probe` if found target with `XXX:` scheme.

You can't use URI schemes that `ayd`, `alert`, and the scheme that is supported by Ayd itself.

Plugin receives these values as the environment variable.

|variable name|example |description |
|-------------|-----------------------|--------------------------|
|`ayd_url` |`http://localhost:9000`|The URL of Ayd |
|`ayd_target` |`foobar:hello-world` |The target URI to checking|

You can use [the directives the same as exec](#extra-report-output-for-exec) in output of plugin.

#### source

This is a special scheme for load targets from a file.
Expand Down
1 change: 1 addition & 0 deletions help.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ OPTIONS:
-a TARGET The alert URI that the same format as target URI.
-o FILE Path to log file. Log file is also use for restore status history. (default "./ayd.log")
-p PORT Listen port of status page. (default 9000)
-u URL The external URL like "http://example.com:9000".

INTERVALS:
Specify execution schedule in interval (e.g. "2m" means "every 2 minutes")
Expand Down
13 changes: 11 additions & 2 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ var (
storePath = flag.String("o", "./ayd.log", "Path to log file. Log file is also use for restore status history.")
oneshot = flag.Bool("1", false, "Check status only once and exit. Exit with 0 if all check passed, otherwise exit with code 1.")
alertURI = flag.String("a", "", "The alert URI that the same format as target URI.")
externalURL = flag.String("u", "", "The external URL like \"http://example.com:9000\".")
showVersion = flag.Bool("v", false, "Show version and exit.")
)

Expand All @@ -45,7 +46,15 @@ func Usage() {
})
}

func StartProbeServer(ctx context.Context, tasks []Task) {
func SetupProbe(ctx context.Context, tasks []Task) {
if *externalURL != "" {
probe.ExternalURL = *externalURL
} else if *listenPort == 80 {
probe.ExternalURL = "http://localhost"
} else {
probe.ExternalURL = fmt.Sprintf("http://localhost:%d", listenPort)
}

for _, task := range tasks {
if task.Probe.Target().Scheme == "ping" {
if err := probe.StartPinger(ctx); err != nil {
Expand Down Expand Up @@ -101,7 +110,7 @@ func main() {
})
}

StartProbeServer(ctx, tasks)
SetupProbe(ctx, tasks)

if *oneshot {
os.Exit(RunOneshot(ctx, s, tasks))
Expand Down
22 changes: 13 additions & 9 deletions probe/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,15 @@ func getStatusByMessage(message string, default_ store.Status) (replacedMessage
return message, default_
}

func (p ExecuteProbe) Check(ctx context.Context, r Reporter) {
ctx, cancel := context.WithTimeout(ctx, 60*time.Minute)
defer cancel()

func executeExternalCommand(ctx context.Context, r Reporter, target *url.URL, command, argument string, env []string) {
var cmd *exec.Cmd
if p.target.Fragment != "" {
cmd = exec.CommandContext(ctx, p.target.Opaque, p.target.Fragment)
if argument != "" {
cmd = exec.CommandContext(ctx, command, argument)
} else {
cmd = exec.CommandContext(ctx, p.target.Opaque)
cmd = exec.CommandContext(ctx, command)
}

cmd.Env = p.env
cmd.Env = env

st := time.Now()
stdout, err := cmd.CombinedOutput()
Expand Down Expand Up @@ -107,9 +104,16 @@ func (p ExecuteProbe) Check(ctx context.Context, r Reporter) {

r.Report(timeoutOr(ctx, store.Record{
CheckedAt: st,
Target: p.target,
Target: target,
Status: status,
Message: message,
Latency: latency,
}))
}

func (p ExecuteProbe) Check(ctx context.Context, r Reporter) {
ctx, cancel := context.WithTimeout(ctx, 60*time.Minute)
defer cancel()

executeExternalCommand(ctx, r, p.target, p.target.Opaque, p.target.Fragment, p.env)
}
58 changes: 58 additions & 0 deletions probe/plugin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package probe

import (
"context"
"errors"
"fmt"
"net/url"
"os"
"os/exec"
"time"
)

var (
ExternalURL = "http://localhost:9000"
)

type PluginProbe struct {
target *url.URL
command string
env []string
}

func NewPluginProbe(u *url.URL) (PluginProbe, error) {
if u.Scheme == "ayd" || u.Scheme == "alert" {
return PluginProbe{}, ErrUnsupportedScheme
}

p := PluginProbe{
target: u,
command: "ayd-" + u.Scheme + "-probe",
env: os.Environ(),
}

if _, err := exec.LookPath(p.command); errors.Unwrap(err) == exec.ErrNotFound {
return PluginProbe{}, ErrUnsupportedScheme
} else if err != nil {
return PluginProbe{}, err
}

p.env = append(
p.env,
fmt.Sprintf("ayd_url=%s", ExternalURL),
fmt.Sprintf("ayd_target=%s", u),
)

return p, nil
}

func (p PluginProbe) Target() *url.URL {
return p.target
}

func (p PluginProbe) Check(ctx context.Context, r Reporter) {
ctx, cancel := context.WithTimeout(ctx, 60*time.Minute)
defer cancel()

executeExternalCommand(ctx, r, p.target, p.command, "", p.env)
}
42 changes: 42 additions & 0 deletions probe/plugin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package probe_test

import (
"os"
"path"
"runtime"
"testing"

"github.com/macrat/ayd/probe"
"github.com/macrat/ayd/store"
)

func TestPluginProbe(t *testing.T) {
t.Parallel()

cwd, err := os.Getwd()
if err != nil {
t.Fatalf("failed to get current path: %s", err)
}

origPath := os.Getenv("PATH")
os.Setenv("PATH", origPath+":"+path.Join(cwd, "testdata"))
t.Cleanup(func() {
os.Setenv("PATH", origPath)
})

AssertProbe(t, []ProbeTest{
{"plug:", store.STATUS_HEALTHY, "http://localhost:9000 -> plug:"},
{"plug:hello-world", store.STATUS_HEALTHY, "http://localhost:9000 -> plug:hello-world"},
})

AssertTimeout(t, "plug:")

if runtime.GOOS != "windows" {
t.Run("forbidden:", func(t *testing.T) {
_, err := probe.New("forbidden:")
if err != probe.ErrUnsupportedScheme {
t.Errorf("unexpected error: %v", err)
}
})
}
}
2 changes: 1 addition & 1 deletion probe/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ func NewFromURL(u *url.URL) (Probe, error) {
case "dummy":
return NewDummyProbe(u)
default:
return nil, ErrUnsupportedScheme
return NewPluginProbe(u)
}
}

Expand Down
7 changes: 7 additions & 0 deletions probe/probe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ func TestTargetURLNormalize(t *testing.T) {
t.Errorf("%#v expected query %#v but go %#v", tt.Input, tt.Want.RawQuery, u.RawQuery)
}
}

t.Run("unknown:target", func(t *testing.T) {
_, err := probe.New("unknown:target")
if err != probe.ErrUnsupportedScheme {
t.Errorf("unexpected error: %v", err)
}
})
}

type ProbeTest struct {
Expand Down
4 changes: 4 additions & 0 deletions probe/testdata/ayd-forbidden-probe
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

echo "::status::FAILURE"
echo "It should be failure because it has no permission"
4 changes: 4 additions & 0 deletions probe/testdata/ayd-plug-probe
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh

echo "::status::HEALTHY"
echo "${ayd_url} -> ${ayd_target}"
2 changes: 2 additions & 0 deletions probe/testdata/ayd-plug-probe.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
echo "::status::HEALTHY::"
echo "%AYD_URL% -> %AYD_TARGET%"

0 comments on commit 428a376

Please sign in to comment.