├── .dockerignore
├── .editorconfig
├── .gitignore
├── .travis.yml
├── Dockerfile
├── README.md
├── docker-compose.yml
├── main.go
├── monitor
├── data
│ ├── data.go
│ ├── db.go
│ ├── target.go
│ └── track.go
├── docker.go
├── email.go
├── entities
│ ├── event.go
│ ├── target.go
│ └── track.go
├── http.go
└── job.go
├── monitor_api_suite_test.go
└── server
├── api
├── target.go
└── track.go
├── router.go
├── router_test.go
├── server_suite_test.go
└── websocket.go
/.dockerignore:
--------------------------------------------------------------------------------
1 | .git
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 | indent_style = space
9 | indent_size = 4
10 |
11 | [*.go]
12 | indent_style = tab
13 | indent_size = 8
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | gin-bin
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 | go:
3 | - 1.4
4 | env:
5 | global:
6 | - MONGO_PORT=tcp://127.0.0.1:27017
7 | - MONGODB_DATABASE=go-uptime-api
8 | - VIRTUAL_PORT=3000
9 | - VIRTUAL_HOST=fake-domain.com
10 | - CHECK_TARGETS_AT_EVERY=1d
11 | services:
12 | - mongodb
13 | install:
14 | - go get -d -v ./...
15 | - go get -v github.com/onsi/ginkgo/ginkgo
16 | - go get -v github.com/onsi/gomega
17 | script:
18 | - ginkgo -r -v --randomizeAllSpecs --randomizeSuites --failOnPending --trace --race --compilers=2
19 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:1.4
2 |
3 | WORKDIR /go/src/github.com/maxcnunes/go-uptime-api
4 |
5 | ADD . /go/src/github.com/maxcnunes/go-uptime-api
6 |
7 | RUN go get -d -v ./...
8 |
9 | # dev environment
10 | RUN go get github.com/codegangsta/gin \
11 | github.com/onsi/ginkgo/ginkgo \
12 | github.com/onsi/gomega
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # go-uptime
2 |
3 | [](https://travis-ci.org/maxcnunes/go-uptime-api)
4 |
5 | Simple monitor server to check uptime of any target reachable through HTTP.
6 |
7 | The Go Uptime is composed of an API and [APP](https://github.com/maxcnunes/go-uptime-app) separated in different projects.
8 |
9 | ## API
10 |
11 | This current project is responsible for manipulating the targets data and polling all targets' URL to check if each one is up or down. Also the monitor will listen to all Docker events and capture the URL from all containers that has the `VIRTUAL_HOST` environment variable.
12 | Concerned in a better user experience the monitor uses web socket to notify the connected clients when a target has been created or updated.
13 |
14 |
15 | # Developing
16 |
17 | The simplest way is using Dockito vagrant box and docker-compose to provide a configured environment for you.
18 |
19 | Setup the [Dockito vagrant box](https://github.com/dockito/devbox#dockito-vagrant-box) then inside the VM execute the command below to the docker-compose start the container:
20 |
21 | ```bash
22 | docker-compose run local
23 | ```
24 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | mongo:
2 | image: mongo:2.6
3 |
4 | local: &LOCAL
5 | build: .
6 | command: gin
7 | ports:
8 | - 3000
9 | volumes:
10 | - .:/go/src/github.com/maxcnunes/go-uptime-api
11 | - /var/run/docker.sock:/tmp/docker.sock
12 | environment: &LOCAL-ENVIRONMENT
13 | MONGODB_DATABASE: go-uptime-api
14 | VIRTUAL_PORT: 3000
15 | VIRTUAL_HOST: go-uptime-api.local.dockito.org
16 | PORT_BEHIND_PROXY: 3001
17 | CHECK_TARGETS_AT_EVERY: 1m # 1 minute
18 | EMAIL_FROM: # gets environment variable from machine
19 | EMAIL_USERNAME: # gets environment variable from machine
20 | EMAIL_PASSWORD: # gets environment variable from machine
21 | EMAIL_HOST: smtp.gmail.com
22 | links:
23 | - mongo:mongo
24 |
25 | test:
26 | <<: *LOCAL
27 | command: ginkgo watch -v -p -r -race -failOnPending -randomizeAllSpecs
28 | environment:
29 | <<: *LOCAL-ENVIRONMENT
30 | MONGODB_DATABASE: go-uptime-api-test
31 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "os"
7 |
8 | "github.com/maxcnunes/go-uptime-api/monitor"
9 | dm "github.com/maxcnunes/go-uptime-api/monitor/data"
10 | "github.com/maxcnunes/go-uptime-api/server"
11 | )
12 |
13 | var (
14 | db = dm.DB{}
15 | data = dm.DataMonitor{}
16 | job = monitor.Job{}
17 | router = server.Router{}
18 | websocket = server.Websocket{}
19 | )
20 |
21 | func main() {
22 | db.Start()
23 | defer db.Close()
24 |
25 | data.Start(db)
26 |
27 | http.Handle("/", router.Start(&data))
28 | http.HandleFunc("/ws", websocket.Start(&data))
29 |
30 | job.Start(&data, getTimeTargetsVerification())
31 |
32 | addr := getServiceAddress()
33 | log.Printf("Server running on http://%s", addr)
34 | if err := http.ListenAndServe(addr, nil); err != nil {
35 | log.Fatal("ListenAndServe: ", err)
36 | }
37 | }
38 |
39 | func getTimeTargetsVerification() string {
40 | if env := os.Getenv("CHECK_TARGETS_AT_EVERY"); env != "" {
41 | return env
42 | }
43 |
44 | return "10m"
45 | }
46 |
47 | func getServiceAddress() string {
48 | if env := os.Getenv("PORT_BEHIND_PROXY"); env != "" {
49 | return ":" + env
50 | }
51 | if env := os.Getenv("VIRTUAL_PORT"); env != "" {
52 | return ":" + env
53 | }
54 |
55 | return ":3000"
56 | }
57 |
--------------------------------------------------------------------------------
/monitor/data/data.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import "github.com/maxcnunes/go-uptime-api/monitor/entities"
4 |
5 | // DataMonitor aggregates the data configuration
6 | // like the database configuration and connections to MongoDB collections
7 | type DataMonitor struct {
8 | DB DB
9 | Events chan entities.Event
10 | Target *DataTarget
11 | Track *DataTrack
12 | }
13 |
14 | // Start ...
15 | func (d *DataMonitor) Start(db DB) {
16 | d.DB = db
17 | d.Events = make(chan entities.Event)
18 | d.Target = &DataTarget{}
19 | d.Track = &DataTrack{}
20 |
21 | d.Target.Start(db, d.Events)
22 | d.Track.Start(db, d.Events)
23 | }
24 |
--------------------------------------------------------------------------------
/monitor/data/db.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import (
4 | "log"
5 | "net"
6 | "net/url"
7 | "os"
8 |
9 | "gopkg.in/mgo.v2"
10 | )
11 |
12 | // DB has the database configuration
13 | type DB struct {
14 | connectionURI string
15 | DBName string
16 | Session *mgo.Session
17 | }
18 |
19 | func (db *DB) setConnectionURIFromEnvConfig() {
20 | errMsg := "no connection string provided"
21 |
22 | mongoConn, err := url.Parse(os.Getenv("MONGO_PORT"))
23 | if err != nil {
24 | log.Fatalln(errMsg)
25 | }
26 |
27 | dbHost, dbPort, err := net.SplitHostPort(mongoConn.Host)
28 | if err != nil {
29 | log.Fatalln(errMsg)
30 | }
31 |
32 | db.DBName = os.Getenv("MONGODB_DATABASE")
33 | if db.DBName == "" || dbHost == "" || dbPort == "" {
34 | log.Fatalln(errMsg)
35 | }
36 |
37 | db.connectionURI = "mongodb://" + dbHost + ":" + dbPort + "/" + db.DBName
38 | }
39 |
40 | // Start a new instance of database
41 | func (db *DB) Start() {
42 | var err error
43 | db.setConnectionURIFromEnvConfig()
44 |
45 | db.Session, err = mgo.Dial(db.connectionURI)
46 | if err != nil {
47 | log.Fatalf("Can't connect to mongo, go error %v\n", err)
48 | }
49 |
50 | db.Session.SetSafe(&mgo.Safe{})
51 | db.Session.SetMode(mgo.Monotonic, true)
52 | }
53 |
54 | // Close the current database session
55 | func (db *DB) Close() {
56 | db.Session.Close()
57 | }
58 |
59 | // Wipe the whole database. Use it only in test environment.
60 | func (db *DB) Wipe() {
61 | db.Session.DB(db.DBName).C("target").RemoveAll(nil)
62 | db.Session.DB(db.DBName).C("track").RemoveAll(nil)
63 | }
64 |
--------------------------------------------------------------------------------
/monitor/data/target.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/maxcnunes/go-uptime-api/monitor/entities"
7 | "gopkg.in/mgo.v2"
8 | "gopkg.in/mgo.v2/bson"
9 | )
10 |
11 | // DataTarget is the data configuration related to Target collection
12 | type DataTarget struct {
13 | collection *mgo.Collection
14 | events chan entities.Event
15 | }
16 |
17 | // FindOneByURL ...
18 | func (d *DataTarget) FindOneByURL(url string) *entities.Target {
19 | var target entities.Target
20 | err := d.collection.Find(bson.M{"url": url}).One(&target)
21 | if err != nil {
22 | if err == mgo.ErrNotFound {
23 | return nil
24 | }
25 |
26 | log.Printf("got an error finding a doc %v\n", err)
27 | }
28 |
29 | return &target
30 | }
31 |
32 | // FindOneByID finds a single target by the id field
33 | func (d *DataTarget) FindOneByID(id string) *entities.Target {
34 | _id := bson.ObjectIdHex(id)
35 | var target entities.Target
36 |
37 | err := d.collection.Find(bson.M{"_id": _id}).One(&target)
38 | if err != nil {
39 | if err == mgo.ErrNotFound {
40 | return nil
41 | }
42 |
43 | log.Printf("got an error finding a doc %v\n", err)
44 | }
45 |
46 | return &target
47 | }
48 |
49 | // Create a new target in the database
50 | func (d *DataTarget) Create(url string, emails []string) *entities.Target {
51 | target := d.FindOneByURL(url)
52 | if target != nil {
53 | return target
54 | }
55 |
56 | log.Printf("Adding target %s", url)
57 |
58 | doc := entities.Target{ID: bson.NewObjectId(), URL: url, Status: 0, Emails: emails}
59 | if err := d.collection.Insert(doc); err != nil {
60 | log.Printf("Can't insert document: %v\n", err)
61 | }
62 |
63 | go func() { d.events <- entities.Event{Event: entities.Added, Target: &doc} }()
64 |
65 | return &doc
66 | }
67 |
68 | // Remove an existing target by the URL field
69 | func (d *DataTarget) Remove(url string) {
70 | target := d.FindOneByURL(url)
71 | if target == nil {
72 | log.Printf("Can't find document with url: %s\n", url)
73 | return
74 | }
75 |
76 | log.Printf("Removing url %s", url)
77 |
78 | err := d.collection.Remove(bson.M{"url": url})
79 | if err != nil {
80 | log.Printf("Can't delete document: %v\n", err)
81 | }
82 |
83 | go func() { d.events <- entities.Event{Event: entities.Removed, Target: target} }()
84 | }
85 |
86 | // RemoveByID removes an existing target by the ID field
87 | func (d *DataTarget) RemoveByID(id string) {
88 | target := d.FindOneByID(id)
89 | if target == nil {
90 | log.Printf("Can't find document with id: %s\n", id)
91 | return
92 | }
93 |
94 | log.Printf("Removing url %s", target.URL)
95 |
96 | err := d.collection.Remove(bson.M{"_id": target.ID})
97 | if err != nil {
98 | log.Printf("Can't delete document: %v\n", err)
99 | }
100 |
101 | go func() { d.events <- entities.Event{Event: entities.Removed, Target: target} }()
102 | }
103 |
104 | // UpdateStatusByURL updates a existing target by the URL field
105 | func (d *DataTarget) UpdateStatusByURL(url string, status string) {
106 | target := d.FindOneByURL(url)
107 | if target != nil {
108 | log.Printf("Can't find document with url: %s\n", url)
109 | return
110 | }
111 |
112 | log.Printf("Updating url %s to status %s", url, status)
113 | err := d.collection.Update(bson.M{"url": url}, bson.M{"status": status})
114 | if err != nil {
115 | log.Printf("Can't update document: %v\n", err)
116 | }
117 |
118 | go func() { d.events <- entities.Event{Event: entities.Updated, Target: target} }()
119 | }
120 |
121 | // Update an existing target
122 | func (d *DataTarget) Update(id string, data entities.Target) {
123 | target := d.FindOneByID(id)
124 | if target == nil {
125 | log.Printf("Can't find document with id: %s\n", id)
126 | return
127 | }
128 |
129 | attrs := bson.M{"url": data.URL, "emails": data.Emails}
130 | if data.Status != 0 {
131 | attrs["status"] = data.Status
132 | }
133 |
134 | log.Printf("Updating url %s to status %d", target.URL, target.Status)
135 | err := d.collection.UpdateId(target.ID, bson.M{"$set": attrs})
136 | if err != nil {
137 | log.Printf("Can't update document: %v\n", err)
138 | }
139 |
140 | go func() { d.events <- entities.Event{Event: entities.Updated, Target: target} }()
141 | }
142 |
143 | // GetAllURLS returns all existing target's URLs in the databse
144 | func (d *DataTarget) GetAllURLS() []string {
145 | urls := []string{}
146 |
147 | targets := d.GetAll()
148 |
149 | for _, target := range targets {
150 | urls = append(urls, target.URL)
151 | }
152 |
153 | return urls
154 | }
155 |
156 | // GetAll returns all targets
157 | func (d *DataTarget) GetAll() []entities.Target {
158 | targets := []entities.Target{}
159 |
160 | err := d.collection.Find(nil).All(&targets)
161 | if err != nil {
162 | log.Printf("got an error finding a doc %v\n", err)
163 | }
164 |
165 | return targets
166 | }
167 |
168 | // Start a new instance of data target
169 | func (d *DataTarget) Start(db DB, events chan entities.Event) {
170 | d.collection = db.Session.DB(db.DBName).C("target")
171 | d.events = events
172 | }
173 |
--------------------------------------------------------------------------------
/monitor/data/track.go:
--------------------------------------------------------------------------------
1 | package data
2 |
3 | import (
4 | "log"
5 | "time"
6 |
7 | "github.com/maxcnunes/go-uptime-api/monitor/entities"
8 | "gopkg.in/mgo.v2"
9 | "gopkg.in/mgo.v2/bson"
10 | )
11 |
12 | // DataTrack is the data configuration related to Track collection
13 | type DataTrack struct {
14 | collection *mgo.Collection
15 | events chan entities.Event
16 | }
17 |
18 | // Find all tracks
19 | func (d *DataTrack) Find(targetID string) []entities.Track {
20 | tracks := []entities.Track{}
21 |
22 | query := bson.M{}
23 | if targetID != "" {
24 | query["targetId"] = bson.ObjectIdHex(targetID)
25 | }
26 |
27 | err := d.collection.Find(query).Limit(50).Sort("-createdAt").All(&tracks)
28 | if err != nil {
29 | log.Printf("got an error finding a doc %v\n", err)
30 | }
31 |
32 | return tracks
33 | }
34 |
35 | // Create a new track
36 | func (d *DataTrack) Create(target entities.Target, status int) *entities.Track {
37 | doc := entities.Track{
38 | ID: bson.NewObjectId(),
39 | TargetID: target.ID,
40 | Status: status,
41 | CreatedAt: time.Now(),
42 | }
43 | if err := d.collection.Insert(doc); err != nil {
44 | log.Printf("Can't insert document: %v\n", err)
45 | }
46 |
47 | return &doc
48 | }
49 |
50 | // RemoveByTargetID removes a track by the targetId field
51 | func (d *DataTrack) RemoveByTargetID(id string) {
52 | err := d.collection.Remove(bson.M{"targetId": bson.ObjectIdHex(id)})
53 | if err != nil {
54 | log.Printf("Can't delete document: %v\n", err)
55 | }
56 | }
57 |
58 | // Start a new instance of data track
59 | func (d *DataTrack) Start(db DB, events chan entities.Event) {
60 | d.collection = db.Session.DB(db.DBName).C("track")
61 | d.events = events
62 | }
63 |
--------------------------------------------------------------------------------
/monitor/docker.go:
--------------------------------------------------------------------------------
1 | package monitor
2 |
3 | import (
4 | "log"
5 | "strings"
6 |
7 | docker "github.com/fsouza/go-dockerclient"
8 | "github.com/maxcnunes/go-uptime-api/monitor/data"
9 | )
10 |
11 | var (
12 | client *docker.Client
13 | )
14 |
15 | const (
16 | dockerCreate = "create"
17 | endpoint = "unix:///tmp/docker.sock"
18 | )
19 |
20 | // Container has informations about a Docker container that will be used as a target
21 | type Container struct {
22 | URL string
23 | Name string
24 | }
25 |
26 | // StartEventListener starts to listen for all events from Docker.
27 | // But only cares to created containers.
28 | func StartEventListener(data *data.DataMonitor) {
29 | client, _ = docker.NewClient(endpoint)
30 |
31 | dockerEvents := make(chan *docker.APIEvents)
32 | log.Println("Starting docker events...")
33 | go func() {
34 | // add our channel as an event listener for docker events
35 | if err := client.AddEventListener(dockerEvents); err != nil {
36 | log.Fatalf("Unable to register docker events listener, error: %s", err)
37 | }
38 |
39 | // start the event loop and wait for docker events
40 | log.Print("Entering into the docker events loop")
41 | for {
42 | select {
43 | case event := <-dockerEvents:
44 | log.Printf("Received docker event status: %s, id: %s", event.Status, event.ID)
45 |
46 | // only cares to created containers
47 | if event.Status == "create" {
48 | virtualHost := getVirtualHost(event.ID)
49 | if virtualHost != "" {
50 | // assumes all virtual host are http for while
51 | data.Target.Create("http://"+virtualHost, nil)
52 | }
53 | }
54 | }
55 | }
56 | }()
57 | }
58 |
59 | // LoadAllVirtualHosts gets all VIRTUAL_HOST environment variables from all Docker containers
60 | func LoadAllVirtualHosts(data *data.DataMonitor) {
61 | filters := make(map[string][]string)
62 | filters["status"] = []string{"running"}
63 |
64 | containers, _ := client.ListContainers(docker.ListContainersOptions{All: false, Filters: filters})
65 | for _, container := range containers {
66 | log.Printf("ID: %s", container.ID)
67 | log.Printf("Names: %s", container.Names)
68 |
69 | virtualHost := getVirtualHost(container.ID)
70 |
71 | if virtualHost != "" {
72 | // assumes all virtual host are http for while
73 | data.Target.Create("http://"+virtualHost, nil)
74 | }
75 | }
76 | }
77 |
78 | func getVirtualHost(containerID string) string {
79 | info, err := client.InspectContainer(containerID)
80 | if err != nil {
81 | log.Fatalf("Unable to inspect container %s, error: %s", containerID, err)
82 | }
83 |
84 | var virtualHost string
85 | for _, env := range info.Config.Env {
86 | if strings.Contains(env, "VIRTUAL_HOST=") {
87 | virtualHost = strings.Replace(env, "VIRTUAL_HOST=", "", -1)
88 | break
89 | }
90 | }
91 |
92 | return virtualHost
93 | }
94 |
--------------------------------------------------------------------------------
/monitor/email.go:
--------------------------------------------------------------------------------
1 | package monitor
2 |
3 | import (
4 | "log"
5 | "os"
6 | "strconv"
7 |
8 | "github.com/maxcnunes/go-uptime-api/monitor/entities"
9 | "gopkg.in/gomail.v1"
10 | )
11 |
12 | type emailConfig struct {
13 | from string
14 | username string
15 | password string
16 | host string
17 | port int
18 | }
19 |
20 | func getEmailConfig() emailConfig {
21 | conf := emailConfig{
22 | from: os.Getenv("EMAIL_FROM"),
23 | username: os.Getenv("EMAIL_USERNAME"),
24 | password: os.Getenv("EMAIL_PASSWORD"),
25 | host: os.Getenv("EMAIL_HOST"),
26 | }
27 |
28 | if conf.from == "" || conf.username == "" || conf.password == "" {
29 | log.Fatalln("Missing email configurations")
30 | }
31 | if conf.host == "" {
32 | conf.host = "smtp.gmail.com"
33 | }
34 | if os.Getenv("EMAIL_PORT") == "" {
35 | conf.port = 587
36 | } else {
37 | if port, err := strconv.Atoi(os.Getenv("EMAIL_PORT")); err == nil {
38 | conf.port = port
39 | }
40 | }
41 |
42 | return conf
43 | }
44 |
45 | // SendNotificaton sends notifications to all emails related to a target
46 | // The notification can be about a uptime or downtime depending in the current target's status
47 | func SendNotificaton(target entities.Target) {
48 | if len(target.Emails) == 0 {
49 | return
50 | }
51 |
52 | conf := getEmailConfig()
53 |
54 | msg := gomail.NewMessage()
55 | msg.SetHeader("From", conf.from)
56 | msg.SetHeader("To", target.Emails...)
57 | if target.Status < 500 {
58 | msg.SetHeader("Subject", "Target is UP: "+target.URL)
59 | msg.SetBody("text/html", "Hi,
The target "+target.URL+" is back UP (HTTP "+strconv.Itoa(target.Status)+").")
60 | } else {
61 | msg.SetHeader("Subject", "Target is DOWN: "+target.URL)
62 | msg.SetBody("text/html", "The target "+target.URL+" is currently DOWN (HTTP "+strconv.Itoa(target.Status)+").
Uptime Robot will alert you when it is back up.")
63 | }
64 |
65 | mailer := gomail.NewMailer(conf.host, conf.username, conf.password, conf.port)
66 | if err := mailer.Send(msg); err != nil {
67 | panic(err)
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/monitor/entities/event.go:
--------------------------------------------------------------------------------
1 | package entities
2 |
3 | // Event has the event type triggered target data manipulation actions
4 | // and also has the related target data
5 | type Event struct {
6 | Event string `json:"event"`
7 | Target *Target `json:"target"`
8 | }
9 |
10 | const (
11 | // Added is the event type triggered when a new has been created
12 | Added = "added"
13 | // Removed is the event type triggered when an existing target has been removed
14 | Removed = "removed"
15 | // Updated is the event type triggered when an existing target has been updated
16 | Updated = "updated"
17 | )
18 |
--------------------------------------------------------------------------------
/monitor/entities/target.go:
--------------------------------------------------------------------------------
1 | package entities
2 |
3 | import "gopkg.in/mgo.v2/bson"
4 |
5 | // Target data structure
6 | type Target struct {
7 | ID bson.ObjectId `bson:"_id" json:"id"`
8 | URL string `bson:"url" json:"url"`
9 | Status int `bson:"status" json:"status"`
10 | Emails []string `bson:"emails" json:"emails"`
11 | }
12 |
--------------------------------------------------------------------------------
/monitor/entities/track.go:
--------------------------------------------------------------------------------
1 | package entities
2 |
3 | import (
4 | "time"
5 |
6 | "gopkg.in/mgo.v2/bson"
7 | )
8 |
9 | // Track data structure
10 | type Track struct {
11 | ID bson.ObjectId `bson:"_id" json:"id"`
12 | TargetID bson.ObjectId `bson:"targetId" json:"targetId"`
13 | Status int `bson:"status" json:"status"`
14 | CreatedAt time.Time `bson:"createdAt" json:"createdAt"`
15 | }
16 |
--------------------------------------------------------------------------------
/monitor/http.go:
--------------------------------------------------------------------------------
1 | package monitor
2 |
3 | import (
4 | "fmt"
5 | "net/http"
6 | "time"
7 | )
8 |
9 | // HTTPResponse defines the results from a http request
10 | type HTTPResponse struct {
11 | URL string
12 | Response *http.Response
13 | Error error
14 | }
15 |
16 | // AsyncHTTPGets requests urls concurrently
17 | func AsyncHTTPGets(urls []string) []*HTTPResponse {
18 | ch := make(chan *HTTPResponse)
19 | responses := []*HTTPResponse{}
20 |
21 | // executes concurrently the request to all urls
22 | for _, url := range urls {
23 | go func(url string) {
24 | fmt.Printf("Fetching %s \n", url)
25 | resp, err := http.Get(url)
26 | ch <- &HTTPResponse{url, resp, err}
27 | }(url)
28 | }
29 |
30 | // waits finish all requests
31 | for {
32 | select {
33 | case r := <-ch:
34 | fmt.Printf("%s was fetched: %v\n", r.URL, r)
35 | responses = append(responses, r)
36 | if len(responses) == len(urls) {
37 | return responses
38 | }
39 | case <-time.After(50 * time.Millisecond):
40 | // fmt.Println(".")
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/monitor/job.go:
--------------------------------------------------------------------------------
1 | package monitor
2 |
3 | import (
4 | "log"
5 | "net/http"
6 | "time"
7 |
8 | "github.com/maxcnunes/go-uptime-api/monitor/data"
9 | )
10 |
11 | // Job is responsible for running the polling request for all targets
12 | type Job struct {
13 | data *data.DataMonitor
14 | checkAtEvery time.Duration
15 | }
16 |
17 | func (j Job) checkTargetsStatus() {
18 | results := AsyncHTTPGets(j.data.Target.GetAllURLS())
19 | for _, result := range results {
20 | status := http.StatusBadGateway
21 | if result.Response != nil {
22 | log.Printf("%s status: %s\n", result.URL, result.Response.Status)
23 | status = result.Response.StatusCode
24 | }
25 |
26 | j.saveTracking(result.URL, status)
27 | }
28 | }
29 |
30 | func (j Job) saveTracking(url string, status int) {
31 | target := j.data.Target.FindOneByURL(url)
32 | oldStatus := target.Status
33 | j.data.Track.Create(*target, status)
34 | target.Status = status
35 | j.data.Target.Update(target.ID.Hex(), *target)
36 |
37 | // sends notification only if the status has changed
38 | if oldStatus != status {
39 | SendNotificaton(*target)
40 | }
41 | }
42 |
43 | func (j Job) checkTargetsPeriodically() {
44 | StartEventListener(j.data)
45 |
46 | ticker := time.NewTicker(j.checkAtEvery)
47 | go func() {
48 | for {
49 | select {
50 | case <-ticker.C:
51 | log.Printf("Checking %d URLs status...", len(j.data.Target.GetAll()))
52 | j.checkTargetsStatus()
53 | }
54 | }
55 | }()
56 | }
57 |
58 | // Start a new instance of Job
59 | func (j Job) Start(data *data.DataMonitor, checkAtEvery string) {
60 | j.data = data
61 |
62 | duration, err := time.ParseDuration(checkAtEvery)
63 | if err != nil {
64 | log.Fatalf("Value %v is not a valid duration time", checkAtEvery)
65 | }
66 | j.checkAtEvery = duration
67 |
68 | log.Printf("Starting targets checking async (every %s)", checkAtEvery)
69 | j.checkTargetsPeriodically()
70 | }
71 |
--------------------------------------------------------------------------------
/monitor_api_suite_test.go:
--------------------------------------------------------------------------------
1 | package main_test
2 |
3 | import (
4 | . "github.com/onsi/ginkgo"
5 | . "github.com/onsi/gomega"
6 |
7 | "testing"
8 | )
9 |
10 | func TestMonitorApi(t *testing.T) {
11 | RegisterFailHandler(Fail)
12 | RunSpecs(t, "MonitorApi Suite")
13 | }
14 |
--------------------------------------------------------------------------------
/server/api/target.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "log"
6 | "net/http"
7 |
8 | "github.com/gorilla/mux"
9 | "github.com/maxcnunes/go-uptime-api/monitor/data"
10 | "github.com/maxcnunes/go-uptime-api/monitor/entities"
11 | )
12 |
13 | // TargetAPI has all routes related to target data manipulation
14 | type TargetAPI struct {
15 | data *data.DataMonitor
16 | }
17 |
18 | // Start a new instance of target api
19 | func (api *TargetAPI) Start(data *data.DataMonitor) {
20 | api.data = data
21 | }
22 |
23 | // ListHandler handles GET request returning all targets
24 | func (api *TargetAPI) ListHandler(rw http.ResponseWriter, req *http.Request) {
25 | j, err := json.Marshal(api.data.Target.GetAll())
26 | if err != nil {
27 | panic(err)
28 | }
29 |
30 | rw.Header().Set("Content-Type", "application/json")
31 | rw.Write(j)
32 | }
33 |
34 | // DetailHandler handles GET request returning a single target
35 | func (api *TargetAPI) DetailHandler(rw http.ResponseWriter, req *http.Request) {
36 | vars := mux.Vars(req)
37 | j, err := json.Marshal(api.data.Target.FindOneByID(vars["id"]))
38 | if err != nil {
39 | panic(err)
40 | }
41 |
42 | rw.Header().Set("Content-Type", "application/json")
43 | rw.Write(j)
44 | }
45 |
46 | // CreateHanler handles POST request creating a new target
47 | func (api *TargetAPI) CreateHanler(rw http.ResponseWriter, req *http.Request) {
48 | var target entities.Target
49 |
50 | err := json.NewDecoder(req.Body).Decode(&target)
51 | if err != nil {
52 | panic(err)
53 | }
54 |
55 | api.data.Target.Create(target.URL, target.Emails)
56 |
57 | rw.WriteHeader(http.StatusCreated)
58 | }
59 |
60 | // DeleteHandler handles DELETE request deleting the proper target
61 | func (api *TargetAPI) DeleteHandler(rw http.ResponseWriter, req *http.Request) {
62 | vars := mux.Vars(req)
63 |
64 | targetID := vars["id"]
65 | api.data.Target.RemoveByID(targetID)
66 | api.data.Track.RemoveByTargetID(targetID)
67 |
68 | rw.WriteHeader(http.StatusOK)
69 | }
70 |
71 | // UpdateHandler handles PUT request updating the proper target
72 | func (api *TargetAPI) UpdateHandler(rw http.ResponseWriter, req *http.Request) {
73 | vars := mux.Vars(req)
74 | var target entities.Target
75 |
76 | err := json.NewDecoder(req.Body).Decode(&target)
77 | if err != nil {
78 | panic(err)
79 | }
80 |
81 | api.data.Target.Update(vars["id"], target)
82 |
83 | if err != nil {
84 | panic(err)
85 | }
86 |
87 | log.Printf("Updated target %s with new URL %s", vars["id"], target.URL)
88 |
89 | rw.WriteHeader(http.StatusOK)
90 | }
91 |
--------------------------------------------------------------------------------
/server/api/track.go:
--------------------------------------------------------------------------------
1 | package api
2 |
3 | import (
4 | "encoding/json"
5 | "net/http"
6 |
7 | "github.com/maxcnunes/go-uptime-api/monitor/data"
8 | )
9 |
10 | // TrackAPI has all routes related to track data manipulation
11 | type TrackAPI struct {
12 | data *data.DataMonitor
13 | }
14 |
15 | // Start a new instance of track api
16 | func (api *TrackAPI) Start(data *data.DataMonitor) {
17 | api.data = data
18 | }
19 |
20 | // ListHandler handles GET request returning all tracks
21 | func (api *TrackAPI) ListHandler(rw http.ResponseWriter, req *http.Request) {
22 | query := req.URL.Query()
23 | targetID := ""
24 | if len(query["targetId"]) > 0 {
25 | targetID = query["targetId"][0]
26 | }
27 |
28 | j, err := json.Marshal(api.data.Track.Find(targetID))
29 | if err != nil {
30 | panic(err)
31 | }
32 |
33 | rw.Header().Set("Content-Type", "application/json")
34 | rw.Write(j)
35 | }
36 |
--------------------------------------------------------------------------------
/server/router.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "log"
5 | "net/http"
6 |
7 | "github.com/maxcnunes/go-uptime-api/server/api"
8 |
9 | "github.com/gorilla/mux"
10 | "github.com/maxcnunes/go-uptime-api/monitor/data"
11 | )
12 |
13 | // Router aggregates all API routes
14 | type Router struct{}
15 |
16 | var (
17 | targetAPI = api.TargetAPI{}
18 | trackAPI = api.TrackAPI{}
19 | )
20 |
21 | // Start the API router
22 | func (r *Router) Start(data *data.DataMonitor) *mux.Router {
23 | log.Print("Starting API server")
24 | router := mux.NewRouter()
25 |
26 | // targets api
27 | targetAPI.Start(data)
28 | router.HandleFunc("/targets", addDefaultHeaders(targetAPI.ListHandler)).Methods("GET", "OPTIONS")
29 | router.HandleFunc("/targets", addDefaultHeaders(targetAPI.CreateHanler)).Methods("POST")
30 | router.HandleFunc("/targets/{id}", addDefaultHeaders(targetAPI.DetailHandler)).Methods("GET", "OPTIONS")
31 | router.HandleFunc("/targets/{id}", addDefaultHeaders(targetAPI.UpdateHandler)).Methods("PUT")
32 | router.HandleFunc("/targets/{id}", addDefaultHeaders(targetAPI.DeleteHandler)).Methods("DELETE")
33 |
34 | // tracks api
35 | trackAPI.Start(data)
36 | router.HandleFunc("/tracks", addDefaultHeaders(trackAPI.ListHandler)).Methods("GET", "OPTIONS")
37 |
38 | return router
39 | }
40 |
41 | func addDefaultHeaders(fn http.HandlerFunc) http.HandlerFunc {
42 | return func(w http.ResponseWriter, r *http.Request) {
43 | if origin := r.Header.Get("Origin"); origin != "" {
44 | w.Header().Set("Access-Control-Allow-Origin", origin)
45 | }
46 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE")
47 | w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token")
48 | w.Header().Set("Access-Control-Allow-Credentials", "true")
49 | fn(w, r)
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/server/router_test.go:
--------------------------------------------------------------------------------
1 | package server_test
2 |
3 | import (
4 | "net/http"
5 |
6 | . "github.com/onsi/ginkgo"
7 | . "github.com/onsi/gomega"
8 |
9 | "github.com/maxcnunes/go-uptime-api/monitor/entities"
10 | . "github.com/maxcnunes/go-uptime-api/server"
11 | )
12 |
13 | var (
14 | router = Router{}
15 | )
16 |
17 | var _ = Describe("server", func() {
18 | Context("Performing GET request to '/targets' route", func() {
19 | BeforeEach(func() {
20 | dataMonitor.Target.Create("http://first-targe.com", nil)
21 | })
22 |
23 | It("returns a 200 Status Code", func() {
24 | response := Request("GET", "/targets", nil, nil)
25 | Expect(response.Code).To(Equal(http.StatusOK))
26 | })
27 |
28 | It("returns a list of targets", func() {
29 | var result []entities.Target
30 | _ = Request("GET", "/targets", &result, nil)
31 |
32 | Expect(len(result)).To(Equal(1))
33 | Expect(result[0].URL).To(Equal("http://first-targe.com"))
34 | })
35 | })
36 |
37 | Context("Performing POST request to '/targets' route", func() {
38 | It("returns a 201 Status Code", func() {
39 | target := entities.Target{URL: "http://second-targe.com"}
40 | response := Request("POST", "/targets", nil, target)
41 | Expect(response.Code).To(Equal(http.StatusCreated))
42 | })
43 | })
44 |
45 | Context("Performing PUT request to '/targets' route", func() {
46 | var target *entities.Target
47 | var result entities.Target
48 |
49 | BeforeEach(func() {
50 | target = dataMonitor.Target.Create("http://first-targe.com", nil)
51 |
52 | target.URL = "http://updated-targe.com"
53 | target.Status = http.StatusBadGateway
54 | })
55 |
56 | It("returns a 200 Status Code", func() {
57 | response := Request("PUT", "/targets/"+target.ID.Hex(), &result, target)
58 | Expect(response.Code).To(Equal(http.StatusOK))
59 | })
60 |
61 | It("updates the target in the database", func() {
62 | _ = Request("PUT", "/targets/"+target.ID.Hex(), &result, target)
63 |
64 | item := dataMonitor.Target.FindOneByID(target.ID.Hex())
65 |
66 | Expect(item.URL).To(Equal(target.URL))
67 | Expect(item.Status).To(Equal(target.Status))
68 | })
69 | })
70 |
71 | Context("Performing DELETE request to '/targets' route", func() {
72 | var target *entities.Target
73 | BeforeEach(func() {
74 | target = dataMonitor.Target.Create("http://first-targe.com", nil)
75 | })
76 |
77 | It("returns a 200 Status Code", func() {
78 | response := Request("DELETE", "/targets/"+target.ID.Hex(), nil, nil)
79 | Expect(response.Code).To(Equal(http.StatusOK))
80 | })
81 | })
82 |
83 | Context("Performing GET request to '/tracks' route", func() {
84 | var target *entities.Target
85 |
86 | BeforeEach(func() {
87 | target = dataMonitor.Target.Create("http://first-targe.com", nil)
88 | dataMonitor.Track.Create(*target, http.StatusCreated)
89 | })
90 |
91 | It("returns a 200 Status Code", func() {
92 | response := Request("GET", "/tracks", nil, nil)
93 | Expect(response.Code).To(Equal(http.StatusOK))
94 | })
95 |
96 | It("returns a list of tracks", func() {
97 | var result []entities.Track
98 | _ = Request("GET", "/tracks", &result, nil)
99 |
100 | Expect(len(result)).To(Equal(1))
101 | Expect(result[0].TargetID).To(Equal(target.ID))
102 | Expect(result[0].Status).To(Equal(http.StatusCreated))
103 | })
104 | })
105 | })
106 |
--------------------------------------------------------------------------------
/server/server_suite_test.go:
--------------------------------------------------------------------------------
1 | package server_test
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "io"
7 | "log"
8 | "net/http"
9 | "net/http/httptest"
10 | "testing"
11 |
12 | "github.com/gorilla/mux"
13 |
14 | . "github.com/onsi/ginkgo"
15 | . "github.com/onsi/gomega"
16 |
17 | "github.com/maxcnunes/go-uptime-api/monitor/data"
18 | )
19 |
20 | var (
21 | db = data.DB{}
22 | dataMonitor = data.DataMonitor{}
23 | handlers *mux.Router
24 | )
25 |
26 | func TestMonitorApi(t *testing.T) {
27 | RegisterFailHandler(Fail)
28 | RunSpecs(t, "Monitor-API-Server Suite")
29 | }
30 |
31 | // Setup server's tests
32 | var _ = BeforeSuite(func() {
33 | db.Start()
34 | dataMonitor.Start(db)
35 |
36 | handlers = router.Start(&dataMonitor)
37 | })
38 |
39 | // Teardown server's tests
40 | var _ = AfterSuite(func() {
41 | db.Close()
42 | })
43 |
44 | // Cleans the databse after each test
45 | var _ = AfterEach(func() {
46 | db.Wipe()
47 | })
48 |
49 | //
50 | // Helper functions for server's test
51 | //
52 |
53 | // Request sends a new request to the server been tested
54 | func Request(method string, route string, result interface{}, body interface{}) *httptest.ResponseRecorder {
55 | bodyRequest := parseBodyRequest(body)
56 |
57 | request, _ := http.NewRequest(method, route, bodyRequest)
58 | response := httptest.NewRecorder()
59 |
60 | handlers.ServeHTTP(response, request)
61 |
62 | if result != nil {
63 | json.Unmarshal(response.Body.Bytes(), &result)
64 | }
65 |
66 | return response
67 | }
68 |
69 | func parseBodyRequest(item interface{}) io.Reader {
70 | if item == nil {
71 | return nil
72 | }
73 |
74 | body, err := json.Marshal(item)
75 | if err != nil {
76 | log.Println("Unable to marshal item")
77 | }
78 |
79 | return bytes.NewReader(body)
80 | }
81 |
--------------------------------------------------------------------------------
/server/websocket.go:
--------------------------------------------------------------------------------
1 | package server
2 |
3 | import (
4 | "log"
5 | "net/http"
6 |
7 | "github.com/gorilla/websocket"
8 | "github.com/maxcnunes/go-uptime-api/monitor/data"
9 | "github.com/maxcnunes/go-uptime-api/monitor/entities"
10 | )
11 |
12 | // Websocket aggregates all the web socket configuration and actions
13 | type Websocket struct {
14 | data *data.DataMonitor
15 | }
16 |
17 | type wsConnection struct {
18 | conn *websocket.Conn
19 | }
20 |
21 | var (
22 | upgrader = websocket.Upgrader{
23 | ReadBufferSize: 1024,
24 | WriteBufferSize: 1024,
25 | CheckOrigin: func(r *http.Request) bool { return true }, // accepts any origin
26 | }
27 | )
28 |
29 | // SendText ...
30 | func (ws wsConnection) SendText(msg string) {
31 | log.Printf("Sends ws message [%s]", msg)
32 | ws.conn.WriteMessage(websocket.TextMessage, []byte(msg))
33 | }
34 |
35 | // SendJSON ...
36 | func (ws wsConnection) SendJSON(json interface{}) {
37 | log.Printf("Sends ws object [%v]", json)
38 | if err := ws.conn.WriteJSON(json); err != nil {
39 | log.Printf(" Error sending ws object [%v]: %v", json, err)
40 | }
41 | }
42 |
43 | // Start ...
44 | func (ws Websocket) Start(dm *data.DataMonitor) func(http.ResponseWriter, *http.Request) {
45 | log.Print("Starting websocket server")
46 | ws.data = dm
47 |
48 | return func(rw http.ResponseWriter, req *http.Request) {
49 |
50 | conn, err := upgrader.Upgrade(rw, req, nil)
51 | if err != nil {
52 | log.Println(err)
53 | return
54 | }
55 | wsConn := &wsConnection{conn: conn}
56 |
57 | log.Printf("WS Connnection entering into the data events loop %s", conn.LocalAddr().String())
58 | for {
59 | select {
60 | case event := <-ws.data.Events:
61 | switch event.Event {
62 | case entities.Added:
63 | // wsConn.SendText("WS: Added new target")
64 | wsConn.SendJSON(event)
65 | case entities.Removed:
66 | wsConn.SendJSON(event)
67 | // wsConn.SendText("WS: Removed old target")
68 | }
69 | }
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------