├── .dockerignore
├── .gitattributes
├── .gitignore
├── .golangci.yml
├── .travis.yml
├── Dockerfile
├── LICENSE
├── README.md
├── alert
├── alert.go
├── pushbullet.go
└── slack.go
├── config.go
├── config_test.go
├── example.yaml
├── go.mod
├── go.sum
├── main.go
├── manager.go
├── probe
├── dns.go
├── http.go
├── http_test.go
├── minecraft.go
├── port.go
├── probe.go
└── smtp.go
├── screenshot.png
└── static
├── css
├── font-awesome.min.css
└── main.css
├── fonts
├── FontAwesome.otf
├── fontawesome-webfont.eot
├── fontawesome-webfont.svg
├── fontawesome-webfont.ttf
├── fontawesome-webfont.woff
└── fontawesome-webfont.woff2
├── index.html
└── js
├── angular.min.js
└── controller.js
/.dockerignore:
--------------------------------------------------------------------------------
1 | README.md
2 | .travis.yaml
3 | .gitignore
4 | .gitattributes
5 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | config.sample linguist-language=Go
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /board
2 | /board.yaml
3 |
--------------------------------------------------------------------------------
/.golangci.yml:
--------------------------------------------------------------------------------
1 | linters-settings:
2 | govet:
3 | check-shadowing: true
4 |
5 | linters:
6 | enable-all: true
7 | disable:
8 | - gomnd
9 | - gochecknoinits
10 | - gochecknoglobals
11 |
12 | issues:
13 | max-per-linter: 0
14 | max-same: 0
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | go:
4 | - 1.12.x
5 |
6 | install: true
7 |
8 | git:
9 | depth: 1
10 |
11 | env:
12 | - GO111MODULE=on
13 |
14 | before_script:
15 | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.24.0
16 |
17 | script:
18 | - golangci-lint run # Run a bunch of code checkers/linters in parallel
19 | - go test -v -race ./... # Run all the tests with the race detector enabled
20 |
21 | notifications:
22 | email: false
23 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM golang:latest AS build-env
2 |
3 | # Dependencies
4 | WORKDIR /build
5 | ENV GO111MODULE=on
6 | RUN go get github.com/GeertJohan/go.rice/rice
7 | COPY go.mod go.sum ./
8 | RUN go mod download
9 |
10 | # Build
11 | COPY . ./
12 | RUN CGO_ENABLED=0 go build -ldflags '-w -s' -o /board
13 | RUN rice append --exec /board
14 |
15 | # Build runtime container
16 | FROM alpine
17 | WORKDIR /app
18 | COPY --from=build-env /board /app/board
19 | EXPOSE 8080
20 | CMD ["/app/board"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
167 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Board [](https://travis-ci.org/Lesterpig/board)
2 | =======================================================================================================================
3 |
4 | > One dashboard to check them all.
5 |
6 | This repository contains a small web server used to provide a very accurate status *for all your systems*. It also supports live alerts when a service goes down.
7 |
8 | With just a glance, you'll be able to spot the faulty parts of your infrastructure.
9 |
10 | 
11 |
12 | Installation
13 | ------------
14 |
15 | ```
16 | go get github.com/Lesterpig/board
17 | cp example.yaml board.yaml
18 | vim board.yaml
19 | ```
20 |
21 | Docker Build
22 | ------------
23 | After installation the board can be build as a docker image.
24 |
25 | ```bash
26 | # Building image
27 | docker build -t lesterpig/board .
28 | # Running container with board.yaml from current dir
29 | docker run -p 8080:8080 -v ${PWD}/board.yaml:/app/board.yaml lesterpig/board
30 | ```
31 |
32 | Build single binary
33 | -------------------
34 |
35 | You may want to include the `/static/` directory as a ZIP resource in your binary.
36 |
37 | ```
38 | go build -ldflags "-s -w" .
39 | rice append --exec board
40 | ```
41 |
42 | Documentation: https://github.com/GeertJohan/go.rice#append
43 |
--------------------------------------------------------------------------------
/alert/alert.go:
--------------------------------------------------------------------------------
1 | package alert
2 |
3 | import (
4 | "github.com/Lesterpig/board/probe"
5 | "github.com/sirupsen/logrus"
6 | )
7 |
8 | var log = logrus.StandardLogger()
9 |
10 | // Alerter is the interface all alerters should implement.
11 | type Alerter interface {
12 | Alert(status probe.Status, category, serviceName, message, link, date string)
13 | }
14 |
--------------------------------------------------------------------------------
/alert/pushbullet.go:
--------------------------------------------------------------------------------
1 | package alert
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http"
7 | "net/url"
8 | "strings"
9 |
10 | "github.com/Lesterpig/board/probe"
11 | )
12 |
13 | // Pushbullet alert container.
14 | type Pushbullet struct {
15 | client *http.Client
16 | token string
17 | }
18 |
19 | // NewPushbullet returns a Pushbullet alerter from the private token
20 | // available in the `account` page.
21 | func NewPushbullet(token string) *Pushbullet {
22 | return &Pushbullet{
23 | client: &http.Client{},
24 | token: token,
25 | }
26 | }
27 |
28 | // Alert sends a pushbullet note to the owner of the provided token.
29 | func (p *Pushbullet) Alert(status probe.Status, category, serviceName, message, link, date string) {
30 | u, _ := url.Parse("https://api.pushbullet.com/v2/pushes")
31 | r := strings.NewReader(`{
32 | "title": "` + strings.Replace(fmt.Sprintf("%s %s", serviceName, status), "\"", "\\\"", -1) + `",
33 | "body": "` + strings.Replace(fmt.Sprintf("%s (%s)", message, date), "\"", "\\\"", -1) + `",
34 | "type": "note"
35 | }`)
36 |
37 | res, err := p.client.Do(&http.Request{
38 | Method: "POST",
39 | URL: u,
40 | Header: map[string][]string{
41 | "Access-Token": {p.token},
42 | "Content-Type": {"application/json"},
43 | },
44 | Body: ioutil.NopCloser(r),
45 | })
46 |
47 | if err == nil {
48 | _ = res.Body.Close()
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/alert/slack.go:
--------------------------------------------------------------------------------
1 | package alert
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "time"
10 |
11 | "github.com/Lesterpig/board/probe"
12 | )
13 |
14 | // Slack alert container.
15 | type Slack struct {
16 | client *http.Client
17 | webhookURL string
18 | channel string
19 | httpTimeout time.Duration
20 | }
21 |
22 | // NewSlack returns a Slack alerter from the webhookURL
23 | func NewSlack(webhookURL, channel string) *Slack {
24 | return &Slack{
25 | client: &http.Client{},
26 | webhookURL: webhookURL,
27 | channel: channel,
28 | httpTimeout: time.Duration(10) * time.Second,
29 | }
30 | }
31 |
32 | // Alert sends a pushbullet note to the owner of the provided token.
33 | func (p *Slack) Alert(status probe.Status, category, title, message, link, date string) {
34 | color := "#ff0000"
35 | if status == probe.StatusOK {
36 | color = "#00ff00"
37 | }
38 |
39 | slackMessage := SlackPostMessage{
40 | Channel: p.channel,
41 | Alias: category,
42 | Attachments: []Attachment{
43 | {
44 | Color: color,
45 | Fields: []AttachmentField{
46 | {
47 | Short: false,
48 | Title: fmt.Sprintf("%s is %s", title, status),
49 | Value: fmt.Sprintf("[%s](%s) - Response: %s at (%s)", title, link, message, date),
50 | },
51 | },
52 | },
53 | },
54 | }
55 |
56 | slackBody, _ := json.Marshal(slackMessage)
57 | req, err := http.NewRequest(http.MethodPost, p.webhookURL, bytes.NewBuffer(slackBody))
58 |
59 | if err != nil {
60 | return
61 | }
62 |
63 | req.Header.Add("Content-Type", "application/json")
64 |
65 | client := &http.Client{Timeout: p.httpTimeout}
66 |
67 | resp, err := client.Do(req)
68 | if err != nil {
69 | log.Errorf("Error sending request to Slack: %s ", err)
70 | return
71 | }
72 | defer resp.Body.Close()
73 |
74 | bytes, err := ioutil.ReadAll(resp.Body)
75 |
76 | if err != nil {
77 | log.Errorf("Error response from Slack: %s ", err)
78 | return
79 | }
80 |
81 | if string(bytes) != "{\"success\":true}" {
82 | log.Errorf("Non-ok response returned from Slack: %s ", string(bytes))
83 | return
84 | }
85 |
86 | log.Info("Alert sendt")
87 | }
88 |
89 | // All models with inspiration from https://github.com/RocketChat/Rocket.Chat.Go.SDK/blob/master/models/message.go
90 |
91 | // SlackPostMessage is the main model for sending messages
92 | type SlackPostMessage struct {
93 | RoomID string `json:"roomId,omitempty"`
94 | Channel string `json:"channel,omitempty"`
95 | Text string `json:"text,omitempty"`
96 | ParseUrls bool `json:"parseUrls,omitempty"`
97 | Alias string `json:"alias,omitempty"`
98 | Emoji string `json:"emoji,omitempty"`
99 | Avatar string `json:"avatar,omitempty"`
100 | Attachments []Attachment `json:"attachments,omitempty"`
101 | }
102 |
103 | // Attachment Payload for postmessage rest API
104 | //
105 | // https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/
106 | type Attachment struct {
107 | Color string `json:"color,omitempty"`
108 | Fields []AttachmentField `json:"fields,omitempty"`
109 | }
110 |
111 | // AttachmentField Payload for postmessage rest API
112 | //
113 | // https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/
114 | type AttachmentField struct {
115 | Short bool `json:"short"`
116 | Title string `json:"title,omitempty"`
117 | Value string `json:"value,omitempty"`
118 | }
119 |
--------------------------------------------------------------------------------
/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "errors"
5 | "path/filepath"
6 | "strings"
7 | "time"
8 |
9 | "github.com/Lesterpig/board/alert"
10 | "github.com/Lesterpig/board/probe"
11 |
12 | "github.com/spf13/viper"
13 | )
14 |
15 | type serviceConfig struct {
16 | probe.Config
17 | Name string
18 | Category string
19 | Probe string
20 | }
21 |
22 | type alertConfig struct {
23 | Type string
24 | Token string
25 | Webhook string
26 | Channel string
27 | }
28 |
29 | var probeConstructors = map[string](func() probe.Prober){
30 | "dns": func() probe.Prober { return &probe.DNS{} },
31 | "http": func() probe.Prober { return &probe.HTTP{} },
32 | "minecraft": func() probe.Prober { return &probe.Minecraft{} },
33 | "port": func() probe.Prober { return &probe.Port{} },
34 | "smtp": func() probe.Prober { return &probe.SMTP{} },
35 | }
36 |
37 | var alertConstructors = map[string](func(c alertConfig) alert.Alerter){
38 | "pushbullet": func(c alertConfig) alert.Alerter {
39 | return alert.NewPushbullet(c.Token)
40 | },
41 | "slack": func(c alertConfig) alert.Alerter {
42 | return alert.NewSlack(c.Webhook, c.Channel)
43 | },
44 | }
45 |
46 | var alerters []alert.Alerter
47 |
48 | func parseConfigString(cnf string) (dir string, name string) {
49 | dir = filepath.Dir(cnf)
50 | basename := filepath.Base(cnf)
51 | name = strings.TrimSuffix(basename, filepath.Ext(basename))
52 |
53 | return
54 | }
55 |
56 | func loadConfig(configPath, configName string) (*Manager, error) {
57 | viper.SetConfigName(configName)
58 | viper.AddConfigPath(configPath)
59 |
60 | err := viper.ReadInConfig()
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | sc := make([]serviceConfig, 0)
66 |
67 | err = viper.UnmarshalKey("services", &sc)
68 | if err != nil {
69 | return nil, err
70 | }
71 |
72 | manager := Manager{}
73 | manager.Services = make(map[string]([]*Service))
74 |
75 | for _, c := range sc {
76 | constructor := probeConstructors[c.Probe]
77 | if constructor == nil {
78 | return nil, errors.New("unknown probe type: " + c.Probe)
79 | }
80 |
81 | c.Config = setProbeConfigDefaults(c.Config)
82 |
83 | prober := constructor()
84 |
85 | err = prober.Init(c.Config)
86 | if err != nil {
87 | return nil, err
88 | }
89 |
90 | manager.Services[c.Category] = append(manager.Services[c.Category], &Service{
91 | Prober: prober,
92 | Name: c.Name,
93 | Target: c.Target,
94 | })
95 | }
96 |
97 | ac := make([]alertConfig, 0)
98 |
99 | err = viper.UnmarshalKey("alerts", &ac)
100 | if err != nil {
101 | return nil, err
102 | }
103 |
104 | alerters = make([]alert.Alerter, 0)
105 |
106 | for _, c := range ac {
107 | constructor := alertConstructors[c.Type]
108 | if constructor == nil {
109 | return nil, errors.New("unknown alert type: " + c.Type)
110 | }
111 |
112 | alerters = append(alerters, constructor(c))
113 | }
114 |
115 | return &manager, err
116 | }
117 |
118 | func setProbeConfigDefaults(c probe.Config) probe.Config {
119 | if c.Warning == 0 {
120 | c.Warning = 500 * time.Millisecond
121 | }
122 |
123 | if c.Fatal == 0 {
124 | c.Fatal = time.Minute
125 | }
126 |
127 | return c
128 | }
129 |
--------------------------------------------------------------------------------
/config_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import "testing"
4 |
5 | func Test_parseConfigString(t *testing.T) {
6 | tests := []struct {
7 | name string
8 | args string
9 | wantDir string
10 | wantName string
11 | }{
12 | {
13 | args: "/etc/config/board.yaml",
14 | wantDir: "/etc/config",
15 | wantName: "board",
16 | },
17 | {
18 | args: "../config/conf.yaml",
19 | wantDir: "../config",
20 | wantName: "conf",
21 | },
22 | }
23 | for _, tt := range tests {
24 | tt := tt
25 | t.Run(tt.args, func(t *testing.T) {
26 | gotDir, gotName := parseConfigString(tt.args)
27 | if gotDir != tt.wantDir {
28 | t.Errorf("parseConfigString() gotDir = %v, want %v", gotDir, tt.wantDir)
29 | }
30 | if gotName != tt.wantName {
31 | t.Errorf("parseConfigString() gotName = %v, want %v", gotName, tt.wantName)
32 | }
33 | })
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/example.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | - name: Lesterpig.com
3 | category: Web
4 | probe: http
5 | config:
6 | target: https://www.lesterpig.com
7 | options:
8 | regex: Loïck Bonniot
9 | - name: SMTP
10 | category: Services
11 | probe: smtp
12 | config:
13 | warning: 10s
14 | fatal: 15s
15 | target: mail.google.com:465
16 | # ...
17 |
18 | alerts:
19 | - type: pushbullet
20 | token:
21 | # ...
22 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/Lesterpig/board
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/GeertJohan/go.rice v1.0.0
7 | github.com/gobuffalo/envy v1.8.1
8 | github.com/miekg/dns v1.1.13
9 | github.com/mitchellh/mapstructure v1.1.2
10 | github.com/sirupsen/logrus v1.2.0
11 | github.com/spf13/viper v1.4.0
12 | github.com/stretchr/testify v1.4.0
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
2 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
3 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
4 | github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
5 | github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ=
6 | github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0=
7 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
8 | github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
9 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
10 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
11 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
12 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
13 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
14 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
15 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
16 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
17 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
18 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
19 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
20 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
21 | github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY=
22 | github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E=
23 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
24 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
25 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
26 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
27 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
28 | github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
29 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
30 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
31 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
32 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
33 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
34 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
35 | github.com/gobuffalo/envy v1.8.1 h1:RUr68liRvs0TS1D5qdW3mQv2SjAsu1QWMCx1tG4kDjs=
36 | github.com/gobuffalo/envy v1.8.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
37 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
38 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
39 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
40 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
41 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
42 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
43 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
44 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
45 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
46 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
47 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
48 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
49 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
50 | github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
51 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
52 | github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
53 | github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
54 | github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
55 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
56 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
57 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
58 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
59 | github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
60 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
61 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
62 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
63 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
64 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
65 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
66 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
67 | github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
68 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
69 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
70 | github.com/miekg/dns v1.1.13 h1:x7DQtkU0cedzeS8TD36tT/w1Hm4rDtfCaYYAHE7TTBI=
71 | github.com/miekg/dns v1.1.13/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
72 | github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
73 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
74 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
75 | github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
76 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
77 | github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
78 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
79 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
80 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
81 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
82 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
83 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
84 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
85 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
86 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
87 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
88 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
89 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
90 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
91 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
92 | github.com/rogpeppe/go-internal v1.3.2 h1:XU784Pr0wdahMY2bYcyK6N1KuaRAdLtqD4qd8D18Bfs=
93 | github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
94 | github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo=
95 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
96 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
97 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
98 | github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
99 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
100 | github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
101 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
102 | github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
103 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
104 | github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
105 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
106 | github.com/spf13/viper v1.4.0 h1:yXHLWeravcrgGyFSyCgdYpXQ9dR9c/WED3pg1RhxqEU=
107 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
108 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
109 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
110 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
111 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
112 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
113 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
114 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
115 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
116 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
117 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
118 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
119 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
120 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
121 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
122 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
123 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
124 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
125 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
126 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
127 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
128 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
129 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
130 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
131 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
132 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092 h1:4QSRKanuywn15aTZvI/mIDEgPQpswuFndXpOj3rKEco=
133 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
134 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
135 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
136 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
137 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
138 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
139 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
140 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
141 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
142 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
143 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
144 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
145 | golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
146 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
147 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
148 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
149 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
150 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
151 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
152 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
153 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
154 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
155 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
156 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
157 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
158 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
159 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
160 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
161 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
162 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
163 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
164 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
165 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
166 |
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "flag"
6 | "fmt"
7 | "net/http"
8 | "os"
9 | "strconv"
10 | "time"
11 |
12 | rice "github.com/GeertJohan/go.rice"
13 | "github.com/gobuffalo/envy"
14 | "github.com/sirupsen/logrus"
15 | )
16 |
17 | var port = flag.Int("p", 8080, "Port to use")
18 | var intervalCli = flag.Int("i", 0, "Interval in minutes")
19 | var configPath = flag.String("f", "./board.yaml", "Path to config file")
20 |
21 | var log = logrus.StandardLogger()
22 |
23 | func main() {
24 | flag.Parse()
25 |
26 | interval := getInterval()
27 |
28 | log.Infof("Probe interval: %d", interval)
29 |
30 | manager, err := loadConfig(parseConfigString(*configPath))
31 | if err != nil {
32 | fmt.Fprintln(os.Stderr, err)
33 | os.Exit(1)
34 | }
35 |
36 | // Setup static folder
37 | http.Handle("/", http.FileServer(rice.MustFindBox("static").HTTPBox()))
38 |
39 | // Setup logic route
40 | http.HandleFunc("/data", func(w http.ResponseWriter, req *http.Request) {
41 | w.Header().Set("Content-Type", "application/json")
42 | data, _ := json.Marshal(manager)
43 | _, _ = w.Write(data)
44 | })
45 |
46 | go manager.ProbeLoop(time.Duration(int64(interval)) * time.Minute)
47 | log.Fatal(http.ListenAndServe(":"+strconv.Itoa(*port), nil))
48 | }
49 |
50 | func getInterval() int {
51 | intervalEnv := getInt(envy.Get("INTERVAL", ""))
52 |
53 | if *intervalCli != 0 {
54 | return *intervalCli
55 | } else if intervalEnv != 0 {
56 | return intervalEnv
57 | }
58 |
59 | return 10
60 | }
61 |
62 | func getInt(s string) int {
63 | i, err := strconv.ParseInt(s, 10, 0)
64 | if nil != err {
65 | return 0
66 | }
67 |
68 | return int(i)
69 | }
70 |
--------------------------------------------------------------------------------
/manager.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 |
6 | "github.com/Lesterpig/board/probe"
7 | )
8 |
9 | // Service stores several information from a service, especially its last status.
10 | type Service struct {
11 | Prober probe.Prober `json:"-"`
12 | Name string
13 | Status probe.Status
14 | Message string
15 | Target string
16 | }
17 |
18 | // Manager stores several services sorted by categories.
19 | type Manager struct {
20 | LastUpdate time.Time
21 | Services map[string]([]*Service)
22 | }
23 |
24 | // ProbeLoop starts the main loop that will call ProbeAll regularly.
25 | func (manager *Manager) ProbeLoop(interval time.Duration) {
26 | manager.ProbeAll()
27 |
28 | c := time.Tick(interval)
29 | for range c {
30 | manager.ProbeAll()
31 | }
32 | }
33 |
34 | // ProbeAll triggers the Probe function for each registered service in the manager.
35 | // Everything is done asynchronously.
36 | func (manager *Manager) ProbeAll() {
37 | log.Debug("Probing all")
38 |
39 | manager.LastUpdate = time.Now()
40 |
41 | for category, services := range manager.Services {
42 | for _, service := range services {
43 | go func(category string, service *Service) {
44 | prevStatus := service.Status
45 | service.Status, service.Message = service.Prober.Probe()
46 |
47 | if prevStatus != service.Status {
48 | if service.Status == probe.StatusError {
49 | AlertAll(category, service)
50 | } else if prevStatus == probe.StatusError {
51 | AlertAll(category, service)
52 | }
53 | }
54 | }(category, service)
55 | }
56 | }
57 | }
58 |
59 | // AlertAll sends an alert signaling the provided service is DOWN.
60 | // It uses global configuration for list of alert (`A` variable).
61 | func AlertAll(category string, service *Service) {
62 | date := time.Now().Format("15:04:05 MST")
63 |
64 | for _, alert := range alerters {
65 | alert.Alert(service.Status, category, service.Name, service.Message, service.Target, date)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/probe/dns.go:
--------------------------------------------------------------------------------
1 | package probe
2 |
3 | import (
4 | "github.com/miekg/dns"
5 | "github.com/mitchellh/mapstructure"
6 | )
7 |
8 | // DNS Probe, used to check whether a DNS server is answering.
9 | // `domain` will be resolved through a lookup for an A record.
10 | // `expected` should be the first returned IPv4 address or empty to accept any IP address.
11 | type DNS struct {
12 | Config
13 | domain, expected string
14 | }
15 |
16 | // Init configures the probe.
17 | func (d *DNS) Init(c Config) error {
18 | err := mapstructure.Decode(c.Options, d)
19 | d.Config = c
20 |
21 | return err
22 | }
23 |
24 | // Probe checks a DNS server.
25 | // If the operation succeeds, the message will be the duration of the dial in ms.
26 | // Otherwise, an error message is returned.
27 | func (d *DNS) Probe() (status Status, message string) {
28 | m := new(dns.Msg)
29 | m.SetQuestion(d.domain, dns.TypeA)
30 |
31 | c := new(dns.Client)
32 | r, rtt, err := c.Exchange(m, d.Target+":53")
33 |
34 | if err != nil {
35 | return StatusError, err.Error()
36 | }
37 |
38 | if r.Rcode != dns.RcodeSuccess {
39 | return StatusError, "Failed to resolve domain."
40 | }
41 |
42 | if answer, ok := r.Answer[0].(*dns.A); ok {
43 | if d.expected != "" && answer.A.String() != d.expected {
44 | return StatusError, "Unexpected DNS answer"
45 | }
46 | } else {
47 | return StatusError, "Failed to resolve domain."
48 | }
49 |
50 | return EvaluateDuration(rtt, d.Warning)
51 | }
52 |
--------------------------------------------------------------------------------
/probe/http.go:
--------------------------------------------------------------------------------
1 | package probe
2 |
3 | import (
4 | "crypto/tls"
5 | "io/ioutil"
6 | "net/http"
7 | "regexp"
8 | "strconv"
9 | "time"
10 |
11 | "github.com/mitchellh/mapstructure"
12 | )
13 |
14 | // HTTP Probe, used to check HTTP(S) websites status.
15 | type HTTP struct {
16 | Config
17 | client *http.Client
18 | regex *regexp.Regexp
19 | }
20 |
21 | // HTTPOptions is a structure containing optional parameters.
22 | // The `Regex` is used to check the content of the website, and can be empty.
23 | // Set `VerifyCertificate` to `false` to skip the TLS certificate verification.
24 | type HTTPOptions struct {
25 | Regex string
26 | VerifyCertificate bool
27 | }
28 |
29 | // Init configures the probe.
30 | func (h *HTTP) Init(c Config) error {
31 | h.Config = c
32 |
33 | var opts HTTPOptions
34 |
35 | err := mapstructure.Decode(c.Options, &opts)
36 | if err != nil {
37 | return err
38 | }
39 |
40 | /* #nosec G402 */
41 | tr := &http.Transport{
42 | TLSClientConfig: &tls.Config{InsecureSkipVerify: !opts.VerifyCertificate},
43 | }
44 |
45 | h.client = &http.Client{
46 | Timeout: c.Fatal,
47 | Transport: tr,
48 | }
49 | h.regex, err = regexp.Compile(opts.Regex)
50 |
51 | return err
52 | }
53 |
54 | // Probe checks a website status.
55 | // If the operation succeeds, the message will be the duration of the HTTP request in ms.
56 | // Otherwise, an error message is returned.
57 | func (h *HTTP) Probe() (status Status, message string) {
58 | start := time.Now()
59 | res, err := h.client.Get(h.Target)
60 | duration := time.Since(start)
61 |
62 | if err != nil {
63 | return StatusError, defaultConnectErrorMsg
64 | }
65 |
66 | defer func() { _ = res.Body.Close() }()
67 |
68 | if res.StatusCode != 200 {
69 | return StatusError, strconv.Itoa(res.StatusCode)
70 | }
71 |
72 | body, _ := ioutil.ReadAll(res.Body)
73 | if h.regex != nil && !h.regex.Match(body) {
74 | return StatusError, "Unexpected result"
75 | }
76 |
77 | return EvaluateDuration(duration, h.Warning)
78 | }
79 |
--------------------------------------------------------------------------------
/probe/http_test.go:
--------------------------------------------------------------------------------
1 | package probe
2 |
3 | import (
4 | "testing"
5 | "time"
6 |
7 | "github.com/stretchr/testify/assert"
8 | )
9 |
10 | func TestHTTPSuccess(t *testing.T) {
11 | h := &HTTP{}
12 | assert.Nil(t, h.Init(Config{
13 | Target: "https://www.lesterpig.com",
14 | Warning: 5 * time.Second,
15 | Fatal: 10 * time.Second,
16 | Options: map[string]interface{}{"Regex": "Loïck"},
17 | }))
18 |
19 | s, m := h.Probe()
20 |
21 | assert.True(t, StatusOK == s)
22 | t.Log(m)
23 | }
24 |
25 | func TestHTTPWarning(t *testing.T) {
26 | h := &HTTP{}
27 | assert.Nil(t, h.Init(Config{
28 | Target: "https://www.lesterpig.com",
29 | Warning: time.Microsecond,
30 | Fatal: 10 * time.Second,
31 | }))
32 |
33 | s, _ := h.Probe()
34 |
35 | assert.True(t, StatusWarning == s)
36 | }
37 |
38 | func TestHTTP404(t *testing.T) {
39 | h := &HTTP{}
40 | assert.Nil(t, h.Init(Config{
41 | Target: "https://www.lesterpig.com/404",
42 | Warning: 5 * time.Second,
43 | Fatal: 10 * time.Second,
44 | }))
45 |
46 | s, _ := h.Probe()
47 |
48 | assert.True(t, StatusError == s)
49 | }
50 |
51 | func TestHTTPError(t *testing.T) {
52 | h := &HTTP{}
53 | assert.Nil(t, h.Init(Config{
54 | Target: "https://www.lesteerpig.com",
55 | Warning: 5 * time.Second,
56 | Fatal: 10 * time.Second,
57 | }))
58 |
59 | s, _ := h.Probe()
60 |
61 | assert.True(t, StatusError == s)
62 | }
63 |
64 | func TestHTTPTimeout(t *testing.T) {
65 | h := &HTTP{}
66 | assert.Nil(t, h.Init(Config{
67 | Target: "https://www.lesterpig.com/",
68 | Warning: 5 * time.Second,
69 | Fatal: time.Microsecond,
70 | }))
71 |
72 | s, _ := h.Probe()
73 |
74 | assert.True(t, StatusError == s)
75 | }
76 |
77 | func TestHTTPUnexpected(t *testing.T) {
78 | h := &HTTP{}
79 | assert.Nil(t, h.Init(Config{
80 | Target: "https://www.lesterpig.com",
81 | Warning: 5 * time.Second,
82 | Fatal: 10 * time.Second,
83 | Options: map[string]interface{}{"Regex": "Unexpected"},
84 | }))
85 |
86 | s, m := h.Probe()
87 |
88 | assert.True(t, StatusError == s)
89 | assert.True(t, m == "Unexpected result")
90 | }
91 |
--------------------------------------------------------------------------------
/probe/minecraft.go:
--------------------------------------------------------------------------------
1 | package probe
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io"
7 | "net"
8 | )
9 |
10 | // Minecraft Probe, used to check minecraft servers status
11 | type Minecraft struct {
12 | Config
13 | }
14 |
15 | // Init configures the probe.
16 | func (m *Minecraft) Init(c Config) error {
17 | m.Config = c
18 | return nil
19 | }
20 |
21 | // Probe checks a minecraft server status.
22 | // If the operation succeeds, the message will contain the number of connected
23 | // and allowed players and the server version.
24 | // If there is no slot available for a new player, a warning will be triggered.
25 | // Otherwise, an error message is returned.
26 | func (m *Minecraft) Probe() (status Status, message string) {
27 | conn, err := net.DialTimeout("tcp", m.Target, m.Fatal)
28 | if err != nil {
29 | return StatusError, defaultConnectErrorMsg
30 | }
31 |
32 | defer func() { _ = conn.Close() }()
33 |
34 | // Handshake
35 | handshake := []byte{
36 | 0x06, // Length
37 | 0x00, // PacketID
38 | 0x13, // Protocol version varint (109)
39 | 0x00, // String length of server name, not used
40 | 0x00, // Unsigned-short port used, not used
41 | 0x00,
42 | 0x01, // Ask for status
43 | }
44 |
45 | _, err = conn.Write(handshake)
46 | if err != nil {
47 | return StatusError, "Error during handshake"
48 | }
49 |
50 | // Status
51 | stat := []byte{0x01, 0x00}
52 |
53 | _, err = conn.Write(stat)
54 | if err != nil {
55 | return StatusError, "Error during status"
56 | }
57 |
58 | // Result
59 | _ = readVarInt(conn) // Packet length
60 | _ = readVarInt(conn) // PacketID
61 | data := make([]byte, 10000)
62 |
63 | read, err := conn.Read(data)
64 | if err != nil || read < 2 {
65 | return StatusError, "No stat received"
66 | }
67 |
68 | // Try to parse data
69 | result := new(minecraftServerStats)
70 |
71 | err = json.Unmarshal(data[2:read], result)
72 | if err != nil {
73 | return StatusError, "Invalid stats"
74 | }
75 |
76 | message = fmt.Sprintf("%d / %d - %s", result.Players.Online, result.Players.Max, result.Version.Name)
77 | status = StatusOK
78 |
79 | if result.Players.Online == result.Players.Max {
80 | status = StatusWarning
81 | }
82 |
83 | return status, message
84 | }
85 |
86 | type minecraftServerStats struct {
87 | Version struct {
88 | Name string `json:"name"`
89 | } `json:"version"`
90 | Players struct {
91 | Max int `json:"max"`
92 | Online int `json:"online"`
93 | } `json:"players"`
94 | }
95 |
96 | func readVarInt(c io.Reader) (err error) {
97 | buf := []byte{0x00}
98 | res := 0
99 |
100 | for i := 0; i < 5; i++ {
101 | _, err := c.Read(buf)
102 | if err != nil {
103 | return err
104 | }
105 |
106 | res |= int((buf[0] & 0x7F) << uint(i*7))
107 |
108 | if buf[0]&0x80 == 0x00 {
109 | break
110 | }
111 | }
112 |
113 | _ = res
114 |
115 | return
116 | }
117 |
--------------------------------------------------------------------------------
/probe/port.go:
--------------------------------------------------------------------------------
1 | package probe
2 |
3 | import (
4 | "net"
5 | "net/url"
6 | "time"
7 | )
8 |
9 | // Port Probe, used to check if a port is open or not.
10 | type Port struct {
11 | Config
12 | network, addrport string
13 | }
14 |
15 | // Init configures the probe.
16 | func (p *Port) Init(c Config) error {
17 | p.Config = c
18 |
19 | u, err := url.Parse(p.Target)
20 | if err != nil {
21 | return err
22 | }
23 |
24 | p.network = u.Scheme
25 | p.addrport = u.Host
26 |
27 | return nil
28 | }
29 |
30 | // Probe checks a port status
31 | // If the operation succeeds, the message will be the duration of the dial in ms.
32 | // Otherwise, an error message is returned.
33 | func (p *Port) Probe() (status Status, message string) {
34 | start := time.Now()
35 |
36 | conn, err := net.DialTimeout(p.network, p.addrport, p.Fatal)
37 | if err != nil {
38 | return StatusError, defaultConnectErrorMsg
39 | }
40 |
41 | _ = conn.Close()
42 |
43 | return EvaluateDuration(time.Since(start), p.Warning)
44 | }
45 |
--------------------------------------------------------------------------------
/probe/probe.go:
--------------------------------------------------------------------------------
1 | // Package probe stores basic probes that are used to check services health
2 | package probe
3 |
4 | import (
5 | "fmt"
6 | "time"
7 | )
8 |
9 | // Config holds probe configuration, submitted through Init methods.
10 | type Config struct {
11 | Target string
12 | Options map[string]interface{}
13 |
14 | Warning time.Duration
15 | Fatal time.Duration
16 | }
17 |
18 | // Status represents the current status of a monitored service.
19 | type Status string
20 |
21 | // These constants represent the different available statuses of a service.
22 | const (
23 | StatusUnknown Status = ""
24 | StatusWarning Status = "WARNING"
25 | StatusError Status = "ERROR"
26 | StatusOK Status = "OK"
27 | )
28 |
29 | const defaultConnectErrorMsg = "Unable to connect"
30 |
31 | // Prober is the base interface that each probe must implement.
32 | type Prober interface {
33 | Init(Config) error
34 | Probe() (status Status, message string)
35 | }
36 |
37 | // EvaluateDuration is a shortcut for warning duration checks.
38 | // It returns a message containing the duration, and a OK or a WARNING status
39 | // depending on the provided warning duration.
40 | func EvaluateDuration(duration time.Duration, warning time.Duration) (status Status, message string) {
41 | if duration >= warning {
42 | status = StatusWarning
43 | } else {
44 | status = StatusOK
45 | }
46 |
47 | message = fmt.Sprintf("%d ms", duration.Nanoseconds()/1000000)
48 |
49 | return
50 | }
51 |
--------------------------------------------------------------------------------
/probe/smtp.go:
--------------------------------------------------------------------------------
1 | package probe
2 |
3 | import (
4 | "crypto/tls"
5 | "fmt"
6 | "net"
7 | "time"
8 | )
9 |
10 | // SMTP Probe, used to check smtp servers status
11 | // BEWARE! Only full TLS servers are working with this probe.
12 | type SMTP struct {
13 | Config
14 | }
15 |
16 | // Init configures the probe.
17 | func (s *SMTP) Init(c Config) error {
18 | s.Config = c
19 | return nil
20 | }
21 |
22 | // Probe checks a mailbox status.
23 | // If the operation succeeds, the message will be the duration of the SMTP handshake in ms.
24 | // Otherwise, an error message is returned.
25 | func (s *SMTP) Probe() (status Status, message string) {
26 | start := time.Now()
27 |
28 | conn, err := net.DialTimeout("tcp", s.Target, s.Fatal)
29 | if err != nil {
30 | return StatusError, defaultConnectErrorMsg
31 | }
32 |
33 | defer func() { _ = conn.Close() }()
34 |
35 | host, _, _ := net.SplitHostPort(s.Target)
36 | secure := tls.Client(conn, &tls.Config{
37 | ServerName: host,
38 | })
39 |
40 | data := make([]byte, 4)
41 |
42 | _, err = secure.Read(data)
43 | if err != nil {
44 | return StatusError, "TLS Error"
45 | }
46 |
47 | if fmt.Sprintf("%s", data) != "220 " {
48 | return StatusError, "Unexpected reply"
49 | }
50 |
51 | return EvaluateDuration(time.Since(start), s.Warning)
52 | }
53 |
--------------------------------------------------------------------------------
/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/screenshot.png
--------------------------------------------------------------------------------
/static/css/font-awesome.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Font Awesome 4.6.1 by @davegandy - http://fontawesome.io - @fontawesome
3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License)
4 | */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.6.1');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.6.1') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.6.1') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.6.1') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.6.1') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.6.1#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}
5 |
--------------------------------------------------------------------------------
/static/css/main.css:
--------------------------------------------------------------------------------
1 | /* General */
2 |
3 | body {
4 | background-color: #192823;
5 | color: white;
6 | font-family: BlinkMacSystemFont, -apple-system, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", "Helvetica", "Arial", sans-serif;
7 | }
8 |
9 | h1 {
10 | margin: 15px;
11 | margin-top: 30px;
12 | }
13 |
14 | .service {
15 | margin-left: 20px;
16 | display: inline-block;
17 | position: relative;
18 | text-align: center;
19 | }
20 |
21 | .service p {
22 | color: #D0C6B1;
23 | }
24 |
25 | .service .info {
26 | display: block;
27 | position: absolute;
28 | width: 100%;
29 | bottom: 20px;
30 | z-index: 2;
31 | font-size: 0.8em;
32 | }
33 |
34 | .status {
35 | height: 140px;
36 | width: 150px;
37 | padding-top: 10px;
38 | }
39 |
40 | .status i {
41 | font-size: 100px;
42 | }
43 |
44 | /* Status boxes */
45 |
46 | .status-OK {
47 | background-color: #218559;
48 | }
49 |
50 | .status-ERROR {
51 | background-color: #DD1E2F;
52 | animation: blink 3s linear infinite;
53 | }
54 |
55 | .status-WARNING {
56 | background-color: #EBB035;
57 | }
58 |
59 | .status-UNKNOWN {
60 | background-color: #D0C6B1;
61 | }
62 |
63 |
64 | .lastupdate {
65 | font-size: 100px;
66 | color: #204237;
67 |
68 | }
69 | /* Animations */
70 |
71 | @keyframes blink {
72 | 50% { opacity: 0.2; }
73 | }
74 |
75 | /* Links */
76 | /* unvisited link */
77 | a:link {
78 | color: white;
79 | }
80 |
81 | /* visited link */
82 | a:visited {
83 | color: white;
84 | }
85 |
86 | /* mouse over link */
87 | a:hover {
88 | color: white;
89 | }
90 |
91 | /* selected link */
92 | a:active {
93 | color: white;
94 | }
95 |
--------------------------------------------------------------------------------
/static/fonts/FontAwesome.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/FontAwesome.otf
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/fontawesome-webfont.eot
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/fontawesome-webfont.ttf
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/fontawesome-webfont.woff
--------------------------------------------------------------------------------
/static/fonts/fontawesome-webfont.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lesterpig/board/b7c00452f7911ace8f6b916aaba75d8b0978e3a7/static/fonts/fontawesome-webfont.woff2
--------------------------------------------------------------------------------
/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | Board
4 |
5 |
6 |
7 |
8 |
9 | {{ category }}
10 |
11 |
12 |
{{ service.Name }}
13 |
{{ service.Message }}
14 |
15 |
31 |
32 |
33 |
34 | Last update: {{ lastUpdate | date: 'yyyy-MM-dd - HH:mm' }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/static/js/controller.js:
--------------------------------------------------------------------------------
1 | angular.module('boardApp', [])
2 | .controller('boardCtrl', function($scope, $interval, $http) {
3 |
4 | $scope.categories = {}
5 | $scope.lastUpdate = ""
6 |
7 | function pool() {
8 | $http.get('/data').success(function(data) {
9 | $scope.categories = data.Services
10 | $scope.lastUpdate = data.LastUpdate
11 | })
12 | }
13 |
14 | pool()
15 | $interval(pool, 30000)
16 |
17 | })
18 |
--------------------------------------------------------------------------------