diff --git a/api.go b/api.go index 48c28d26eed08888be47eb56fd2a5e4725928833..a0bd46e0a2f903893f5fd5fa1c1093d842cf143f 100644 --- a/api.go +++ b/api.go @@ -27,7 +27,7 @@ func (api ConfigApi) Request(target interface{}, method string, url string, body } } - targetUri := api.Url+url + targetUri := api.Url + url req, err := http.NewRequest(method, targetUri, bytes.NewBuffer(encodedBody)) if err != nil { return MetaResponse{}, err @@ -56,7 +56,7 @@ func (api ConfigApi) Request(target interface{}, method string, url string, body } if res.StatusCode != 200 { - return MetaResponse{}, errors.New(fmt.Sprintf("API Responded with non-200 status code: %d", res.StatusCode)) + return MetaResponse{}, errors.New(fmt.Sprintf("API Responded with status code %d: %s %s", res.StatusCode, res.Request.Method, res.Request.URL)) } if target != nil { diff --git a/api_metric_points.go b/api_metric_points.go index b25b85e97eeafd34fc699fe0aacd94aa4d192664..6b931ae0de4f4395b0e09ed87c78b652fcdd9cd0 100644 --- a/api_metric_points.go +++ b/api_metric_points.go @@ -28,11 +28,11 @@ func (api ConfigApi) MetricPoints(metricId int) ApiMetricPointRepo { } } -func (repo ApiMetricPointRepo) List(id int) (points []ApiMetricPoint, err error) { +func (repo ApiMetricPointRepo) List() (points []ApiMetricPoint, err error) { _, err = repo.Request( &points, "GET", - fmt.Sprintf("/metrics/%d/points/%d", repo.MetricId, id), + fmt.Sprintf("/metrics/%d/points", repo.MetricId), nil, ) return diff --git a/config.go b/config.go index cfb18f6f788dc7c8598a17e51318846c9b6729b0..160004516d84bea3871d0df6a2f37d29cb6e3eb6 100644 --- a/config.go +++ b/config.go @@ -38,4 +38,17 @@ type ConfigComponent struct { Body string `yaml:"body,omitempty"` } `yaml:"expected,omitempty"` } `yaml:"http,omitempty"` + Quassel struct { + Hostname string `yaml:"hostname,omitempty"` + Port int `yaml:"port,omitempty"` + Insecure bool `yaml:"insecure,omitempty"` + } `yaml:"quassel,omitempty"` + Postgres struct { + Hostname string `yaml:"hostname,omitempty"` + Port int `yaml:"port,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + Database string `yaml:"database,omitempty"` + Insecure bool `yaml:"insecure,omitempty"` + } `yaml:"postgres,omitempty"` } diff --git a/go.mod b/go.mod index e123a5ba71de8025977a8840c2f8979c8f246930..c422871d895bd3e6c9e9289f8b2aaa478d91c7de 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module git.kuschku.de/justjanne/cachet-monitor go 1.12 require ( + github.com/lib/pq v1.0.0 github.com/sirupsen/logrus v1.4.1 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index 3d16688ffed6c88dc68b83f9353f5429ae7f8cdb..b1a643227e50852114bffdd04320f87193a28e4d 100644 --- a/go.sum +++ b/go.sum @@ -1,28 +1,14 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docopt/docopt-go v0.0.0-20160216232012-784ddc588536/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815 h1:bWDMxwH3px2JBh6AyO7hdCn/PkvCZXii8TGj7sbtEbQ= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/miekg/dns v1.1.8 h1:1QYRAKU3lN5cRfLCkPU08hwvLJFhvjP6MqNMmQz6ZVI= -github.com/miekg/dns v1.1.8/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= -github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a h1:Igim7XhdOpBnWPuYJ70XcNpq8q3BCACtVgNfoJxOV7g= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e h1:nFYrTHrdrAOpShe27kaFHjsqYSEQ0KWqdWLu3xuZJts= -golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go index 1fd38e371ed8da09d3ec52e877a3d210a99c4e17..17f701ef9d7355bc2d3b32a9ce47ac693e38c970 100644 --- a/main.go +++ b/main.go @@ -50,8 +50,11 @@ func main() { setDefaultString(&config.Components[index].ThresholdDuration, "5m") setDefaultString(&config.Components[index].Http.Method, "GET") - setDefaultInt(&config.Components[index].Http.Expected.StatusCode, 200) + + setDefaultInt(&config.Components[index].Quassel.Port, 4242) + + setDefaultInt(&config.Components[index].Postgres.Port, 5432) } storage := NewStorage() @@ -61,8 +64,13 @@ func main() { for index, component := range config.Components { logrus.Infof("Starting Monitor #%d: %s", index, component.Name) var monitor Monitor - if component.Type == "http" { + switch component.Type { + case "http": monitor = NewHttpMonitor(component) + case "quassel": + monitor = NewQuasselMonitor(component) + case "postgres": + monitor = NewPostgresMonitor(component) } wrapper := NewMonitorWrapper(&config.Api, &storage, monitor) monitors = append(monitors, &wrapper) diff --git a/monitor.go b/monitor.go index a9fbe64143ec2f6d1e0843a6ad35e62dbdb1357a..d83da3adb7b8a2488806634f7f54b3ef7f8828e4 100644 --- a/monitor.go +++ b/monitor.go @@ -1,8 +1,8 @@ package main import ( - "fmt" "github.com/sirupsen/logrus" + "strconv" "sync" "time" ) @@ -13,10 +13,6 @@ type Datapoint struct { Latency int } -func (d Datapoint) String() string { - return fmt.Sprintf("Datapoint{time=%s, up=%t, latency=%d}", d.Time.Format(time.RFC3339), d.Up, d.Latency) -} - type MonitorWrapper struct { api *ConfigApi storage *Storage @@ -79,12 +75,24 @@ func (w *MonitorWrapper) tick() { return } - status := DetermineStatus(w.config, w.storage.Store(w.config.ComponentId, datapoint, w.thresholdDuration)) - _, err = w.api.Components().Save(w.config.ComponentId, ApiComponentBody{ - Status: status, - }) - if err != nil { - logrus.Error(err.Error()) + if w.config.ComponentId != 0 { + status := DetermineStatus(w.config, w.storage.Store(w.config.ComponentId, datapoint, w.thresholdDuration)) + _, err = w.api.Components().Save(w.config.ComponentId, ApiComponentBody{ + Status: status, + }) + if err != nil { + logrus.Error(err.Error()) + } + } + if w.config.MetricId != 0 { + _, err = w.api.MetricPoints(w.config.MetricId).Create(ApiMetricPointBody{ + Metric: w.config.MetricId, + Value: datapoint.Latency, + Timestamp: strconv.FormatInt(datapoint.Time.Unix(), 10), + }) + if err != nil { + logrus.Error(err.Error()) + } } } diff --git a/monitor_http.go b/monitor_http.go index 525caf3e8c5c9d4240b18548732ac84538680db3..b09a65ed81f5dbc035c7c87097f3ddd6c28e4f0d 100644 --- a/monitor_http.go +++ b/monitor_http.go @@ -47,7 +47,12 @@ func (m *HttpMonitor) Measure() (Datapoint, error) { after := time.Now() latency := int(after.Sub(before).Nanoseconds() / 1000000) if err != nil { - return Datapoint{}, err + logrus.Infof("Monitor %s: %s", err.Error()) + return Datapoint{ + Time: after, + Up: false, + Latency: latency, + }, nil } defer resp.Body.Close() diff --git a/monitor_postgres.go b/monitor_postgres.go new file mode 100644 index 0000000000000000000000000000000000000000..f58c4b78057801bcad3bf99a42572d219bd9c4eb --- /dev/null +++ b/monitor_postgres.go @@ -0,0 +1,58 @@ +package main + +import ( + "database/sql" + "fmt" + _ "github.com/lib/pq" + "github.com/sirupsen/logrus" + "time" +) + +type PostgresMonitor struct { + config ConfigComponent +} + +func NewPostgresMonitor(config ConfigComponent) *PostgresMonitor { + return &PostgresMonitor{ + config: config, + } +} + +func (m *PostgresMonitor) Config() *ConfigComponent { + return &m.config +} + +func (m *PostgresMonitor) Measure() (Datapoint, error) { + sslMode := "verify-full" + if m.config.Postgres.Insecure { + sslMode = "allow" + } + connStr := fmt.Sprintf( + "postgres://%s:%s@%s:%d/%s?sslmode=%s", + m.config.Postgres.Username, + m.config.Postgres.Password, + m.config.Postgres.Hostname, + m.config.Postgres.Port, + m.config.Postgres.Database, + sslMode, + ) + before := time.Now() + db, err := sql.Open("postgres", connStr) + after := time.Now() + latency := int(after.Sub(before).Nanoseconds() / 1000000) + defer db.Close() + if err != nil { + logrus.Infof("Monitor %s: %s", err) + return Datapoint{ + Time: after, + Up: false, + Latency: latency, + }, nil + } + + return Datapoint{ + Time: after, + Up: true, + Latency: latency, + }, nil +} diff --git a/monitor_quassel.go b/monitor_quassel.go new file mode 100644 index 0000000000000000000000000000000000000000..2b528fdce77b7c03d6a06d861574758f6b6a6c00 --- /dev/null +++ b/monitor_quassel.go @@ -0,0 +1,51 @@ +package main + +import ( + "git.kuschku.de/justjanne/cachet-monitor/quassel" + "github.com/sirupsen/logrus" + "time" +) + +type QuasselMonitor struct { + config ConfigComponent +} + +func NewQuasselMonitor(config ConfigComponent) *QuasselMonitor { + return &QuasselMonitor{ + config: config, + } +} + +func (m *QuasselMonitor) Config() *ConfigComponent { + return &m.config +} + +func (m *QuasselMonitor) Measure() (Datapoint, error) { + before := time.Now() + conn := quassel.Connect(quassel.ConnectOptions{ + ConnectionOptions: quassel.ConnectionOptions{ + Address: m.config.Quassel.Hostname, + Port: m.config.Quassel.Port, + Timeout: time.Duration(m.config.LatencyThresholds.Down) * time.Millisecond, + }, + Verify: !m.config.Quassel.Insecure, + }) + defer conn.Close() + after := time.Now() + latency := int(after.Sub(before).Nanoseconds() / 1000000) + + if conn.Error() != nil { + logrus.Infof("Monitor %s: %s", conn.Error()) + return Datapoint{ + Time: after, + Up: false, + Latency: latency, + }, nil + } + + return Datapoint{ + Time: after, + Up: true, + Latency: latency, + }, nil +} diff --git a/quassel/connection.go b/quassel/connection.go new file mode 100644 index 0000000000000000000000000000000000000000..f73c4b4f1db0bdd41a7ad936782739088f1a0793 --- /dev/null +++ b/quassel/connection.go @@ -0,0 +1,102 @@ +package quassel + +import ( + "bufio" + "crypto/tls" + "encoding/binary" + "fmt" + "net" + "time" +) + +type Connection struct { + hostname string + socket net.Conn + tlsSocket *tls.Conn + readWriter *bufio.ReadWriter + buffer []byte + protocolInfo ProtocolInfo + err error +} + +type ConnectionOptions struct { + Address string + Port int + Timeout time.Duration +} + +func MakeConnection(options ConnectionOptions) Connection { + socket, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", options.Address, options.Port), options.Timeout) + if err != nil { + panic(err.Error()) + } + + return Connection{ + hostname: options.Address, + socket: socket, + readWriter: bufio.NewReadWriter( + bufio.NewReader(socket), + bufio.NewWriter(socket), + ), + buffer: make([]byte, 4), + } +} + +func (c *Connection) Write(data uint32) { + if c.err == nil { + binary.BigEndian.PutUint32(c.buffer, data) + _, c.err = c.readWriter.Write(c.buffer) + } +} + +func (c *Connection) Read(len int) []byte { + buffer := make([]byte, len) + if c.err == nil { + _, c.err = c.readWriter.Read(buffer) + } + return buffer +} + +func (c *Connection) Flush() { + if c.err == nil { + c.err = c.readWriter.Flush() + } +} + +func (c *Connection) WithTLS(verify bool) { + if c.err == nil { + config := &tls.Config{ + ServerName: c.hostname, + InsecureSkipVerify: !verify, + } + c.tlsSocket = tls.Client(c.socket, config) + c.readWriter = bufio.NewReadWriter( + bufio.NewReader(c.tlsSocket), + bufio.NewWriter(c.tlsSocket), + ) + c.err = c.tlsSocket.Handshake() + } +} + +func (c *Connection) TlsState() *tls.ConnectionState { + if c.tlsSocket != nil { + state := c.tlsSocket.ConnectionState() + return &state + } else { + return nil + } +} + +func (c *Connection) ProtocolInfo() ProtocolInfo { + return c.protocolInfo +} + +func (c *Connection) Error() error { + return c.err +} + +func (c *Connection) Close() { + _ = c.readWriter.Flush() + _ = c.tlsSocket.Close() + _ = c.socket.Close() +} diff --git a/quassel/quassel.go b/quassel/quassel.go new file mode 100644 index 0000000000000000000000000000000000000000..d6f0f5cf83aa5f6614ca907d0e71988aaacea9f0 --- /dev/null +++ b/quassel/quassel.go @@ -0,0 +1,55 @@ +package quassel + +import ( + "encoding/binary" +) + +const ProtocolMagic uint32 = 0x42b33f00 + +const ProtocolFeatureTls uint8 = 0x01 +const ProtocolFeatureCompression uint8 = 0x02 + +const ProtocolDatastream uint32 = 0x02 + +type ProtocolInfo struct { + FlagTLS bool + FlagCompression bool + Data uint16 + Version uint8 +} + +func parseProtocolInfo(data []byte) ProtocolInfo { + rawFeatures := data[0] + rawData := data[1:3] + rawVersion := data[3] + + return ProtocolInfo{ + rawFeatures&ProtocolFeatureTls != 0, + rawFeatures&ProtocolFeatureCompression != 0, + binary.BigEndian.Uint16(rawData), + rawVersion, + } +} + +type ConnectOptions struct { + ConnectionOptions + Verify bool +} + +func Connect(options ConnectOptions) Connection { + conn := MakeConnection(options.ConnectionOptions) + conn.Write(ProtocolMagic | uint32(ProtocolFeatureTls)) + supportedProtocols := []uint32{ + ProtocolDatastream, + } + for _, protocol := range supportedProtocols { + conn.Write(protocol) + } + conn.Write(1 << 31) + conn.Flush() + conn.protocolInfo = parseProtocolInfo(conn.Read(4)) + if conn.protocolInfo.FlagTLS { + conn.WithTLS(options.Verify) + } + return conn +}