Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
137cc389a7 | ||
|
|
12c54177d4 | ||
|
|
ca11aff020 | ||
|
|
dc44979952 | ||
|
|
da2abf6585 | ||
|
|
13df66aa7e | ||
|
|
184625088b | ||
|
|
9d0e176695 | ||
|
|
6e9f5440ba | ||
|
|
0ceccccd45 | ||
|
|
c729fbdf41 | ||
|
|
30eea191d3 | ||
|
|
e0556b56b4 | ||
|
|
2d62fc7443 | ||
|
|
dfad6f0906 | ||
|
|
267a6cb6b3 | ||
|
|
025d0c5822 | ||
|
|
5793df7353 | ||
|
|
fae03e7561 | ||
|
|
bcf53f0afc | ||
|
|
e4a586b92a | ||
|
|
76b897eb05 | ||
|
|
3c1e2cd452 | ||
|
|
270dbd361b | ||
|
|
a83cf43e60 | ||
|
|
b30c46546d | ||
|
|
8b0bc42d50 | ||
|
|
466e4c05af | ||
|
|
ab4ee52859 | ||
|
|
b609679993 | ||
|
|
850f4d237b | ||
|
|
019bc8c057 | ||
|
|
a710944218 | ||
|
|
2b4097e90a | ||
|
|
7a5ad278bb | ||
|
|
f918ea38cd |
@@ -1,6 +0,0 @@
|
|||||||
FROM golang
|
|
||||||
|
|
||||||
ADD . /go/src/github.com/castawaylabs/cachet-monitor
|
|
||||||
RUN go install github.com/castawaylabs/cachet-monitor
|
|
||||||
|
|
||||||
ENTRYPOINT /go/bin/cachet-monitor
|
|
||||||
8
_config.yml
Normal file
8
_config.yml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
title: Cachet-monitor
|
||||||
|
description: Distributed monitoring plugin for CachetHQ
|
||||||
|
google_analytics:
|
||||||
|
show_downloads: true
|
||||||
|
theme: jekyll-theme-minimal
|
||||||
|
|
||||||
|
gems:
|
||||||
|
- jekyll-mentions
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
package cachet
|
|
||||||
|
|
||||||
// Component Cachet model
|
|
||||||
type Component struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Status int `json:"status_id"`
|
|
||||||
HumanStatus string `json:"-"`
|
|
||||||
IncidentCount int `json:"-"`
|
|
||||||
CreatedAt int `json:"created_at"`
|
|
||||||
UpdatedAt int `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ComponentData json response model
|
|
||||||
type ComponentData struct {
|
|
||||||
Component Component `json:"data"`
|
|
||||||
}
|
|
||||||
116
cachet/config.go
116
cachet/config.go
@@ -1,116 +0,0 @@
|
|||||||
package cachet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"github.com/castawaylabs/cachet-monitor/system"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Static config
|
|
||||||
var Config CachetConfig
|
|
||||||
|
|
||||||
// Central logger
|
|
||||||
var Logger *log.Logger
|
|
||||||
|
|
||||||
// CachetConfig is the monitoring tool configuration
|
|
||||||
type CachetConfig struct {
|
|
||||||
APIUrl string `json:"api_url"`
|
|
||||||
APIToken string `json:"api_token"`
|
|
||||||
Monitors []*Monitor `json:"monitors"`
|
|
||||||
SystemName string `json:"system_name"`
|
|
||||||
LogPath string `json:"log_path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var configPath string
|
|
||||||
var systemName string
|
|
||||||
var logPath string
|
|
||||||
flag.StringVar(&configPath, "c", "/etc/cachet-monitor.config.json", "Config path")
|
|
||||||
flag.StringVar(&systemName, "name", "", "System Name")
|
|
||||||
flag.StringVar(&logPath, "log", "", "Log path")
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
var data []byte
|
|
||||||
|
|
||||||
// test if its a url
|
|
||||||
url, err := url.ParseRequestURI(configPath)
|
|
||||||
if err == nil && len(url.Scheme) > 0 {
|
|
||||||
// download config
|
|
||||||
response, err := http.Get(configPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Cannot download network config: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
defer response.Body.Close()
|
|
||||||
data, _ = ioutil.ReadAll(response.Body)
|
|
||||||
|
|
||||||
fmt.Println("Downloaded network configuration.")
|
|
||||||
} else {
|
|
||||||
data, err = ioutil.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Config file '" + configPath + "' missing!")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(data, &Config)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Cannot parse config!")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(systemName) > 0 {
|
|
||||||
Config.SystemName = systemName
|
|
||||||
}
|
|
||||||
if len(Config.SystemName) == 0 {
|
|
||||||
// get hostname
|
|
||||||
Config.SystemName = system.GetHostname()
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(os.Getenv("CACHET_API")) > 0 {
|
|
||||||
Config.APIUrl = os.Getenv("CACHET_API")
|
|
||||||
}
|
|
||||||
if len(os.Getenv("CACHET_TOKEN")) > 0 {
|
|
||||||
Config.APIToken = os.Getenv("CACHET_TOKEN")
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(Config.APIToken) == 0 || len(Config.APIUrl) == 0 {
|
|
||||||
fmt.Printf("API URL or API Token not set. cachet-monitor won't be able to report incidents.\n\nPlease set:\n CACHET_API and CACHET_TOKEN environment variable to override settings.\n\nGet help at https://github.com/CastawayLabs/cachet-monitor\n")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(Config.Monitors) == 0 {
|
|
||||||
fmt.Printf("No monitors defined!\nSee sample configuration: https://github.com/CastawayLabs/cachet-monitor/blob/master/example.config.json\n")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(logPath) > 0 {
|
|
||||||
Config.LogPath = logPath
|
|
||||||
}
|
|
||||||
|
|
||||||
var logWriter io.Writer
|
|
||||||
logWriter = os.Stdout
|
|
||||||
if len(Config.LogPath) > 0 {
|
|
||||||
logWriter, err = os.Create(Config.LogPath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Unable to open file '%v' for logging\n", Config.LogPath)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
flags := log.Llongfile|log.Ldate|log.Ltime
|
|
||||||
if len(os.Getenv("DEVELOPMENT")) > 0 {
|
|
||||||
flags = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger = log.New(logWriter, "", flags)
|
|
||||||
}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
package cachet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Incident Cachet data model
|
|
||||||
type Incident struct {
|
|
||||||
ID int `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Status int `json:"status"` // 4?
|
|
||||||
HumanStatus string `json:"human_status"`
|
|
||||||
Component *Component `json:"-"`
|
|
||||||
ComponentID *int `json:"component_id"`
|
|
||||||
CreatedAt int `json:"created_at"`
|
|
||||||
UpdatedAt int `json:"updated_at"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncidentData is a response when creating/updating an incident
|
|
||||||
type IncidentData struct {
|
|
||||||
Incident Incident `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncidentList - from API /incidents
|
|
||||||
type IncidentList struct {
|
|
||||||
Incidents []Incident `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetIncidents - Get list of incidents
|
|
||||||
func GetIncidents() []Incident {
|
|
||||||
_, body, err := makeRequest("GET", "/incidents", nil)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Printf("Cannot get incidents: %v\n", err)
|
|
||||||
return []Incident{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var data IncidentList
|
|
||||||
err = json.Unmarshal(body, &data)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Printf("Cannot parse incidents: %v\n", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return data.Incidents
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send - Create or Update incident
|
|
||||||
func (incident *Incident) Send() {
|
|
||||||
jsonBytes, err := json.Marshal(incident)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Printf("Cannot encode incident: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
requestType := "POST"
|
|
||||||
requestURL := "/incidents"
|
|
||||||
if incident.ID > 0 {
|
|
||||||
requestType = "PUT"
|
|
||||||
requestURL += "/" + strconv.Itoa(incident.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, body, err := makeRequest(requestType, requestURL, jsonBytes)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Printf("Cannot create/update incident: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Logger.Println(strconv.Itoa(resp.StatusCode) + " " + string(body))
|
|
||||||
|
|
||||||
var data IncidentData
|
|
||||||
err = json.Unmarshal(body, &data)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println("Cannot parse incident body.")
|
|
||||||
panic(err)
|
|
||||||
} else {
|
|
||||||
incident.ID = data.Incident.ID
|
|
||||||
incident.Component = data.Incident.Component
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.StatusCode != 200 {
|
|
||||||
Logger.Println("Could not create/update incident!")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSimilarIncidentID gets the same incident.
|
|
||||||
// Updates incident.ID
|
|
||||||
func (incident *Incident) GetSimilarIncidentID() {
|
|
||||||
incidents := GetIncidents()
|
|
||||||
|
|
||||||
for _, inc := range incidents {
|
|
||||||
if incident.Name == inc.Name && incident.Message == inc.Message && incident.Status == inc.Status && incident.HumanStatus == inc.HumanStatus {
|
|
||||||
incident.ID = inc.ID
|
|
||||||
Logger.Printf("Updated incident id to %v\n", inc.ID)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (incident *Incident) fetchComponent() error {
|
|
||||||
_, body, err := makeRequest("GET", "/components/" + strconv.Itoa(*incident.ComponentID), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var data ComponentData
|
|
||||||
err = json.Unmarshal(body, &data)
|
|
||||||
if err != nil {
|
|
||||||
Logger.Println("Cannot parse component body.")
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
incident.Component = &data.Component
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (incident *Incident) UpdateComponent() {
|
|
||||||
if incident.ComponentID == nil || *incident.ComponentID == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if incident.Component == nil {
|
|
||||||
// fetch component
|
|
||||||
if err := incident.fetchComponent(); err != nil {
|
|
||||||
Logger.Printf("Cannot fetch component for incident. %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch incident.Status {
|
|
||||||
case 1, 2, 3:
|
|
||||||
if incident.Component.Status == 3 {
|
|
||||||
incident.Component.Status = 4
|
|
||||||
} else {
|
|
||||||
incident.Component.Status = 3
|
|
||||||
}
|
|
||||||
case 4:
|
|
||||||
incident.Component.Status = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBytes, _ := json.Marshal(map[string]interface{}{
|
|
||||||
"status": incident.Component.Status,
|
|
||||||
})
|
|
||||||
|
|
||||||
resp, _, err := makeRequest("PUT", "/components/" + strconv.Itoa(incident.Component.ID), jsonBytes)
|
|
||||||
if err != nil || resp.StatusCode != 200 {
|
|
||||||
Logger.Printf("Could not update component: (resp code %d) %v", resp.StatusCode, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetInvestigating sets status to Investigating
|
|
||||||
func (incident *Incident) SetInvestigating() {
|
|
||||||
incident.Status = 1
|
|
||||||
incident.HumanStatus = "Investigating"
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetIdentified sets status to Identified
|
|
||||||
func (incident *Incident) SetIdentified() {
|
|
||||||
incident.Status = 2
|
|
||||||
incident.HumanStatus = "Identified"
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetWatching sets status to Watching
|
|
||||||
func (incident *Incident) SetWatching() {
|
|
||||||
incident.Status = 3
|
|
||||||
incident.HumanStatus = "Watching"
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetFixed sets status to Fixed
|
|
||||||
func (incident *Incident) SetFixed() {
|
|
||||||
incident.Status = 4
|
|
||||||
incident.HumanStatus = "Fixed"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package cachet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SendMetric sends lag metric point
|
|
||||||
func SendMetric(metricID int, delay int64) {
|
|
||||||
if metricID <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonBytes, _ := json.Marshal(&map[string]interface{}{
|
|
||||||
"value": delay,
|
|
||||||
})
|
|
||||||
|
|
||||||
resp, _, err := makeRequest("POST", "/metrics/"+strconv.Itoa(metricID)+"/points", jsonBytes)
|
|
||||||
if err != nil || resp.StatusCode != 200 {
|
|
||||||
Logger.Printf("Could not log data point!\n%v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
package cachet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const timeout = time.Duration(time.Second)
|
|
||||||
|
|
||||||
// Monitor data model
|
|
||||||
type Monitor struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
MetricID int `json:"metric_id"`
|
|
||||||
Threshold float32 `json:"threshold"`
|
|
||||||
ComponentID *int `json:"component_id"`
|
|
||||||
ExpectedStatusCode int `json:"expected_status_code"`
|
|
||||||
|
|
||||||
History []bool `json:"-"`
|
|
||||||
LastFailReason *string `json:"-"`
|
|
||||||
Incident *Incident `json:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run loop
|
|
||||||
func (monitor *Monitor) Run() {
|
|
||||||
reqStart := getMs()
|
|
||||||
isUp := monitor.doRequest()
|
|
||||||
lag := getMs() - reqStart
|
|
||||||
|
|
||||||
if len(monitor.History) >= 10 {
|
|
||||||
monitor.History = monitor.History[len(monitor.History)-9:]
|
|
||||||
}
|
|
||||||
monitor.History = append(monitor.History, isUp)
|
|
||||||
monitor.AnalyseData()
|
|
||||||
|
|
||||||
if isUp == true && monitor.MetricID > 0 {
|
|
||||||
SendMetric(monitor.MetricID, lag)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (monitor *Monitor) doRequest() bool {
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: timeout,
|
|
||||||
}
|
|
||||||
resp, err := client.Get(monitor.URL)
|
|
||||||
if err != nil {
|
|
||||||
errString := err.Error()
|
|
||||||
monitor.LastFailReason = &errString
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
return resp.StatusCode == monitor.ExpectedStatusCode
|
|
||||||
}
|
|
||||||
|
|
||||||
// AnalyseData decides if the monitor is statistically up or down and creates / resolves an incident
|
|
||||||
func (monitor *Monitor) AnalyseData() {
|
|
||||||
// look at the past few incidents
|
|
||||||
numDown := 0
|
|
||||||
for _, wasUp := range monitor.History {
|
|
||||||
if wasUp == false {
|
|
||||||
numDown++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t := (float32(numDown) / float32(len(monitor.History))) * 100
|
|
||||||
Logger.Printf("%s %.2f%% Down at %v. Threshold: %.2f%%\n", monitor.URL, t, time.Now().UnixNano()/int64(time.Second), monitor.Threshold)
|
|
||||||
|
|
||||||
if len(monitor.History) != 10 {
|
|
||||||
// not enough data
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if t > monitor.Threshold && monitor.Incident == nil {
|
|
||||||
// is down, create an incident
|
|
||||||
Logger.Println("Creating incident...")
|
|
||||||
|
|
||||||
monitor.Incident = &Incident{
|
|
||||||
Name: monitor.Name + " - " + Config.SystemName,
|
|
||||||
Message: monitor.Name + " failed",
|
|
||||||
ComponentID: monitor.ComponentID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if monitor.LastFailReason != nil {
|
|
||||||
monitor.Incident.Message += "\n\n" + *monitor.LastFailReason
|
|
||||||
}
|
|
||||||
|
|
||||||
// set investigating status
|
|
||||||
monitor.Incident.SetInvestigating()
|
|
||||||
|
|
||||||
// lookup relevant incident
|
|
||||||
monitor.Incident.GetSimilarIncidentID()
|
|
||||||
|
|
||||||
// create/update incident
|
|
||||||
monitor.Incident.Send()
|
|
||||||
monitor.Incident.UpdateComponent()
|
|
||||||
} else if t < monitor.Threshold && monitor.Incident != nil {
|
|
||||||
// was down, created an incident, its now ok, make it resolved.
|
|
||||||
Logger.Println("Updating incident to resolved...")
|
|
||||||
|
|
||||||
// Add resolved message
|
|
||||||
monitor.Incident.Message += "\n\n-\n\nResolved at " + time.Now().String()
|
|
||||||
|
|
||||||
monitor.Incident.SetFixed()
|
|
||||||
monitor.Incident.Send()
|
|
||||||
monitor.Incident.UpdateComponent()
|
|
||||||
|
|
||||||
monitor.Incident = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMs() int64 {
|
|
||||||
return time.Now().UnixNano() / int64(time.Millisecond)
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package cachet
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
func makeRequest(requestType string, url string, reqBody []byte) (*http.Response, []byte, error) {
|
|
||||||
req, err := http.NewRequest(requestType, Config.APIUrl+url, bytes.NewBuffer(reqBody))
|
|
||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-Cachet-Token", Config.APIToken)
|
|
||||||
|
|
||||||
client := &http.Client{}
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, []byte{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
defer res.Body.Close()
|
|
||||||
body, _ := ioutil.ReadAll(res.Body)
|
|
||||||
|
|
||||||
return res, body, nil
|
|
||||||
}
|
|
||||||
123
cli/main.go
Normal file
123
cli/main.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
cachet "github.com/castawaylabs/cachet-monitor"
|
||||||
|
)
|
||||||
|
|
||||||
|
var configPath string
|
||||||
|
var systemName string
|
||||||
|
var logPath string
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.StringVar(&configPath, "c", "/etc/cachet-monitor.config.json", "Config path")
|
||||||
|
flag.StringVar(&systemName, "name", "", "System Name")
|
||||||
|
flag.StringVar(&logPath, "log", "", "Log path")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
cfg, err := getConfiguration(configPath)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(systemName) > 0 {
|
||||||
|
cfg.SystemName = systemName
|
||||||
|
}
|
||||||
|
if len(logPath) > 0 {
|
||||||
|
cfg.LogPath = logPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(os.Getenv("CACHET_API")) > 0 {
|
||||||
|
cfg.APIUrl = os.Getenv("CACHET_API")
|
||||||
|
}
|
||||||
|
if len(os.Getenv("CACHET_TOKEN")) > 0 {
|
||||||
|
cfg.APIToken = os.Getenv("CACHET_TOKEN")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cfg.ValidateConfiguration(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Logger.Printf("System: %s\nAPI: %s\nMonitors: %d\n\n", cfg.SystemName, cfg.APIUrl, len(cfg.Monitors))
|
||||||
|
|
||||||
|
wg := &sync.WaitGroup{}
|
||||||
|
for _, mon := range cfg.Monitors {
|
||||||
|
go mon.Start(cfg, wg)
|
||||||
|
}
|
||||||
|
|
||||||
|
signals := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(signals, os.Interrupt, os.Kill)
|
||||||
|
<-signals
|
||||||
|
|
||||||
|
cfg.Logger.Println("Abort: Waiting monitors to finish")
|
||||||
|
for _, mon := range cfg.Monitors {
|
||||||
|
mon.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLogger(logPath string) *log.Logger {
|
||||||
|
var logWriter = os.Stdout
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if len(logPath) > 0 {
|
||||||
|
logWriter, err = os.Create(logPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Unable to open file '%v' for logging\n", logPath)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flags := log.Llongfile | log.Ldate | log.Ltime
|
||||||
|
if len(os.Getenv("CACHET_DEV")) > 0 {
|
||||||
|
flags = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return log.New(logWriter, "", flags)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfiguration(path string) (*cachet.CachetMonitor, error) {
|
||||||
|
var cfg cachet.CachetMonitor
|
||||||
|
var data []byte
|
||||||
|
|
||||||
|
// test if its a url
|
||||||
|
url, err := url.ParseRequestURI(path)
|
||||||
|
if err == nil && len(url.Scheme) > 0 {
|
||||||
|
// download config
|
||||||
|
response, err := http.Get(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Cannot download network config: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
defer response.Body.Close()
|
||||||
|
data, _ = ioutil.ReadAll(response.Body)
|
||||||
|
|
||||||
|
fmt.Println("Downloaded network configuration.")
|
||||||
|
} else {
|
||||||
|
data, err = ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("Config file '" + path + "' missing!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return nil, errors.New("Cannot parse config!")
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.Logger = getLogger(cfg.LogPath)
|
||||||
|
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
65
config.go
Normal file
65
config.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package cachet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CachetMonitor struct {
|
||||||
|
Logger *log.Logger `json:"-"`
|
||||||
|
|
||||||
|
APIUrl string `json:"api_url"`
|
||||||
|
APIToken string `json:"api_token"`
|
||||||
|
SystemName string `json:"system_name"`
|
||||||
|
LogPath string `json:"log_path"`
|
||||||
|
InsecureAPI bool `json:"insecure_api"`
|
||||||
|
|
||||||
|
Monitors []*Monitor `json:"monitors"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *CachetMonitor) ValidateConfiguration() error {
|
||||||
|
if cfg.Logger == nil {
|
||||||
|
cfg.Logger = log.New(os.Stdout, "", log.Llongfile|log.Ldate|log.Ltime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.SystemName) == 0 {
|
||||||
|
// get hostname
|
||||||
|
cfg.SystemName = getHostname()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.APIToken) == 0 || len(cfg.APIUrl) == 0 {
|
||||||
|
return errors.New("API URL or API Token not set. cachet-monitor won't be able to report incidents.\n\nPlease set:\n CACHET_API and CACHET_TOKEN environment variable to override settings.\n\nGet help at https://github.com/castawaylabs/cachet-monitor\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(cfg.Monitors) == 0 {
|
||||||
|
return errors.New("No monitors defined!\nSee sample configuration: https://github.com/castawaylabs/cachet-monitor/blob/master/example.config.json\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, monitor := range cfg.Monitors {
|
||||||
|
if err := monitor.ValidateConfiguration(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getHostname returns id of the current system
|
||||||
|
func getHostname() string {
|
||||||
|
hostname, err := os.Hostname()
|
||||||
|
if err != nil || len(hostname) == 0 {
|
||||||
|
addrs, err := net.InterfaceAddrs()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
return addr.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return hostname
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"api_url": "https://demo.cachethq.io/api",
|
"api_url": "https://demo.cachethq.io/api/v1",
|
||||||
"api_token": "9yMHsdioQosnyVK4iCVR",
|
"api_token": "9yMHsdioQosnyVK4iCVR",
|
||||||
|
"interval": 5,
|
||||||
"monitors": [
|
"monitors": [
|
||||||
{
|
{
|
||||||
"name": "nodegear frontend",
|
"name": "nodegear frontend",
|
||||||
@@ -8,7 +9,9 @@
|
|||||||
"metric_id": 1,
|
"metric_id": 1,
|
||||||
"threshold": 80,
|
"threshold": 80,
|
||||||
"component_id": null,
|
"component_id": null,
|
||||||
"expected_status_code": 200
|
"expected_status_code": 200,
|
||||||
|
"strict_tls": true
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
}
|
"insecure_api": false
|
||||||
|
}
|
||||||
|
|||||||
58
http.go
Normal file
58
http.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package cachet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (monitor *CachetMonitor) makeRequest(requestType string, url string, reqBody []byte) (*http.Response, []byte, error) {
|
||||||
|
req, err := http.NewRequest(requestType, monitor.APIUrl+url, bytes.NewBuffer(reqBody))
|
||||||
|
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
req.Header.Set("X-Cachet-Token", monitor.APIToken)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
if monitor.InsecureAPI == true {
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, []byte{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer res.Body.Close()
|
||||||
|
body, _ := ioutil.ReadAll(res.Body)
|
||||||
|
|
||||||
|
return res, body, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendMetric sends lag metric point
|
||||||
|
func (monitor *Monitor) SendMetric(delay int64) error {
|
||||||
|
if monitor.MetricID == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, _ := json.Marshal(&map[string]interface{}{
|
||||||
|
"value": delay,
|
||||||
|
})
|
||||||
|
|
||||||
|
resp, _, err := monitor.config.makeRequest("POST", "/metrics/"+strconv.Itoa(monitor.MetricID)+"/points", jsonBytes)
|
||||||
|
if err != nil || resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("Could not log data point!\n%v\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMs() int64 {
|
||||||
|
return time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
}
|
||||||
114
incident.go
Normal file
114
incident.go
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
package cachet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Incident Cachet data model
|
||||||
|
type Incident struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Status int `json:"status"`
|
||||||
|
Visible int `json"visible"`
|
||||||
|
Notify bool `json:"notify"`
|
||||||
|
|
||||||
|
ComponentID int `json:"component_id"`
|
||||||
|
ComponentStatus int `json:"component_status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send - Create or Update incident
|
||||||
|
func (incident *Incident) Send(cfg *CachetMonitor) error {
|
||||||
|
switch incident.Status {
|
||||||
|
case 1, 2, 3:
|
||||||
|
// partial outage
|
||||||
|
incident.ComponentStatus = 3
|
||||||
|
|
||||||
|
componentStatus, err := incident.GetComponentStatus(cfg)
|
||||||
|
if componentStatus == 3 {
|
||||||
|
// major outage
|
||||||
|
incident.ComponentStatus = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
cfg.Logger.Printf("cannot fetch component: %v", err)
|
||||||
|
}
|
||||||
|
case 4:
|
||||||
|
// fixed
|
||||||
|
incident.ComponentStatus = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
requestType := "POST"
|
||||||
|
requestURL := "/incidents"
|
||||||
|
if incident.ID > 0 {
|
||||||
|
requestType = "PUT"
|
||||||
|
requestURL += "/" + strconv.Itoa(incident.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonBytes, _ := json.Marshal(incident)
|
||||||
|
|
||||||
|
resp, body, err := cfg.makeRequest(requestType, requestURL, jsonBytes)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Incident struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
return fmt.Errorf("Cannot parse incident body: %v, %v", err, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
incident.ID = data.Incident.ID
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return fmt.Errorf("Could not create/update incident!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (incident *Incident) GetComponentStatus(cfg *CachetMonitor) (int, error) {
|
||||||
|
resp, body, err := cfg.makeRequest("GET", "/components/"+strconv.Itoa(incident.ComponentID), nil)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return 0, fmt.Errorf("Invalid status code. Received %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data struct {
|
||||||
|
Component struct {
|
||||||
|
Status int `json:"status"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(body, &data); err != nil {
|
||||||
|
return 0, fmt.Errorf("Cannot parse component body: %v. Err = %v", string(body), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.Component.Status, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetInvestigating sets status to Investigating
|
||||||
|
func (incident *Incident) SetInvestigating() {
|
||||||
|
incident.Status = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIdentified sets status to Identified
|
||||||
|
func (incident *Incident) SetIdentified() {
|
||||||
|
incident.Status = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetWatching sets status to Watching
|
||||||
|
func (incident *Incident) SetWatching() {
|
||||||
|
incident.Status = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFixed sets status to Fixed
|
||||||
|
func (incident *Incident) SetFixed() {
|
||||||
|
incident.Status = 4
|
||||||
|
}
|
||||||
187
index.md
Normal file
187
index.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- [x] Creates & Resolves Incidents
|
||||||
|
- [x] Posts monitor lag to cachet graphs
|
||||||
|
- [x] HTTP Checks (body/status code)
|
||||||
|
- [x] DNS Checks
|
||||||
|
- [x] Updates Component to Partial Outage
|
||||||
|
- [x] Updates Component to Major Outage if already in Partial Outage (works with distributed monitors)
|
||||||
|
- [x] Can be run on multiple servers and geo regions
|
||||||
|
|
||||||
|
## Example Configuration
|
||||||
|
|
||||||
|
**Note:** configuration can be in json or yaml format. [`example.config.json`](https://github.com/CastawayLabs/cachet-monitor/blob/master/example.config.json), [`example.config.yaml`](https://github.com/CastawayLabs/cachet-monitor/blob/master/example.config.yml) files.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
api:
|
||||||
|
# cachet url
|
||||||
|
url: https://demo.cachethq.io/api/v1
|
||||||
|
# cachet api token
|
||||||
|
token: 9yMHsdioQosnyVK4iCVR
|
||||||
|
insecure: false
|
||||||
|
# https://golang.org/src/time/format.go#L57
|
||||||
|
date_format: 02/01/2006 15:04:05 MST
|
||||||
|
monitors:
|
||||||
|
# http monitor example
|
||||||
|
- name: google
|
||||||
|
# test url
|
||||||
|
target: https://google.com
|
||||||
|
# strict certificate checking for https
|
||||||
|
strict: true
|
||||||
|
# HTTP method
|
||||||
|
method: POST
|
||||||
|
|
||||||
|
# set to update component (either component_id or metric_id are required)
|
||||||
|
component_id: 1
|
||||||
|
# set to post lag to cachet metric (graph)
|
||||||
|
metric_id: 4
|
||||||
|
|
||||||
|
# custom templates (see readme for details)
|
||||||
|
template:
|
||||||
|
investigating:
|
||||||
|
subject: "{{ .Monitor.Name }} - {{ .SystemName }}"
|
||||||
|
message: "{{ .Monitor.Name }} check **failed** (server time: {{ .now }})\n\n{{ .FailReason }}"
|
||||||
|
fixed:
|
||||||
|
subject: "I HAVE BEEN FIXED"
|
||||||
|
|
||||||
|
# seconds between checks
|
||||||
|
interval: 1
|
||||||
|
# seconds for timeout
|
||||||
|
timeout: 1
|
||||||
|
# If % of downtime is over this threshold, open an incident
|
||||||
|
threshold: 80
|
||||||
|
|
||||||
|
# custom HTTP headers
|
||||||
|
headers:
|
||||||
|
Authorization: Basic <hash>
|
||||||
|
# expected status code (either status code or body must be supplied)
|
||||||
|
expected_status_code: 200
|
||||||
|
# regex to match body
|
||||||
|
expected_body: "P.*NG"
|
||||||
|
# dns monitor example
|
||||||
|
- name: dns
|
||||||
|
# fqdn
|
||||||
|
target: matej.me.
|
||||||
|
# question type (A/AAAA/CNAME/...)
|
||||||
|
question: mx
|
||||||
|
type: dns
|
||||||
|
# set component_id/metric_id
|
||||||
|
component_id: 2
|
||||||
|
# poll every 1s
|
||||||
|
interval: 1
|
||||||
|
timeout: 1
|
||||||
|
# custom DNS server (defaults to system)
|
||||||
|
dns: 8.8.4.4:53
|
||||||
|
answers:
|
||||||
|
# exact/regex check
|
||||||
|
- regex: [1-9] alt[1-9].aspmx.l.google.com.
|
||||||
|
- exact: 10 aspmx2.googlemail.com.
|
||||||
|
- exact: 1 aspmx.l.google.com.
|
||||||
|
- exact: 10 aspmx3.googlemail.com.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Download binary from [release page](https://github.com/CastawayLabs/cachet-monitor/releases)
|
||||||
|
2. Create a configuration
|
||||||
|
3. `cachet-monitor -c /etc/cachet-monitor.yaml`
|
||||||
|
|
||||||
|
pro tip: run in background using `nohup cachet-monitor 2>&1 > /var/log/cachet-monitor.log &`
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage:
|
||||||
|
cachet-monitor (-c PATH | --config PATH) [--log=LOGPATH] [--name=NAME] [--immediate]
|
||||||
|
cachet-monitor -h | --help | --version
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
PATH path to config.json
|
||||||
|
LOGPATH path to log output (defaults to STDOUT)
|
||||||
|
NAME name of this logger
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
cachet-monitor -c /root/cachet-monitor.json
|
||||||
|
cachet-monitor -c /root/cachet-monitor.json --log=/var/log/cachet-monitor.log --name="development machine"
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-c PATH.json --config PATH Path to configuration file
|
||||||
|
-h --help Show this screen.
|
||||||
|
--version Show version
|
||||||
|
--immediate Tick immediately (by default waits for first defined interval)
|
||||||
|
|
||||||
|
Environment varaibles:
|
||||||
|
CACHET_API override API url from configuration
|
||||||
|
CACHET_TOKEN override API token from configuration
|
||||||
|
CACHET_DEV set to enable dev logging
|
||||||
|
```
|
||||||
|
|
||||||
|
## Templates
|
||||||
|
|
||||||
|
This package makes use of [`text/template`](https://godoc.org/text/template). [Default HTTP template](https://github.com/CastawayLabs/cachet-monitor/blob/master/http.go#L14)
|
||||||
|
|
||||||
|
The following variables are available:
|
||||||
|
|
||||||
|
| Root objects |
|
||||||
|
| ------------- | -----------------
|
||||||
|
| `.SystemName` | system name
|
||||||
|
| `.API` | `api` object from configuration
|
||||||
|
| `.Monitor` | `monitor` object from configuration
|
||||||
|
| `.now` | formatted date string
|
||||||
|
|
||||||
|
| Monitor variables |
|
||||||
|
| ------------------ |
|
||||||
|
| `.Name` |
|
||||||
|
| `.Target` |
|
||||||
|
| `.Type` |
|
||||||
|
| `.Strict` |
|
||||||
|
| `.MetricID` |
|
||||||
|
| ... |
|
||||||
|
|
||||||
|
All monitor variables are available from `monitor.go`
|
||||||
|
|
||||||
|
## Vision and goals
|
||||||
|
|
||||||
|
We made this tool because we felt the need to have our own monitoring software (leveraging on Cachet).
|
||||||
|
The idea is a stateless program which collects data and pushes it to a central cachet instance.
|
||||||
|
|
||||||
|
This gives us power to have an army of geographically distributed loggers and reveal issues in both latency & downtime on client websites.
|
||||||
|
|
||||||
|
## Package usage
|
||||||
|
|
||||||
|
When using `cachet-monitor` as a package in another program, you should follow what `cli/main.go` does. It is important to call `Validate` on `CachetMonitor` and all the monitors inside.
|
||||||
|
|
||||||
|
[API Documentation](https://godoc.org/github.com/CastawayLabs/cachet-monitor)
|
||||||
|
|
||||||
|
# Contributions welcome
|
||||||
|
|
||||||
|
We'll happily accept contributions for the following (non exhaustive list).
|
||||||
|
|
||||||
|
- Implement ICMP check
|
||||||
|
- Implement TCP check
|
||||||
|
- Any bug fixes / code improvements
|
||||||
|
- Test cases
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2016 Castaway Labs LLC
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
29
main.go
29
main.go
@@ -1,29 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/castawaylabs/cachet-monitor/cachet"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
config := cachet.Config
|
|
||||||
log := cachet.Logger
|
|
||||||
|
|
||||||
log.Printf("System: %s, API: %s\n", config.SystemName, config.APIUrl)
|
|
||||||
log.Printf("Starting %d monitors:\n", len(config.Monitors))
|
|
||||||
for _, mon := range config.Monitors {
|
|
||||||
log.Printf(" %s: GET %s & Expect HTTP %d\n", mon.Name, mon.URL, mon.ExpectedStatusCode)
|
|
||||||
if mon.MetricID > 0 {
|
|
||||||
log.Printf(" - Logs lag to metric id: %d\n", mon.MetricID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Second)
|
|
||||||
for range ticker.C {
|
|
||||||
for _, mon := range config.Monitors {
|
|
||||||
go mon.Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
252
monitor.go
Normal file
252
monitor.go
Normal file
@@ -0,0 +1,252 @@
|
|||||||
|
package cachet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const HttpTimeout = time.Duration(time.Second)
|
||||||
|
const DefaultInterval = 60
|
||||||
|
const DefaultTimeFormat = "15:04:05 Jan 2 MST"
|
||||||
|
|
||||||
|
// Monitor data model
|
||||||
|
type Monitor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
StrictTLS bool `json:"strict_tls"`
|
||||||
|
CheckInterval time.Duration `json:"interval"`
|
||||||
|
|
||||||
|
MetricID int `json:"metric_id"`
|
||||||
|
ComponentID int `json:"component_id"`
|
||||||
|
|
||||||
|
// Threshold = percentage
|
||||||
|
Threshold float32 `json:"threshold"`
|
||||||
|
ExpectedStatusCode int `json:"expected_status_code"`
|
||||||
|
// compiled to Regexp
|
||||||
|
ExpectedBody string `json:"expected_body"`
|
||||||
|
bodyRegexp *regexp.Regexp
|
||||||
|
|
||||||
|
history []bool
|
||||||
|
lastFailReason string
|
||||||
|
incident *Incident
|
||||||
|
config *CachetMonitor
|
||||||
|
|
||||||
|
// Closed when mon.Stop() is called
|
||||||
|
stopC chan bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mon *Monitor) Start(cfg *CachetMonitor, wg *sync.WaitGroup) {
|
||||||
|
wg.Add(1)
|
||||||
|
mon.config = cfg
|
||||||
|
mon.stopC = make(chan bool)
|
||||||
|
|
||||||
|
mon.config.Logger.Printf(" Starting %s: %d seconds check interval\n - %v %s", mon.Name, mon.CheckInterval, mon.Method, mon.URL)
|
||||||
|
|
||||||
|
// print features
|
||||||
|
if mon.ExpectedStatusCode > 0 {
|
||||||
|
mon.config.Logger.Printf(" - Expect HTTP %d", mon.ExpectedStatusCode)
|
||||||
|
}
|
||||||
|
if len(mon.ExpectedBody) > 0 {
|
||||||
|
mon.config.Logger.Printf(" - Expect Body to match \"%v\"", mon.ExpectedBody)
|
||||||
|
}
|
||||||
|
if mon.MetricID > 0 {
|
||||||
|
mon.config.Logger.Printf(" - Log lag to metric id %d\n", mon.MetricID)
|
||||||
|
}
|
||||||
|
if mon.ComponentID > 0 {
|
||||||
|
mon.config.Logger.Printf(" - Update component id %d\n\n", mon.ComponentID)
|
||||||
|
}
|
||||||
|
|
||||||
|
mon.Tick()
|
||||||
|
|
||||||
|
ticker := time.NewTicker(mon.CheckInterval * time.Second)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
mon.Tick()
|
||||||
|
case <-mon.stopC:
|
||||||
|
wg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) Stop() {
|
||||||
|
if monitor.Stopped() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
close(monitor.stopC)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) Stopped() bool {
|
||||||
|
select {
|
||||||
|
case <-monitor.stopC:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) Tick() {
|
||||||
|
reqStart := getMs()
|
||||||
|
isUp := monitor.doRequest()
|
||||||
|
lag := getMs() - reqStart
|
||||||
|
|
||||||
|
if len(monitor.history) == 9 {
|
||||||
|
monitor.config.Logger.Printf("%v is now saturated\n", monitor.Name)
|
||||||
|
}
|
||||||
|
if len(monitor.history) >= 10 {
|
||||||
|
monitor.history = monitor.history[len(monitor.history)-9:]
|
||||||
|
}
|
||||||
|
monitor.history = append(monitor.history, isUp)
|
||||||
|
monitor.AnalyseData()
|
||||||
|
|
||||||
|
if isUp == true && monitor.MetricID > 0 {
|
||||||
|
monitor.SendMetric(lag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) doRequest() bool {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: HttpTimeout,
|
||||||
|
}
|
||||||
|
if monitor.StrictTLS == false {
|
||||||
|
client.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Get(monitor.URL)
|
||||||
|
if err != nil {
|
||||||
|
monitor.lastFailReason = err.Error()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if monitor.ExpectedStatusCode > 0 && resp.StatusCode != monitor.ExpectedStatusCode {
|
||||||
|
monitor.lastFailReason = "Unexpected response code: " + strconv.Itoa(resp.StatusCode) + ". Expected " + strconv.Itoa(monitor.ExpectedStatusCode)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if monitor.bodyRegexp != nil {
|
||||||
|
// check body
|
||||||
|
responseBody, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
monitor.lastFailReason = err.Error()
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
match := monitor.bodyRegexp.Match(responseBody)
|
||||||
|
if !match {
|
||||||
|
monitor.lastFailReason = "Unexpected body: " + string(responseBody) + ". Expected to match " + monitor.ExpectedBody
|
||||||
|
}
|
||||||
|
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// AnalyseData decides if the monitor is statistically up or down and creates / resolves an incident
|
||||||
|
func (monitor *Monitor) AnalyseData() {
|
||||||
|
// look at the past few incidents
|
||||||
|
numDown := 0
|
||||||
|
for _, wasUp := range monitor.history {
|
||||||
|
if wasUp == false {
|
||||||
|
numDown++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t := (float32(numDown) / float32(len(monitor.history))) * 100
|
||||||
|
monitor.config.Logger.Printf("%s %.2f%%/%.2f%% down at %v\n", monitor.Name, t, monitor.Threshold, time.Now().UnixNano()/int64(time.Second))
|
||||||
|
|
||||||
|
if len(monitor.history) != 10 {
|
||||||
|
// not saturated
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if t > monitor.Threshold && monitor.incident == nil {
|
||||||
|
monitor.incident = &Incident{
|
||||||
|
Name: monitor.Name + " - " + monitor.config.SystemName,
|
||||||
|
ComponentID: monitor.ComponentID,
|
||||||
|
Message: monitor.Name + " check **failed** - " + time.Now().Format(DefaultTimeFormat),
|
||||||
|
Notify: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(monitor.lastFailReason) > 0 {
|
||||||
|
monitor.incident.Message += "\n\n `" + monitor.lastFailReason + "`"
|
||||||
|
}
|
||||||
|
|
||||||
|
// is down, create an incident
|
||||||
|
monitor.config.Logger.Printf("%v creating incident. Monitor is down: %v", monitor.Name, monitor.lastFailReason)
|
||||||
|
// set investigating status
|
||||||
|
monitor.incident.SetInvestigating()
|
||||||
|
// create/update incident
|
||||||
|
if err := monitor.incident.Send(monitor.config); err != nil {
|
||||||
|
monitor.config.Logger.Printf("Error sending incident: %v\n", err)
|
||||||
|
}
|
||||||
|
} else if t < monitor.Threshold && monitor.incident != nil {
|
||||||
|
// was down, created an incident, its now ok, make it resolved.
|
||||||
|
monitor.config.Logger.Printf("%v resolved downtime incident", monitor.Name)
|
||||||
|
|
||||||
|
// resolve incident
|
||||||
|
monitor.incident.Message = "\n**Resolved** - " + time.Now().Format(DefaultTimeFormat) + "\n\n - - - \n\n" + monitor.incident.Message
|
||||||
|
monitor.incident.SetFixed()
|
||||||
|
monitor.incident.Send(monitor.config)
|
||||||
|
|
||||||
|
monitor.lastFailReason = ""
|
||||||
|
monitor.incident = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (monitor *Monitor) ValidateConfiguration() error {
|
||||||
|
if len(monitor.ExpectedBody) > 0 {
|
||||||
|
exp, err := regexp.Compile(monitor.ExpectedBody)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor.bodyRegexp = exp
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(monitor.ExpectedBody) == 0 && monitor.ExpectedStatusCode == 0 {
|
||||||
|
return errors.New("Nothing to check, both 'expected_body' and 'expected_status_code' fields empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if monitor.CheckInterval < 1 {
|
||||||
|
monitor.CheckInterval = DefaultInterval
|
||||||
|
}
|
||||||
|
|
||||||
|
monitor.Method = strings.ToUpper(monitor.Method)
|
||||||
|
switch monitor.Method {
|
||||||
|
case "GET", "POST", "DELETE", "OPTIONS", "HEAD":
|
||||||
|
break
|
||||||
|
case "":
|
||||||
|
monitor.Method = "GET"
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unsupported check method: %v", monitor.Method)
|
||||||
|
}
|
||||||
|
|
||||||
|
if monitor.ComponentID == 0 && monitor.MetricID == 0 {
|
||||||
|
return errors.New("component_id & metric_id are unset")
|
||||||
|
}
|
||||||
|
|
||||||
|
if monitor.Threshold <= 0 {
|
||||||
|
monitor.Threshold = 100
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
117
readme.md
117
readme.md
@@ -1,72 +1,63 @@
|
|||||||
Cachet Monitor plugin
|

|
||||||
=====================
|
|
||||||
|
|
||||||
This is a monitoring plugin for CachetHQ.
|
|
||||||
|
|
||||||
Features
|
Features
|
||||||
--------
|
--------
|
||||||
|
|
||||||
- [x] Creates & Resolves Incidents
|
- [x] Creates & Resolves Incidents
|
||||||
- [x] Posts monitor lag every second
|
- [x] Posts monitor lag to cachet graphs
|
||||||
- [x] Updates Component to Partial Outage
|
- [x] Updates Component to Partial Outage
|
||||||
- [x] Updates Component to Major Outage if in Partial Outage
|
- [x] Updates Component to Major Outage if already in Partial Outage (works well with distributed monitoring)
|
||||||
- [x] Can be run on multiple servers and geo regions
|
- [x] Can be run on multiple servers and geo regions
|
||||||
|
|
||||||
Docker Quickstart
|
|
||||||
-----------------
|
|
||||||
|
|
||||||
1. Create a configuration json
|
|
||||||
2.
|
|
||||||
```
|
|
||||||
docker run -d \
|
|
||||||
--name cachet-monitor \
|
|
||||||
-h cachet-monitor \
|
|
||||||
-v `pwd`/config.json:/etc/cachet-monitor.config.json \
|
|
||||||
castawaylabs/cachet-monitor
|
|
||||||
```
|
|
||||||
|
|
||||||
Configuration
|
Configuration
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"api_url": "https://demo.cachethq.io/api",
|
// URL for the API. Note: Must end with /api/v1
|
||||||
"api_token": "9yMHsdioQosnyVK4iCVR",
|
"api_url": "https://<cachet domain>/api/v1",
|
||||||
"monitors": [
|
// Your API token for Cachet
|
||||||
{
|
"api_token": "<cachet api token>",
|
||||||
"name": "nodegear frontend",
|
// optional, false default, set if your certificate is self-signed/untrusted
|
||||||
"url": "https://nodegear.io/ping",
|
"insecure_api": false,
|
||||||
"metric_id": 0,
|
"monitors": [{
|
||||||
"component_id": 0,
|
// required, friendly name for your monitor
|
||||||
"threshold": 80,
|
"name": "Name of your monitor",
|
||||||
"component_id": null,
|
// required, url to probe
|
||||||
"expected_status_code": 200
|
"url": "Ping URL",
|
||||||
}
|
// optional, http method (defaults GET)
|
||||||
]
|
"method": "get",
|
||||||
|
// self-signed ssl certificate
|
||||||
|
"strict_tls": true,
|
||||||
|
// seconds between checks
|
||||||
|
"interval": 10,
|
||||||
|
// post lag to cachet metric (graph)
|
||||||
|
// note either metric ID or component ID are required
|
||||||
|
"metric_id": <metric id>,
|
||||||
|
// post incidents to this component
|
||||||
|
"component_id": <component id>,
|
||||||
|
// If % of downtime is over this threshold, open an incident
|
||||||
|
"threshold": 80,
|
||||||
|
// optional, expected status code (either status code or body must be supplied)
|
||||||
|
"expected_status_code": 200,
|
||||||
|
// optional, regular expression to match body content
|
||||||
|
"expected_body": "P.*NG"
|
||||||
|
}],
|
||||||
|
// optional, system name to identify bot (uses hostname by default)
|
||||||
|
"system_name": "",
|
||||||
|
// optional, defaults to stdout
|
||||||
|
"log_path": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
*Notes:*
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
- `metric_id` is optional
|
1. Download binary from [release page](https://github.com/CastawayLabs/cachet-monitor/releases)
|
||||||
- `component_id` is optional
|
2. Create your configuration ([example](https://raw.githubusercontent.com/CastawayLabs/cachet-monitor/master/example.config.json))
|
||||||
- `threshold` is a percentage
|
3. `cachet-monitor -c /etc/cachet-monitor.config.json`
|
||||||
- `expected_status_code` is a http response code
|
|
||||||
- GET request will be performed on the `url`
|
|
||||||
|
|
||||||
How to run
|
pro tip: run in background using `nohup cachet-monitor 2>&1 > /var/log/cachet-monitor.log &`
|
||||||
----------
|
|
||||||
|
|
||||||
Example:
|
|
||||||
|
|
||||||
1. Set up [Go](https://golang.org)
|
|
||||||
2. `go install github.com/castawaylabs/cachet-monitor`
|
|
||||||
3. `cachet-monitor -c https://raw.githubusercontent.com/CastawayLabs/cachet-monitor/master/example.config.json`
|
|
||||||
|
|
||||||
Production:
|
|
||||||
|
|
||||||
1. Download the example config and save to `/etc/cachet-monitor.config.json`
|
|
||||||
2. Run in background: `nohup cachet-monitor 2>&1 > /var/log/cachet-monitor.log &`
|
|
||||||
|
|
||||||
```
|
```
|
||||||
Usage of cachet-monitor:
|
Usage of cachet-monitor:
|
||||||
@@ -78,7 +69,23 @@ Usage of cachet-monitor:
|
|||||||
Environment variables
|
Environment variables
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
| Name | Example Value | Description |
|
| Name | Example Value | Description |
|
||||||
| ------------ | --------------------------- | --------------------------- |
|
| ------------ | ------------------------------ | --------------------------- |
|
||||||
| CACHET_API | http://demo.cachethq.io/api | URL endpoint for cachet api |
|
| CACHET_API | http://demo.cachethq.io/api/v1 | URL endpoint for cachet api |
|
||||||
| CACHET_TOKEN | randomvalue | API Authentication token |
|
| CACHET_TOKEN | APIToken123 | API Authentication token |
|
||||||
|
| CACHET_DEV | 1 | Strips logging |
|
||||||
|
|
||||||
|
Vision and goals
|
||||||
|
----------------
|
||||||
|
|
||||||
|
We made this tool because we felt the need to have our own monitoring software (leveraging on Cachet).
|
||||||
|
The idea is a stateless program which collects data and pushes it to a central cachet instance.
|
||||||
|
|
||||||
|
This gives us power to have an army of geographically distributed loggers and reveal issues in both latency & downtime on client websites.
|
||||||
|
|
||||||
|
Package usage
|
||||||
|
-------------
|
||||||
|
|
||||||
|
When using `cachet-monitor` as a package in another program, you should follow what `cli/main.go` does. It is important to call `ValidateConfiguration` on `CachetMonitor` and all the monitors inside.
|
||||||
|
|
||||||
|
[API Documentation](https://godoc.org/github.com/CastawayLabs/cachet-monitor)
|
||||||
|
|||||||
BIN
screenshot.png
Normal file
BIN
screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 97 KiB |
124
stylesheets/github-light.css
Normal file
124
stylesheets/github-light.css
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2015 GitHub, Inc.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
.pl-c /* comment */ {
|
||||||
|
color: #969896;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */,
|
||||||
|
.pl-s .pl-v /* string variable */ {
|
||||||
|
color: #0086b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-e /* entity */,
|
||||||
|
.pl-en /* entity.name */ {
|
||||||
|
color: #795da3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-s .pl-s1 /* string source */,
|
||||||
|
.pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-ent /* entity.name.tag */ {
|
||||||
|
color: #63a35c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-k /* keyword, storage, storage.type */ {
|
||||||
|
color: #a71d5d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-pds /* punctuation.definition.string, string.regexp.character-class */,
|
||||||
|
.pl-s /* string */,
|
||||||
|
.pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */,
|
||||||
|
.pl-sr /* string.regexp */,
|
||||||
|
.pl-sr .pl-cce /* string.regexp constant.character.escape */,
|
||||||
|
.pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */,
|
||||||
|
.pl-sr .pl-sre /* string.regexp source.ruby.embedded */ {
|
||||||
|
color: #183691;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-v /* variable */ {
|
||||||
|
color: #ed6a43;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-id /* invalid.deprecated */ {
|
||||||
|
color: #b52a1d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-ii /* invalid.illegal */ {
|
||||||
|
background-color: #b52a1d;
|
||||||
|
color: #f8f8f8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-sr .pl-cce /* string.regexp constant.character.escape */ {
|
||||||
|
color: #63a35c;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-ml /* markup.list */ {
|
||||||
|
color: #693a17;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-mh /* markup.heading */,
|
||||||
|
.pl-mh .pl-en /* markup.heading entity.name */,
|
||||||
|
.pl-ms /* meta.separator */ {
|
||||||
|
color: #1d3e81;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-mq /* markup.quote */ {
|
||||||
|
color: #008080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-mi /* markup.italic */ {
|
||||||
|
color: #333;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-mb /* markup.bold */ {
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-md /* markup.deleted, meta.diff.header.from-file */ {
|
||||||
|
background-color: #ffecec;
|
||||||
|
color: #bd2c00;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-mi1 /* markup.inserted, meta.diff.header.to-file */ {
|
||||||
|
background-color: #eaffea;
|
||||||
|
color: #55a532;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-mdr /* meta.diff.range */ {
|
||||||
|
color: #795da3;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pl-mo /* meta.output */ {
|
||||||
|
color: #1d3e81;
|
||||||
|
}
|
||||||
|
|
||||||
424
stylesheets/normalize.css
vendored
Normal file
424
stylesheets/normalize.css
vendored
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Set default font family to sans-serif.
|
||||||
|
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||||
|
* user zoom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif; /* 1 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove default margin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display definitions
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||||
|
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||||
|
* and Firefox.
|
||||||
|
* Correct `block` display not defined for `main` in IE 11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
main,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section,
|
||||||
|
summary {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||||
|
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
canvas,
|
||||||
|
progress,
|
||||||
|
video {
|
||||||
|
display: inline-block; /* 1 */
|
||||||
|
vertical-align: baseline; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent modern browsers from displaying `audio` without controls.
|
||||||
|
* Remove excess height in iOS 5 devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||||
|
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden],
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background color from active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Improve readability when focused and also mouse hovered in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:active,
|
||||||
|
a:hover {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: 1px dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in Safari and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dfn {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address variable `h1` font-size and margin within `section` and `article`
|
||||||
|
* contexts in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
margin: 0.67em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 8/9.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background: #ff0;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent and variable font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove border when inside `a` element in IE 8/9/10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct overflow not hidden in IE 9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
svg:not(:root) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address margin not present in IE 8/9 and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 1em 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address differences between Firefox and other browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
box-sizing: content-box;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contain overflow in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address odd `em`-unit font size rendering in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
pre,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||||
|
* styling of `select`, unless a `border` property is set.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct color not being inherited.
|
||||||
|
* Known issue: affects color of disabled elements.
|
||||||
|
* 2. Correct font properties not being inherited.
|
||||||
|
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
color: inherit; /* 1 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
margin: 0; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||||
|
* All other form control elements do not inherit `text-transform` values.
|
||||||
|
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||||
|
* Correct `select` style inheritance in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||||
|
* and `video` controls.
|
||||||
|
* 2. Correct inability to style clickable `input` types in iOS.
|
||||||
|
* 3. Improve usability and consistency of cursor style between image-type
|
||||||
|
* `input` and others.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html input[type="button"], /* 1 */
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="submit"] {
|
||||||
|
-webkit-appearance: button; /* 2 */
|
||||||
|
cursor: pointer; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-set default cursor for disabled elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button[disabled],
|
||||||
|
html input[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and border in Firefox 4+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
border: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||||
|
* the UA stylesheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It's recommended that you don't attempt to style these elements.
|
||||||
|
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||||
|
*
|
||||||
|
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||||
|
* 2. Remove excess padding in IE 8/9/10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="checkbox"],
|
||||||
|
input[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||||
|
* `font-size` values of the `input`, it causes the cursor style of the
|
||||||
|
* decrement button to change from `default` to `text`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||||
|
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||||
|
* (include `-moz` to future-proof).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"] {
|
||||||
|
-webkit-appearance: textfield; /* 1 */ /* 2 */
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||||
|
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||||
|
* padding (and `textfield` appearance).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define consistent border, margin, and padding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
margin: 0 2px;
|
||||||
|
padding: 0.35em 0.625em 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||||
|
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
border: 0; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't inherit the `font-weight` (applied by a rule above).
|
||||||
|
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||||
|
*/
|
||||||
|
|
||||||
|
optgroup {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove most spacing between table cells.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
228
stylesheets/print.css
Normal file
228
stylesheets/print.css
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
html, body, div, span, applet, object, iframe,
|
||||||
|
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||||
|
a, abbr, acronym, address, big, cite, code,
|
||||||
|
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||||
|
small, strike, strong, sub, sup, tt, var,
|
||||||
|
b, u, i, center,
|
||||||
|
dl, dt, dd, ol, ul, li,
|
||||||
|
fieldset, form, label, legend,
|
||||||
|
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||||
|
article, aside, canvas, details, embed,
|
||||||
|
figure, figcaption, footer, header, hgroup,
|
||||||
|
menu, nav, output, ruby, section, summary,
|
||||||
|
time, mark, audio, video {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 100%;
|
||||||
|
vertical-align: baseline;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
/* HTML5 display-role reset for older browsers */
|
||||||
|
article, aside, details, figcaption, figure,
|
||||||
|
footer, header, hgroup, menu, nav, section {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
ol, ul {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
blockquote, q {
|
||||||
|
quotes: none;
|
||||||
|
}
|
||||||
|
blockquote:before, blockquote:after,
|
||||||
|
q:before, q:after {
|
||||||
|
content: '';
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #d5000d;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding-top: 35px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
font-size: 48px;
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #303030;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #aaa;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
#downloads {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#main_content {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
code, pre {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
border: solid 1px #ddd;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol, dl {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* COMMON STYLES */
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f2f2f2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* GENERAL ELEMENT TYPE STYLES */
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #d5000d;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1em;
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: .8em;
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p a {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
padding: 0 0 0 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.6em;
|
||||||
|
border-left: 10px solid #e9e9e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul li {
|
||||||
|
padding-left: 20px;
|
||||||
|
list-style-position: inside;
|
||||||
|
list-style: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol li {
|
||||||
|
padding-left: 3px;
|
||||||
|
list-style-position: inside;
|
||||||
|
list-style: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl dd {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
margin-top: 40px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MISC */
|
||||||
|
.clearfix:after {
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
clear: both;
|
||||||
|
visibility: hidden;
|
||||||
|
content: '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix {display: inline-block;}
|
||||||
|
* html .clearfix {height: 1%;}
|
||||||
|
.clearfix {display: block;}
|
||||||
881
stylesheets/stylesheet.css
Normal file
881
stylesheets/stylesheet.css
Normal file
@@ -0,0 +1,881 @@
|
|||||||
|
/*! normalize.css v3.0.2 | MIT License | git.io/normalize */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Set default font family to sans-serif.
|
||||||
|
* 2. Prevent iOS text size adjust after orientation change, without disabling
|
||||||
|
* user zoom.
|
||||||
|
*/
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-family: sans-serif; /* 1 */
|
||||||
|
-webkit-text-size-adjust: 100%; /* 2 */
|
||||||
|
-ms-text-size-adjust: 100%; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove default margin.
|
||||||
|
*/
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* HTML5 display definitions
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||||
|
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||||
|
* and Firefox.
|
||||||
|
* Correct `block` display not defined for `main` in IE 11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
article,
|
||||||
|
aside,
|
||||||
|
details,
|
||||||
|
figcaption,
|
||||||
|
figure,
|
||||||
|
footer,
|
||||||
|
header,
|
||||||
|
hgroup,
|
||||||
|
main,
|
||||||
|
menu,
|
||||||
|
nav,
|
||||||
|
section,
|
||||||
|
summary {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||||
|
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio,
|
||||||
|
canvas,
|
||||||
|
progress,
|
||||||
|
video {
|
||||||
|
display: inline-block; /* 1 */
|
||||||
|
vertical-align: baseline; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent modern browsers from displaying `audio` without controls.
|
||||||
|
* Remove excess height in iOS 5 devices.
|
||||||
|
*/
|
||||||
|
|
||||||
|
audio:not([controls]) {
|
||||||
|
display: none;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||||
|
* Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.
|
||||||
|
*/
|
||||||
|
|
||||||
|
[hidden],
|
||||||
|
template {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Links
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the gray background color from active links in IE 10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Improve readability when focused and also mouse hovered in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
a:active,
|
||||||
|
a:hover {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text-level semantics
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
abbr[title] {
|
||||||
|
border-bottom: 1px dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in Safari and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
dfn {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address variable `h1` font-size and margin within `section` and `article`
|
||||||
|
* contexts in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin: 0.67em 0;
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address styling not present in IE 8/9.
|
||||||
|
*/
|
||||||
|
|
||||||
|
mark {
|
||||||
|
color: #000;
|
||||||
|
background: #ff0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent and variable font size in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
small {
|
||||||
|
font-size: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
position: relative;
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Embedded content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove border when inside `a` element in IE 8/9/10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Correct overflow not hidden in IE 9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
svg:not(:root) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Grouping content
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address margin not present in IE 8/9 and Safari.
|
||||||
|
*/
|
||||||
|
|
||||||
|
figure {
|
||||||
|
margin: 1em 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address differences between Firefox and other browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contain overflow in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
pre {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address odd `em`-unit font size rendering in all browsers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
pre,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Forms
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||||
|
* styling of `select`, unless a `border` property is set.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct color not being inherited.
|
||||||
|
* Known issue: affects color of disabled elements.
|
||||||
|
* 2. Correct font properties not being inherited.
|
||||||
|
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
input,
|
||||||
|
optgroup,
|
||||||
|
select,
|
||||||
|
textarea {
|
||||||
|
margin: 0; /* 3 */
|
||||||
|
font: inherit; /* 2 */
|
||||||
|
color: inherit; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button {
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||||
|
* All other form control elements do not inherit `text-transform` values.
|
||||||
|
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||||
|
* Correct `select` style inheritance in Firefox.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
select {
|
||||||
|
text-transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||||
|
* and `video` controls.
|
||||||
|
* 2. Correct inability to style clickable `input` types in iOS.
|
||||||
|
* 3. Improve usability and consistency of cursor style between image-type
|
||||||
|
* `input` and others.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button,
|
||||||
|
html input[type="button"], /* 1 */
|
||||||
|
input[type="reset"],
|
||||||
|
input[type="submit"] {
|
||||||
|
-webkit-appearance: button; /* 2 */
|
||||||
|
cursor: pointer; /* 3 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-set default cursor for disabled elements.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button[disabled],
|
||||||
|
html input[disabled] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and border in Firefox 4+.
|
||||||
|
*/
|
||||||
|
|
||||||
|
button::-moz-focus-inner,
|
||||||
|
input::-moz-focus-inner {
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Address Firefox 4+ setting `line-height` on `input` using `!important` in
|
||||||
|
* the UA stylesheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input {
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* It's recommended that you don't attempt to style these elements.
|
||||||
|
* Firefox's implementation doesn't respect box-sizing, padding, or width.
|
||||||
|
*
|
||||||
|
* 1. Address box sizing set to `content-box` in IE 8/9/10.
|
||||||
|
* 2. Remove excess padding in IE 8/9/10.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="checkbox"],
|
||||||
|
input[type="radio"] {
|
||||||
|
box-sizing: border-box; /* 1 */
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fix the cursor style for Chrome's increment/decrement buttons. For certain
|
||||||
|
* `font-size` values of the `input`, it causes the cursor style of the
|
||||||
|
* decrement button to change from `default` to `text`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="number"]::-webkit-inner-spin-button,
|
||||||
|
input[type="number"]::-webkit-outer-spin-button {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Address `appearance` set to `searchfield` in Safari and Chrome.
|
||||||
|
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome
|
||||||
|
* (include `-moz` to future-proof).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"] {
|
||||||
|
-webkit-box-sizing: content-box; /* 2 */
|
||||||
|
-moz-box-sizing: content-box;
|
||||||
|
box-sizing: content-box;
|
||||||
|
-webkit-appearance: textfield; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove inner padding and search cancel button in Safari and Chrome on OS X.
|
||||||
|
* Safari (but not Chrome) clips the cancel button when the search input has
|
||||||
|
* padding (and `textfield` appearance).
|
||||||
|
*/
|
||||||
|
|
||||||
|
input[type="search"]::-webkit-search-cancel-button,
|
||||||
|
input[type="search"]::-webkit-search-decoration {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define consistent border, margin, and padding.
|
||||||
|
*/
|
||||||
|
|
||||||
|
fieldset {
|
||||||
|
padding: 0.35em 0.625em 0.75em;
|
||||||
|
margin: 0 2px;
|
||||||
|
border: 1px solid #c0c0c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. Correct `color` not being inherited in IE 8/9/10/11.
|
||||||
|
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
|
||||||
|
*/
|
||||||
|
|
||||||
|
legend {
|
||||||
|
padding: 0; /* 2 */
|
||||||
|
border: 0; /* 1 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove default vertical scrollbar in IE 8/9/10/11.
|
||||||
|
*/
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Don't inherit the `font-weight` (applied by a rule above).
|
||||||
|
* NOTE: the default cannot safely be changed in Chrome and Safari on OS X.
|
||||||
|
*/
|
||||||
|
|
||||||
|
optgroup {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tables
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove most spacing between table cells.
|
||||||
|
*/
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-spacing: 0;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
td,
|
||||||
|
th {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* LAYOUT STYLES */
|
||||||
|
body {
|
||||||
|
font-family: 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #666;
|
||||||
|
background: #fafafa url(../images/body-bg.jpg) 0 0 repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #2879d0;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #2268b2;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
padding-top: 40px;
|
||||||
|
padding-bottom: 40px;
|
||||||
|
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
background: #2e7bcf url(../images/header-bg.jpg) 0 0 repeat-x;
|
||||||
|
border-bottom: solid 1px #275da1;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
width: 540px;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0.2em;
|
||||||
|
font-size: 72px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h2 {
|
||||||
|
width: 540px;
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: #9ddcff;
|
||||||
|
letter-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inner {
|
||||||
|
position: relative;
|
||||||
|
width: 940px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#content-wrapper {
|
||||||
|
padding-top: 30px;
|
||||||
|
border-top: solid 1px #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content {
|
||||||
|
float: left;
|
||||||
|
width: 690px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside#sidebar {
|
||||||
|
float: right;
|
||||||
|
width: 200px;
|
||||||
|
min-height: 504px;
|
||||||
|
padding-left: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.3;
|
||||||
|
background: transparent url(../images/sidebar-bg.jpg) 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
aside#sidebar p.repo-owner,
|
||||||
|
aside#sidebar p.repo-owner a {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#downloads {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a.button {
|
||||||
|
width: 134px;
|
||||||
|
height: 58px;
|
||||||
|
padding-top: 22px;
|
||||||
|
padding-left: 68px;
|
||||||
|
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
font-size: 23px;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
a.button small {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
header a.button {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
background: transparent url(../images/github-button.png) 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
aside a.button {
|
||||||
|
display: block;
|
||||||
|
width: 138px;
|
||||||
|
padding-left: 64px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 21px;
|
||||||
|
background: transparent url(../images/download-button.png) 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
code, pre {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-family: Monaco, "Bitstream Vera Sans Mono", "Lucida Console", Terminal, monospace;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding: 0 3px;
|
||||||
|
background-color: #f2f8fc;
|
||||||
|
border: solid 1px #dbe7f3;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
padding: 20px;
|
||||||
|
overflow: auto;
|
||||||
|
text-shadow: none;
|
||||||
|
background: #fff;
|
||||||
|
border: solid 1px #f2f2f2;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
padding: 0;
|
||||||
|
color: #2879d0;
|
||||||
|
background-color: #fff;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol, dl {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* COMMON STYLES */
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 0;
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 1em;
|
||||||
|
border: 0;
|
||||||
|
border-top: solid 1px #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
font-weight: 300;
|
||||||
|
text-align: center;
|
||||||
|
border: 1px solid #ebebeb;
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
padding: 20px;
|
||||||
|
background: #f2f2f2;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* GENERAL ELEMENT TYPE STYLES */
|
||||||
|
|
||||||
|
#main-content h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
font-size: 2.8em;
|
||||||
|
font-weight: normal;
|
||||||
|
color: #474747;
|
||||||
|
text-indent: 6px;
|
||||||
|
letter-spacing: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content h1:before {
|
||||||
|
padding-right: 0.3em;
|
||||||
|
margin-left: -0.9em;
|
||||||
|
color: #9ddcff;
|
||||||
|
content: "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content h2 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #474747;
|
||||||
|
text-indent: 4px;
|
||||||
|
}
|
||||||
|
#main-content h2:before {
|
||||||
|
padding-right: 0.3em;
|
||||||
|
margin-left: -1.5em;
|
||||||
|
content: "//";
|
||||||
|
color: #9ddcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content h3 {
|
||||||
|
margin-top: 24px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #474747;
|
||||||
|
text-indent: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content h3:before {
|
||||||
|
padding-right: 0.3em;
|
||||||
|
margin-left: -2em;
|
||||||
|
content: "///";
|
||||||
|
color: #9ddcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content h4 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #474747;
|
||||||
|
text-indent: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4:before {
|
||||||
|
padding-right: 0.3em;
|
||||||
|
margin-left: -2.8em;
|
||||||
|
content: "////";
|
||||||
|
color: #9ddcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content h5 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #474747;
|
||||||
|
text-indent: 3px;
|
||||||
|
}
|
||||||
|
h5:before {
|
||||||
|
padding-right: 0.3em;
|
||||||
|
margin-left: -3.2em;
|
||||||
|
content: "/////";
|
||||||
|
color: #9ddcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
#main-content h6 {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-family: 'Architects Daughter', 'Helvetica Neue', Helvetica, Arial, serif;
|
||||||
|
font-size: .8em;
|
||||||
|
color: #474747;
|
||||||
|
text-indent: 3px;
|
||||||
|
}
|
||||||
|
h6:before {
|
||||||
|
padding-right: 0.3em;
|
||||||
|
margin-left: -3.7em;
|
||||||
|
content: "//////";
|
||||||
|
color: #9ddcff;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
p a {
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
padding: 0 0 0 30px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 1.6em;
|
||||||
|
border-left: 10px solid #e9e9e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-position: inside;
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style-position: inside;
|
||||||
|
list-style: decimal;
|
||||||
|
padding-left: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
dl dd {
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
padding-top: 20px;
|
||||||
|
padding-bottom: 30px;
|
||||||
|
margin-top: 40px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #aaa;
|
||||||
|
background: transparent url('../images/hr.png') 0 0 no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
footer a:hover {
|
||||||
|
color: #444;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* MISC */
|
||||||
|
.clearfix:after {
|
||||||
|
display: block;
|
||||||
|
height: 0;
|
||||||
|
clear: both;
|
||||||
|
visibility: hidden;
|
||||||
|
content: '.';
|
||||||
|
}
|
||||||
|
|
||||||
|
.clearfix {display: inline-block;}
|
||||||
|
* html .clearfix {height: 1%;}
|
||||||
|
.clearfix {display: block;}
|
||||||
|
|
||||||
|
/* #Media Queries
|
||||||
|
================================================== */
|
||||||
|
|
||||||
|
/* Smaller than standard 960 (devices and browsers) */
|
||||||
|
@media only screen and (max-width: 959px) { }
|
||||||
|
|
||||||
|
/* Tablet Portrait size to standard 960 (devices and browsers) */
|
||||||
|
@media only screen and (min-width: 768px) and (max-width: 959px) {
|
||||||
|
.inner {
|
||||||
|
width: 740px;
|
||||||
|
}
|
||||||
|
header h1, header h2 {
|
||||||
|
width: 340px;
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
header h2 {
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
#main-content {
|
||||||
|
width: 490px;
|
||||||
|
}
|
||||||
|
#main-content h1:before,
|
||||||
|
#main-content h2:before,
|
||||||
|
#main-content h3:before,
|
||||||
|
#main-content h4:before,
|
||||||
|
#main-content h5:before,
|
||||||
|
#main-content h6:before {
|
||||||
|
padding-right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* All Mobile Sizes (devices and browser) */
|
||||||
|
@media only screen and (max-width: 767px) {
|
||||||
|
.inner {
|
||||||
|
width: 93%;
|
||||||
|
}
|
||||||
|
header {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
header .inner {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
header h1, header h2 {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
header h1 {
|
||||||
|
font-size: 48px;
|
||||||
|
}
|
||||||
|
header h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
header a.button {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
padding: 5px 10px;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1;
|
||||||
|
color: #2879d0;
|
||||||
|
text-align: center;
|
||||||
|
background-color: #9ddcff;
|
||||||
|
background-image: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
-moz-border-radius: 5px;
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
}
|
||||||
|
header a.button small {
|
||||||
|
display: inline;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
#main-content,
|
||||||
|
aside#sidebar {
|
||||||
|
float: none;
|
||||||
|
width: 100% ! important;
|
||||||
|
}
|
||||||
|
aside#sidebar {
|
||||||
|
min-height: 0;
|
||||||
|
padding: 20px 0;
|
||||||
|
margin-top: 20px;
|
||||||
|
background-image: none;
|
||||||
|
border-top: solid 1px #ddd;
|
||||||
|
}
|
||||||
|
aside#sidebar a.button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
#main-content h1:before,
|
||||||
|
#main-content h2:before,
|
||||||
|
#main-content h3:before,
|
||||||
|
#main-content h4:before,
|
||||||
|
#main-content h5:before,
|
||||||
|
#main-content h6:before {
|
||||||
|
padding-right: 0;
|
||||||
|
margin-left: 0;
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Landscape Size to Tablet Portrait (devices and browsers) */
|
||||||
|
@media only screen and (min-width: 480px) and (max-width: 767px) { }
|
||||||
|
|
||||||
|
/* Mobile Portrait Size to Mobile Landscape Size (devices and browsers) */
|
||||||
|
@media only screen and (max-width: 479px) { }
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package system
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
// GetHostname returns id of the current system
|
|
||||||
func GetHostname() string {
|
|
||||||
hostname, err := os.Hostname()
|
|
||||||
if err != nil || len(hostname) == 0 {
|
|
||||||
addrs, err := net.InterfaceAddrs()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "unknown"
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
return addr.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hostname
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user