Skip to content

Commit f4d97c5

Browse files
committed
initial version of monitoring
1 parent 56f9e51 commit f4d97c5

File tree

11 files changed

+324
-51
lines changed

11 files changed

+324
-51
lines changed

cmd/server/cmd.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
package main
22

3-
import "github.com/spf13/cobra"
3+
import (
4+
"os"
5+
6+
log "github.com/sirupsen/logrus"
7+
"github.com/spf13/cobra"
8+
)
49

510
var (
611
rootCmd = &cobra.Command{
@@ -10,6 +15,11 @@ var (
1015
)
1116

1217
func init() {
18+
if os.Getenv("DEBUG") != "" {
19+
log.Info("debug level set")
20+
log.SetLevel(log.DebugLevel)
21+
}
22+
1323
rootCmd.PersistentFlags().StringP("config", "c", "traffikey.json", "json configuration filename")
1424
}
1525

cmd/server/cmd_monitor.go

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package main
22

33
import (
4+
"context"
45
"fmt"
6+
"os"
7+
"os/signal"
8+
"time"
59

610
"github.com/labstack/echo/v4"
711
logrusmiddleware "github.com/numkem/echo-logrusmiddleware"
@@ -12,7 +16,7 @@ import (
1216
var monitorCmd = &cobra.Command{
1317
Use: "monitor",
1418
Short: "monitor configured endpoints",
15-
Long: "monitor configured endpoints. If one become unreachable, it will redirect to itself and show an error message explaining the target is not available.",
19+
Long: "monitor configured endpoints. If one become unreachable, it will redirect to itself and show an error message",
1620
Run: monitorCmdRun,
1721
}
1822

@@ -24,12 +28,36 @@ func init() {
2428
}
2529

2630
func monitorCmdRun(cmd *cobra.Command, args []string) {
31+
configFilename := cmd.Flag("config").Value.String()
32+
mon, err := NewMonitor(configFilename)
33+
if err != nil {
34+
log.Fatalf("failed to read configuration: %v", err)
35+
}
36+
37+
h := &handler{monitor: mon}
38+
39+
// echo init
2740
e := echo.New()
41+
e.HideBanner = true
2842
e.Logger = logrusmiddleware.Logger{Logger: log.StandardLogger()}
2943

30-
e.GET("/", func(c echo.Context) error {
31-
return nil
32-
})
44+
e.GET("/", h.List)
45+
46+
go func() {
47+
mon.Start()
48+
e.Logger.Fatal(e.Start(fmt.Sprintf("%s", cmd.Flag("bind").Value)))
49+
}()
50+
51+
// Listen for signal and quit
52+
sig := make(chan os.Signal, 1)
53+
signal.Notify(sig, os.Interrupt)
54+
55+
select {
56+
case <-sig:
57+
ctx, cancel := context.WithTimeout(cmd.Context(), 2*time.Second)
3358

34-
e.Logger.Fatal(e.Start(fmt.Sprintf("%s", cmd.Flag("bind").Value)))
59+
mon.Stop()
60+
e.Shutdown(ctx)
61+
cancel()
62+
}
3563
}

cmd/server/handler.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package main
2+
3+
import (
4+
"github.com/labstack/echo/v4"
5+
)
6+
7+
type handler struct {
8+
monitor *Monitor
9+
}
10+
11+
func (h *handler) List(c echo.Context) error {
12+
return c.JSON(200, nil)
13+
}

cmd/server/monitor.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net/http"
7+
"sync"
8+
"time"
9+
10+
"github.com/gofrs/uuid"
11+
traffikey "github.com/numkem/traffikey"
12+
"github.com/numkem/traffikey/keymate"
13+
14+
log "github.com/sirupsen/logrus"
15+
)
16+
17+
type Monitor struct {
18+
currentTargets *sync.Map
19+
manager keymate.KeymateConnector
20+
cfg *traffikey.Config
21+
}
22+
23+
func NewMonitor(configFilename string) (*Monitor, error) {
24+
// Read configuration file
25+
cfg, err := traffikey.NewConfig(configFilename)
26+
if err != nil {
27+
return nil, fmt.Errorf("Failed to read configuraiton: %v", err)
28+
}
29+
30+
// Check if defaults are set
31+
if cfg.Traefik.DefaultPrefix == "" {
32+
log.Warn("default traefik prefix isn't set, this could cause errors")
33+
}
34+
35+
if cfg.Traefik.DefaultEntrypoint == "" {
36+
log.Warn("default Traefik entrypoint isn't set, this could cause errors")
37+
}
38+
39+
// Create manager connection
40+
mgr, err := keymate.NewEtcdManager(cfg)
41+
if err != nil {
42+
log.Fatalf("failed to create manager: %v", err)
43+
}
44+
45+
return &Monitor{cfg: cfg, currentTargets: &sync.Map{}, manager: mgr}, nil
46+
}
47+
48+
func testTarget(tgt *traffikey.Target) []string {
49+
// Attempt a test connection to the target
50+
switch tgt.Type {
51+
case "http":
52+
return testHTTPTarget(tgt)
53+
default:
54+
log.WithField("target", tgt.Name).Warnf("invalid target type %s", tgt.Type)
55+
}
56+
57+
return []string{}
58+
}
59+
60+
func testHTTPTarget(tgt *traffikey.Target) []string {
61+
// Try to connect to the http endpoint with a very small timeout
62+
aliveUrls := []string{}
63+
for _, url := range tgt.ServerURLs {
64+
client := http.Client{
65+
Timeout: 1 * time.Second,
66+
}
67+
68+
_, err := client.Get(url)
69+
// TODO: Check the HTTP response code
70+
if err == nil {
71+
aliveUrls = append(aliveUrls, url)
72+
}
73+
}
74+
75+
return aliveUrls
76+
}
77+
78+
type monitoredTarget struct {
79+
ID string
80+
Context context.Context
81+
Cancel context.CancelFunc
82+
Target *traffikey.Target
83+
}
84+
85+
func (m *Monitor) Start() {
86+
// Start a monitor goroutine for each target
87+
for _, tgt := range m.cfg.Targets {
88+
if !tgt.Monitored {
89+
log.WithField("target", tgt.Name).Infof("target %s isn't monitored", tgt.Name)
90+
continue
91+
}
92+
93+
id, err := uuid.NewV4()
94+
if err != nil {
95+
log.WithField("target", tgt.Name).Errorf("failed to generate UUID: %v", err)
96+
}
97+
98+
ctx, cancel := context.WithCancel(context.Background())
99+
mt := &monitoredTarget{
100+
ID: id.String(),
101+
Context: ctx,
102+
Cancel: cancel,
103+
Target: tgt,
104+
}
105+
m.currentTargets.Store(id, mt)
106+
107+
log.WithField("target", tgt.Name).Debug("starting monitoring of target")
108+
go watchTarget(mt)
109+
}
110+
}
111+
112+
func (m *Monitor) Stop() {
113+
m.currentTargets.Range(func(key, value interface{}) bool {
114+
mt := value.(*monitoredTarget)
115+
116+
log.Debugf("Stopping monitoring of target ID %s :: %s", mt.ID, mt.Target.Name)
117+
mt.Cancel()
118+
return true
119+
})
120+
}
121+
122+
func watchTarget(m *monitoredTarget) {
123+
for {
124+
ticker := time.NewTicker(15 * time.Second)
125+
126+
aliveUrls := testTarget(m.Target)
127+
switch len(aliveUrls) {
128+
case 0:
129+
log.WithField("target", m.Target.Name).Infof("Target is DOWN (0/%d)", len(m.Target.ServerURLs))
130+
default:
131+
upNb := len(m.Target.ServerURLs) - len(aliveUrls)
132+
log.WithField("target", m.Target.Name).Infof("Target is UP (%d/%d)", upNb, len(m.Target.ServerURLs))
133+
}
134+
135+
select {
136+
case <-m.Context.Done():
137+
return
138+
139+
case <-ticker.C:
140+
}
141+
}
142+
}

cmd/server/monitor_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"github.com/numkem/traffikey"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestHTTPTarget(t *testing.T) {
11+
aliveUrls := testHTTPTarget(&traffikey.Target{
12+
Name: "google.com",
13+
Type: "http",
14+
ServerURLs: []string{
15+
"https://google.com",
16+
},
17+
})
18+
19+
assert.NotEmpty(t, aliveUrls, "testHTTPTarget should return one url")
20+
}
21+
22+
func TestHTTPTargetFailed(t *testing.T) {
23+
aliveUrls := testHTTPTarget(&traffikey.Target{
24+
Name: "failed.com",
25+
Type: "http",
26+
ServerURLs: []string{
27+
"http://falksjdfalskjfaldkjf.com",
28+
},
29+
})
30+
31+
assert.Empty(t, aliveUrls, "testHTTPTarget should not return any url")
32+
}

config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"encoding/json"
55
"fmt"
66
"os"
7+
8+
log "github.com/sirupsen/logrus"
79
)
810

911
type Config struct {
@@ -23,6 +25,8 @@ type traefikConfig struct {
2325
}
2426

2527
func NewConfig(filename string) (*Config, error) {
28+
log.WithField("filename", filename).Debugf("reading configuration file")
29+
2630
f, err := os.Open(filename)
2731
if err != nil {
2832
if os.IsNotExist(err) {

dev/example.json

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
{
22
"etcd": {
3-
"endpoints": [
4-
"http://172.31.99.4:2379",
5-
"http://172.31.99.5:2379",
6-
"http://172.31.99.3:2769"
7-
]
3+
"endpoints": ["http://127.0.0.1:2379"]
84
},
95
"targets": [
106
{
11-
"name": "test",
12-
"urls": ["http://192.168.0.104:8080"],
7+
"name": "forge",
8+
"urls": ["http://192.168.0.7:8080"],
139
"entrypoint": "web",
1410
"prefix": "traefik",
15-
"rule": "test.numkem.org"
11+
"rule": "test.numkem.org",
12+
"type": "http",
13+
"monitored": true
14+
},
15+
{
16+
"name": "pylon",
17+
"urls": ["http://192.168.0.2:8080"],
18+
"entrypoint": "web",
19+
"prefix": "traefik",
20+
"rule": "test.numkem.org",
21+
"type": "http",
22+
"monitored": true
1623
}
1724
],
1825
"traefik": {
19-
"default_entrypoint": "web"
26+
"default_entrypoint": "web",
27+
"default_prefix": "traefik"
2028
}
2129
}

flake.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)