This commit is contained in:
Alex Dadgar
2018-09-10 15:03:17 -07:00
parent 8ebb17f54d
commit 56f9607c1a
24 changed files with 657 additions and 646 deletions

View File

@@ -32,6 +32,8 @@ func main() {
cfg.CheckManager.API.TokenKey = ""
cfg.CheckManager.API.TokenApp = "circonus-gometrics"
cfg.CheckManager.API.TokenURL = "https://api.circonus.com/v2"
cfg.CheckManager.API.CACert = nil
cfg.CheckManager.API.TLSConfig = nil
// Check
_, an := path.Split(os.Args[0])
@@ -51,6 +53,7 @@ func main() {
cfg.CheckManager.Broker.ID = ""
cfg.CheckManager.Broker.SelectTag = ""
cfg.CheckManager.Broker.MaxResponseTime = "500ms"
cfg.CheckManager.Broker.TLSConfig = nil
// create a new cgm instance and start sending metrics...
// see the complete example in the main README.
@@ -72,9 +75,11 @@ func main() {
| `cfg.CheckManager.API.TokenKey` | "" | [Circonus API Token key](https://login.circonus.com/user/tokens) |
| `cfg.CheckManager.API.TokenApp` | "circonus-gometrics" | App associated with API token |
| `cfg.CheckManager.API.URL` | "https://api.circonus.com/v2" | Circonus API URL |
| `cfg.CheckManager.API.TLSConfig` | nil | Custom tls.Config to use when communicating with Circonus API |
| `cfg.CheckManager.API.CACert` | nil | DEPRECATED - use TLSConfig ~~[*x509.CertPool](https://golang.org/pkg/crypto/x509/#CertPool) with CA Cert to validate API endpoint using internal CA or self-signed certificates~~ |
|Check||
| `cfg.CheckManager.Check.ID` | "" | Check ID of previously created check. (*Note: **check id** not **check bundle id**.*) |
| `cfg.CheckManager.Check.SubmissionURL` | "" | Submission URL of previously created check. |
| `cfg.CheckManager.Check.SubmissionURL` | "" | Submission URL of previously created check. Metrics can also be sent to a local [circonus-agent](https://github.com/circonus-labs/circonus-agent) by using the agent's URL (e.g. `http://127.0.0.1:2609/write/appid` where `appid` is a unique identifier for the application which will prefix all metrics. Additionally, the circonus-agent can optionally listen for requests to `/write` on a unix socket - to leverage this feature, use a URL such as `http+unix:///path/to/socket_file/write/appid`). |
| `cfg.CheckManager.Check.InstanceID` | hostname:program name | An identifier for the 'group of metrics emitted by this process or service'. |
| `cfg.CheckManager.Check.TargetHost` | InstanceID | Explicit setting of `check.target`. |
| `cfg.CheckManager.Check.DisplayName` | InstanceID | Custom `check.display_name`. Shows in UI check list. |
@@ -87,6 +92,7 @@ func main() {
| `cfg.CheckManager.Broker.ID` | "" | ID of a specific broker to use when creating a check. Default is to use a random enterprise broker or the public Circonus default broker. |
| `cfg.CheckManager.Broker.SelectTag` | "" | Used to select a broker with the same tag(s). If more than one broker has the tag(s), one will be selected randomly from the resulting list. (e.g. could be used to select one from a list of brokers serving a specific colo/region. "dc:sfo", "loc:nyc,dc:nyc01", "zone:us-west") |
| `cfg.CheckManager.Broker.MaxResponseTime` | "500ms" | Maximum amount time to wait for a broker connection test to be considered valid. (if latency is > the broker will be considered invalid and not available for selection.) |
| `cfg.CheckManager.Broker.TLSConfig` | nil | Custom tls.Config to use when communicating with Circonus Broker |
## Notes:

View File

@@ -35,7 +35,7 @@ func main() {
logger.Println("Configuring cgm")
cmc := &cgm.Config{}
cmc.Debug := false // set to true for debug messages
cmc.Debug = false // set to true for debug messages
cmc.Log = logger
// Circonus API Token key (https://login.circonus.com/user/tokens)
@@ -122,9 +122,10 @@ func main() {
cmc.CheckManager.API.TokenKey = os.Getenv("CIRCONUS_API_TOKEN")
cmc.CheckManager.API.TokenApp = os.Getenv("CIRCONUS_API_APP")
cmc.CheckManager.API.URL = os.Getenv("CIRCONUS_API_URL")
cmc.CheckManager.API.TLSConfig = nil
// Check configuration options
cmc.CheckManager.Check.SubmissionURL = os.Getenv("CIRCONUS_SUBMISION_URL")
cmc.CheckManager.Check.SubmissionURL = os.Getenv("CIRCONUS_SUBMISSION_URL")
cmc.CheckManager.Check.ID = os.Getenv("CIRCONUS_CHECK_ID")
cmc.CheckManager.Check.InstanceID = ""
cmc.CheckManager.Check.DisplayName = ""
@@ -142,6 +143,7 @@ func main() {
cmc.CheckManager.Broker.ID = ""
cmc.CheckManager.Broker.SelectTag = ""
cmc.CheckManager.Broker.MaxResponseTime = "500ms"
cmc.CheckManager.Broker.TLSConfig = nil
logger.Println("Creating new cgm instance")
@@ -230,3 +232,5 @@ func main() {
```
Unless otherwise noted, the source files are distributed under the BSD-style license found in the LICENSE file.
[![codecov](https://codecov.io/gh/maier/circonus-gometrics/branch/master/graph/badge.svg)](https://codecov.io/gh/maier/circonus-gometrics)

View File

@@ -6,7 +6,10 @@ package api
import (
"bytes"
"context"
crand "crypto/rand"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
@@ -14,10 +17,10 @@ import (
"math"
"math/big"
"math/rand"
"net"
"net/http"
"net/url"
"os"
"regexp"
"strings"
"sync"
"time"
@@ -49,6 +52,9 @@ type TokenKeyType string
// TokenAppType - Circonus API Token app name
type TokenAppType string
// TokenAccountIDType - Circonus API Token account id
type TokenAccountIDType string
// CIDType Circonus object cid
type CIDType *string
@@ -69,11 +75,25 @@ type TagType []string
// Config options for Circonus API
type Config struct {
URL string
// URL defines the API URL - default https://api.circonus.com/v2/
URL string
// TokenKey defines the key to use when communicating with the API
TokenKey string
// TokenApp defines the app to use when communicating with the API
TokenApp string
Log *log.Logger
Debug bool
TokenAccountID string
// CACert deprecating, use TLSConfig instead
CACert *x509.CertPool
// TLSConfig defines a custom tls configuration to use when communicating with the API
TLSConfig *tls.Config
Log *log.Logger
Debug bool
}
// API Circonus API
@@ -81,6 +101,9 @@ type API struct {
apiURL *url.URL
key TokenKeyType
app TokenAppType
accountID TokenAccountIDType
caCert *x509.CertPool
tlsConfig *tls.Config
Debug bool
Log *log.Logger
useExponentialBackoff bool
@@ -114,6 +137,8 @@ func New(ac *Config) (*API, error) {
app = defaultAPIApp
}
acctID := TokenAccountIDType(ac.TokenAccountID)
au := string(ac.URL)
if au == "" {
au = defaultAPIURL
@@ -132,11 +157,14 @@ func New(ac *Config) (*API, error) {
}
a := &API{
apiURL: apiURL,
key: key,
app: app,
Debug: ac.Debug,
Log: ac.Log,
apiURL: apiURL,
key: key,
app: app,
accountID: acctID,
caCert: ac.CACert,
tlsConfig: ac.TLSConfig,
Debug: ac.Debug,
Log: ac.Log,
useExponentialBackoff: false,
}
@@ -213,7 +241,7 @@ func (a *API) apiRequest(reqMethod string, reqPath string, data []byte) ([]byte,
if !a.useExponentialBackoff {
break
}
if matched, _ := regexp.MatchString("code 403", err.Error()); matched {
if strings.Contains(err.Error(), "code 403") {
break
}
}
@@ -245,14 +273,18 @@ func (a *API) apiCall(reqMethod string, reqPath string, data []byte) ([]byte, er
reqURL += "/"
}
if len(reqPath) >= 3 && reqPath[:3] == "/v2" {
reqURL += reqPath[3:len(reqPath)]
reqURL += reqPath[3:]
} else {
reqURL += reqPath
}
// keep last HTTP error in the event of retry failure
var lastHTTPError error
retryPolicy := func(resp *http.Response, err error) (bool, error) {
retryPolicy := func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if ctxErr := ctx.Err(); ctxErr != nil {
return false, ctxErr
}
if err != nil {
lastHTTPError = err
return true, err
@@ -285,8 +317,44 @@ func (a *API) apiCall(reqMethod string, reqPath string, data []byte) ([]byte, er
req.Header.Add("Accept", "application/json")
req.Header.Add("X-Circonus-Auth-Token", string(a.key))
req.Header.Add("X-Circonus-App-Name", string(a.app))
if string(a.accountID) != "" {
req.Header.Add("X-Circonus-Account-ID", string(a.accountID))
}
client := retryablehttp.NewClient()
if a.apiURL.Scheme == "https" {
var tlscfg *tls.Config
if a.tlsConfig != nil { // preference full custom tls config
tlscfg = a.tlsConfig
} else if a.caCert != nil {
tlscfg = &tls.Config{RootCAs: a.caCert}
}
client.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
TLSClientConfig: tlscfg,
DisableKeepAlives: true,
MaxIdleConnsPerHost: -1,
DisableCompression: true,
}
} else {
client.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
DisableKeepAlives: true,
MaxIdleConnsPerHost: -1,
DisableCompression: true,
}
}
a.useExponentialBackoffmu.Lock()
eb := a.useExponentialBackoff
a.useExponentialBackoffmu.Unlock()

View File

@@ -38,7 +38,7 @@ type CheckBundle struct {
Checks []string `json:"_checks,omitempty"` // [] len >= 0
CheckUUIDs []string `json:"_check_uuids,omitempty"` // [] len >= 0
CID string `json:"_cid,omitempty"` // string
Config CheckBundleConfig `json:"config,omitempty"` // NOTE contents of config are check type specific, map len >= 0
Config CheckBundleConfig `json:"config"` // NOTE contents of config are check type specific, map len >= 0
Created uint `json:"_created,omitempty"` // uint
DisplayName string `json:"display_name"` // string
LastModifedBy string `json:"_last_modifed_by,omitempty"` // string

View File

@@ -1,390 +0,0 @@
{
"_active": true,
"_cid": "/dashboard/1234",
"_created": 1483193930,
"_created_by": "/user/1234",
"_dashboard_uuid": "01234567-89ab-cdef-0123-456789abcdef",
"_last_modified": 1483450351,
"account_default": false,
"grid_layout": {
"height": 4,
"width": 4
},
"options": {
"access_configs": [
],
"fullscreen_hide_title": false,
"hide_grid": false,
"linkages": [
],
"scale_text": true,
"text_size": 16
},
"shared": false,
"title": "foo bar baz",
"widgets": [
{
"active": true,
"height": 1,
"name": "Cluster",
"origin": "d0",
"settings": {
"account_id": "1234",
"algorithm": "cor",
"cluster_id": 1234,
"cluster_name": "test",
"layout": "compact",
"size": "medium",
"threshold": 0.7
},
"type": "cluster",
"widget_id": "w4",
"width": 1
},
{
"active": true,
"height": 1,
"name": "HTML",
"origin": "d1",
"settings": {
"markup": "<h1>foo</h1>",
"title": "html"
},
"type": "html",
"widget_id": "w9",
"width": 1
},
{
"active": true,
"height": 1,
"name": "Chart",
"origin": "c0",
"settings": {
"chart_type": "bar",
"datapoints": [
{
"_check_id": 1234,
"_metric_type": "numeric",
"account_id": "1234",
"label": "Used",
"metric": "01234567-89ab-cdef-0123-456789abcdef:vm`memory`used"
},
{
"_check_id": 1234,
"_metric_type": "numeric",
"account_id": "1234",
"label": "Free",
"metric": "01234567-89ab-cdef-0123-456789abcdef:vm`memory`free"
}
],
"definition": {
"datasource": "realtime",
"derive": "gauge",
"disable_autoformat": false,
"formula": "",
"legend": {
"show": false,
"type": "html"
},
"period": 0,
"pop_onhover": false,
"wedge_labels": {
"on_chart": true,
"tooltips": false
},
"wedge_values": {
"angle": "0",
"color": "background",
"show": true
}
},
"title": "chart graph"
},
"type": "chart",
"widget_id": "w5",
"width": 1
},
{
"active": true,
"height": 1,
"name": "Alerts",
"origin": "a0",
"settings": {
"account_id": "1234",
"acknowledged": "all",
"cleared": "all",
"contact_groups": [
],
"dependents": "all",
"display": "list",
"maintenance": "all",
"min_age": "0",
"off_hours": [
17,
9
],
"search": "",
"severity": "12345",
"tag_filter_set": [
],
"time_window": "30M",
"title": "alerts",
"week_days": [
"sun",
"mon",
"tue",
"wed",
"thu",
"fri",
"sat"
]
},
"type": "alerts",
"widget_id": "w2",
"width": 1
},
{
"active": true,
"height": 1,
"name": "Graph",
"origin": "c1",
"settings": {
"_graph_title": "foo bar / %Used",
"account_id": "1234",
"date_window": "2w",
"graph_id": "01234567-89ab-cdef-0123-456789abcdef",
"hide_xaxis": false,
"hide_yaxis": false,
"key_inline": false,
"key_loc": "noop",
"key_size": "1",
"key_wrap": false,
"label": "",
"overlay_set_id": "",
"period": "2000",
"previous_graph_id": "null",
"realtime": false,
"show_flags": false
},
"type": "graph",
"widget_id": "w8",
"width": 1
},
{
"active": true,
"height": 1,
"name": "List",
"origin": "a2",
"settings": {
"account_id": "1234",
"limit": "10",
"search": "",
"type": "graph"
},
"type": "list",
"widget_id": "w10",
"width": 1
},
{
"active": true,
"height": 1,
"name": "Status",
"origin": "b2",
"settings": {
"account_id": "1234",
"agent_status_settings": {
"search": "",
"show_agent_types": "both",
"show_contact": false,
"show_feeds": true,
"show_setup": false,
"show_skew": true,
"show_updates": true
},
"content_type": "agent_status",
"host_status_settings": {
"layout_style": "grid",
"search": "",
"sort_by": "alerts",
"tag_filter_set": [
]
}
},
"type": "status",
"widget_id": "w11",
"width": 1
},
{
"active": true,
"height": 1,
"name": "Text",
"origin": "d2",
"settings": {
"autoformat": false,
"body_format": "<p>{metric_name} ({value_type})<br /><strong>{metric_value}</strong><br /><span class=\"date\">{value_date}</span></p>",
"datapoints": [
{
"_cluster_title": "test",
"_label": "Cluster: test",
"account_id": "1234",
"cluster_id": 1234,
"numeric_only": false
}
],
"period": 0,
"title_format": "Metric Status",
"use_default": true,
"value_type": "gauge"
},
"type": "text",
"widget_id": "w13",
"width": 1
},
{
"active": true,
"height": 1,
"name": "Chart",
"origin": "b0",
"settings": {
"chart_type": "bar",
"datapoints": [
{
"_cluster_title": "test",
"_label": "Cluster: test",
"account_id": "1234",
"cluster_id": 1234,
"numeric_only": true
}
],
"definition": {
"datasource": "realtime",
"derive": "gauge",
"disable_autoformat": false,
"formula": "",
"legend": {
"show": false,
"type": "html"
},
"period": 0,
"pop_onhover": false,
"wedge_labels": {
"on_chart": true,
"tooltips": false
},
"wedge_values": {
"angle": "0",
"color": "background",
"show": true
}
},
"title": "chart metric cluster"
},
"type": "chart",
"widget_id": "w3",
"width": 1
},
{
"active": true,
"height": 1,
"name": "Gauge",
"origin": "b1",
"settings": {
"_check_id": 1234,
"account_id": "1234",
"check_uuid": "01234567-89ab-cdef-0123-456789abcdef",
"disable_autoformat": false,
"formula": "",
"metric_display_name": "%Used",
"metric_name": "fs`/foo`df_used_percent",
"period": 0,
"range_high": 100,
"range_low": 0,
"thresholds": {
"colors": [
"#008000",
"#ffcc00",
"#ee0000"
],
"flip": false,
"values": [
"75%",
"87.5%"
]
},
"title": "Metric Gauge",
"type": "bar",
"value_type": "gauge"
},
"type": "gauge",
"widget_id": "w7",
"width": 1
},
{
"active": true,
"height": 1,
"name": "Text",
"origin": "c2",
"settings": {
"autoformat": false,
"body_format": "<p>{metric_name} ({value_type})<br /><strong>{metric_value}</strong><br /><span class=\"date\">{value_date}</span></p>",
"datapoints": [
{
"_check_id": 1234,
"_metric_type": "numeric",
"account_id": "1234",
"label": "cache entries",
"metric": "01234567-89ab-cdef-0123-456789abcdef:foo`cache_entries"
},
{
"_check_id": 1234,
"_metric_type": "numeric",
"account_id": "1234",
"label": "cache capacity",
"metric": "01234567-89ab-cdef-0123-456789abcdef:foo`cache_capacity"
},
{
"_check_id": 1234,
"_metric_type": "numeric",
"account_id": "1234",
"label": "cache size",
"metric": "01234567-89ab-cdef-0123-456789abcdef:foo`cache_size"
}
],
"period": 0,
"title_format": "Metric Status",
"use_default": true,
"value_type": "gauge"
},
"type": "text",
"widget_id": "w12",
"width": 1
},
{
"active": true,
"height": 1,
"name": "Forecast",
"origin": "a1",
"settings": {
"format": "standard",
"resource_limit": "0",
"resource_usage": "metric:average(\"01234567-89ab-cdef-0123-456789abcdef\",p\"fs%60/foo%60df_used_percent\")",
"thresholds": {
"colors": [
"#008000",
"#ffcc00",
"#ee0000"
],
"values": [
"1d",
"1h"
]
},
"title": "Resource Forecast",
"trend": "auto"
},
"type": "forecast",
"widget_id": "w6",
"width": 1
}
]
}

View File

@@ -24,24 +24,24 @@ type DashboardGridLayout struct {
// DashboardAccessConfig defines access config
type DashboardAccessConfig struct {
BlackDash bool `json:"black_dash,omitempty"`
Enabled bool `json:"enabled,omitempty"`
Fullscreen bool `json:"fullscreen,omitempty"`
FullscreenHideTitle bool `json:"fullscreen_hide_title,omitempty"`
Nickname string `json:"nickname,omitempty"`
ScaleText bool `json:"scale_text,omitempty"`
SharedID string `json:"shared_id,omitempty"`
TextSize uint `json:"text_size,omitempty"`
BlackDash bool `json:"black_dash"`
Enabled bool `json:"enabled"`
Fullscreen bool `json:"fullscreen"`
FullscreenHideTitle bool `json:"fullscreen_hide_title"`
Nickname string `json:"nickname"`
ScaleText bool `json:"scale_text"`
SharedID string `json:"shared_id"`
TextSize uint `json:"text_size"`
}
// DashboardOptions defines options
type DashboardOptions struct {
AccessConfigs []DashboardAccessConfig `json:"access_configs,omitempty"`
FullscreenHideTitle bool `json:"fullscreen_hide_title,omitempty"`
HideGrid bool `json:"hide_grid,omitempty"`
Linkages [][]string `json:"linkages,omitempty"`
ScaleText bool `json:"scale_text,omitempty"`
TextSize uint `json:"text_size,omitempty"`
AccessConfigs []DashboardAccessConfig `json:"access_configs"`
FullscreenHideTitle bool `json:"fullscreen_hide_title"`
HideGrid bool `json:"hide_grid"`
Linkages [][]string `json:"linkages"`
ScaleText bool `json:"scale_text"`
TextSize uint `json:"text_size"`
}
// ChartTextWidgetDatapoint defines datapoints for charts
@@ -116,67 +116,68 @@ type StatusWidgetHostStatusSettings struct {
}
// DashboardWidgetSettings defines settings specific to widget
// Note: optional attributes which are structs need to be pointers so they will be omitted
type DashboardWidgetSettings struct {
AccountID string `json:"account_id,omitempty"` // alerts, clusters, gauges, graphs, lists, status
Acknowledged string `json:"acknowledged,omitempty"` // alerts
AgentStatusSettings StatusWidgetAgentStatusSettings `json:"agent_status_settings,omitempty"` // status
Algorithm string `json:"algorithm,omitempty"` // clusters
Autoformat bool `json:"autoformat,omitempty"` // text
BodyFormat string `json:"body_format,omitempty"` // text
ChartType string `json:"chart_type,omitempty"` // charts
CheckUUID string `json:"check_uuid,omitempty"` // gauges
Cleared string `json:"cleared,omitempty"` // alerts
ClusterID uint `json:"cluster_id,omitempty"` // clusters
ClusterName string `json:"cluster_name,omitempty"` // clusters
ContactGroups []uint `json:"contact_groups,omitempty"` // alerts
ContentType string `json:"content_type,omitempty"` // status
Datapoints []ChartTextWidgetDatapoint `json:"datapoints,omitempty"` // charts, text
DateWindow string `json:"date_window,omitempty"` // graphs
Definition ChartWidgtDefinition `json:"definition,omitempty"` // charts
Dependents string `json:"dependents,omitempty"` // alerts
DisableAutoformat bool `json:"disable_autoformat,omitempty"` // gauges
Display string `json:"display,omitempty"` // alerts
Format string `json:"format,omitempty"` // forecasts
Formula string `json:"formula,omitempty"` // gauges
GraphUUID string `json:"graph_id,omitempty"` // graphs
HideXAxis bool `json:"hide_xaxis,omitempty"` // graphs
HideYAxis bool `json:"hide_yaxis,omitempty"` // graphs
HostStatusSettings StatusWidgetHostStatusSettings `json:"host_status_settings,omitempty"` // status
KeyInline bool `json:"key_inline,omitempty"` // graphs
KeyLoc string `json:"key_loc,omitempty"` // graphs
KeySize string `json:"key_size,omitempty"` // graphs
KeyWrap bool `json:"key_wrap,omitempty"` // graphs
Label string `json:"label,omitempty"` // graphs
Layout string `json:"layout,omitempty"` // clusters
Limit string `json:"limit,omitempty"` // lists
Maintenance string `json:"maintenance,omitempty"` // alerts
Markup string `json:"markup,omitempty"` // html
MetricDisplayName string `json:"metric_display_name,omitempty"` // gauges
MetricName string `json:"metric_name,omitempty"` // gauges
MinAge string `json:"min_age,omitempty"` // alerts
OffHours []uint `json:"off_hours,omitempty"` // alerts
OverlaySetID string `json:"overlay_set_id,omitempty"` // graphs
Period interface{} `json:"period,omitempty"` // BUG type switching between widgets (doc: string; gauges, text: uint; graphs: string)
RangeHigh int `json:"range_high,omitempty"` // gauges
RangeLow int `json:"range_low,omitempty"` // gauges
Realtime bool `json:"realtime,omitempty"` // graphs
ResourceLimit string `json:"resource_limit,omitempty"` // forecasts
ResourceUsage string `json:"resource_usage,omitempty"` // forecasts
Search string `json:"search,omitempty"` // alerts, lists
Severity string `json:"severity,omitempty"` // alerts
ShowFlags bool `json:"show_flags,omitempty"` // graphs
Size string `json:"size,omitempty"` // clusters
TagFilterSet []string `json:"tag_filter_set,omitempty"` // alerts
Threshold float32 `json:"threshold,omitempty"` // clusters
Thresholds ForecastGaugeWidgetThresholds `json:"thresholds,omitempty"` // forecasts, gauges
TimeWindow string `json:"time_window,omitempty"` // alerts
Title string `json:"title,omitempty"` // alerts, charts, forecasts, gauges, html
TitleFormat string `json:"title_format,omitempty"` // text
Trend string `json:"trend,omitempty"` // forecasts
Type string `json:"type,omitempty"` // gauges, lists
UseDefault bool `json:"use_default,omitempty"` // text
ValueType string `json:"value_type,omitempty"` // gauges, text
WeekDays []string `json:"weekdays,omitempty"` // alerts
AccountID string `json:"account_id,omitempty"` // alerts, clusters, gauges, graphs, lists, status
Acknowledged string `json:"acknowledged,omitempty"` // alerts
AgentStatusSettings *StatusWidgetAgentStatusSettings `json:"agent_status_settings,omitempty"` // status
Algorithm string `json:"algorithm,omitempty"` // clusters
Autoformat bool `json:"autoformat,omitempty"` // text
BodyFormat string `json:"body_format,omitempty"` // text
ChartType string `json:"chart_type,omitempty"` // charts
CheckUUID string `json:"check_uuid,omitempty"` // gauges
Cleared string `json:"cleared,omitempty"` // alerts
ClusterID uint `json:"cluster_id,omitempty"` // clusters
ClusterName string `json:"cluster_name,omitempty"` // clusters
ContactGroups []uint `json:"contact_groups,omitempty"` // alerts
ContentType string `json:"content_type,omitempty"` // status
Datapoints []ChartTextWidgetDatapoint `json:"datapoints,omitempty"` // charts, text
DateWindow string `json:"date_window,omitempty"` // graphs
Definition *ChartWidgtDefinition `json:"definition,omitempty"` // charts
Dependents string `json:"dependents,omitempty"` // alerts
DisableAutoformat bool `json:"disable_autoformat,omitempty"` // gauges
Display string `json:"display,omitempty"` // alerts
Format string `json:"format,omitempty"` // forecasts
Formula string `json:"formula,omitempty"` // gauges
GraphUUID string `json:"graph_id,omitempty"` // graphs
HideXAxis bool `json:"hide_xaxis,omitempty"` // graphs
HideYAxis bool `json:"hide_yaxis,omitempty"` // graphs
HostStatusSettings *StatusWidgetHostStatusSettings `json:"host_status_settings,omitempty"` // status
KeyInline bool `json:"key_inline,omitempty"` // graphs
KeyLoc string `json:"key_loc,omitempty"` // graphs
KeySize uint `json:"key_size,omitempty"` // graphs
KeyWrap bool `json:"key_wrap,omitempty"` // graphs
Label string `json:"label,omitempty"` // graphs
Layout string `json:"layout,omitempty"` // clusters
Limit uint `json:"limit,omitempty"` // lists
Maintenance string `json:"maintenance,omitempty"` // alerts
Markup string `json:"markup,omitempty"` // html
MetricDisplayName string `json:"metric_display_name,omitempty"` // gauges
MetricName string `json:"metric_name,omitempty"` // gauges
MinAge string `json:"min_age,omitempty"` // alerts
OffHours []uint `json:"off_hours,omitempty"` // alerts
OverlaySetID string `json:"overlay_set_id,omitempty"` // graphs
Period uint `json:"period,omitempty"` // gauges, text, graphs
RangeHigh int `json:"range_high,omitempty"` // gauges
RangeLow int `json:"range_low,omitempty"` // gauges
Realtime bool `json:"realtime,omitempty"` // graphs
ResourceLimit string `json:"resource_limit,omitempty"` // forecasts
ResourceUsage string `json:"resource_usage,omitempty"` // forecasts
Search string `json:"search,omitempty"` // alerts, lists
Severity string `json:"severity,omitempty"` // alerts
ShowFlags bool `json:"show_flags,omitempty"` // graphs
Size string `json:"size,omitempty"` // clusters
TagFilterSet []string `json:"tag_filter_set,omitempty"` // alerts
Threshold float32 `json:"threshold,omitempty"` // clusters
Thresholds *ForecastGaugeWidgetThresholds `json:"thresholds,omitempty"` // forecasts, gauges
TimeWindow string `json:"time_window,omitempty"` // alerts
Title string `json:"title,omitempty"` // alerts, charts, forecasts, gauges, html
TitleFormat string `json:"title_format,omitempty"` // text
Trend string `json:"trend,omitempty"` // forecasts
Type string `json:"type,omitempty"` // gauges, lists
UseDefault bool `json:"use_default,omitempty"` // text
ValueType string `json:"value_type,omitempty"` // gauges, text
WeekDays []string `json:"weekdays,omitempty"` // alerts
}
// DashboardWidget defines widget

View File

@@ -56,7 +56,7 @@ Verbs
Delete remove an item - e.g. DeleteAnnotation, DeleteAnnotationByCID
Search search for item(s) - e.g. SearchAnnotations
New new item config - e.g. NewAnnotation (returns an empty item,
any applicable defautls defined)
any applicable defaults defined)
Not all endpoints support all verbs.
*/

View File

@@ -37,13 +37,13 @@ type GraphAccessKey struct {
// GraphComposite defines a composite
type GraphComposite struct {
Axis string `json:"axis,omitempty"` // string
Color string `json:"color,omitempty"` // string
DataFormula *string `json:"data_formula,omitempty"` // string or null
Hidden bool `json:"hidden,omitempty"` // boolean
LegendFormula *string `json:"legend_formula,omitempty"` // string or null
Name string `json:"name,omitempty"` // string
Stack *uint `json:"stack,omitempty"` // uint or null
Axis string `json:"axis"` // string
Color string `json:"color"` // string
DataFormula *string `json:"data_formula"` // string or null
Hidden bool `json:"hidden"` // boolean
LegendFormula *string `json:"legend_formula"` // string or null
Name string `json:"name"` // string
Stack *uint `json:"stack"` // uint or null
}
// GraphDatapoint defines a datapoint
@@ -65,17 +65,18 @@ type GraphDatapoint struct {
// GraphGuide defines a guide
type GraphGuide struct {
Color string `json:"color,omitempty"` // string
DataFormula *string `json:"data_formula,omitempty"` // string or null
Hidden bool `json:"hidden,omitempty"` // boolean
LegendFormula *string `json:"legend_formula,omitempty"` // string or null
Name string `json:"name,omitempty"` // string
Color string `json:"color"` // string
DataFormula *string `json:"data_formula"` // string or null
Hidden bool `json:"hidden"` // boolean
LegendFormula *string `json:"legend_formula"` // string or null
Name string `json:"name"` // string
}
// GraphMetricCluster defines a metric cluster
type GraphMetricCluster struct {
AggregateFunc string `json:"aggregate_function,omitempty"` // string
Axis string `json:"axis,omitempty"` // string
Color *string `json:"color,omitempty"` // string
DataFormula *string `json:"data_formula"` // string or null
Hidden bool `json:"hidden"` // boolean
LegendFormula *string `json:"legend_formula"` // string or null
@@ -142,7 +143,7 @@ type Graph struct {
Datapoints []GraphDatapoint `json:"datapoints,omitempt"` // [] len >= 0
Description string `json:"description,omitempty"` // string
Guides []GraphGuide `json:"guides,omitempty"` // [] len >= 0
LineStyle string `json:"line_style,omitempty"` // string
LineStyle *string `json:"line_style"` // string or null
LogLeftY *int `json:"logarithmic_left_y,string,omitempty"` // int encoded as string or null BUG doc: number (not string)
LogRightY *int `json:"logarithmic_right_y,string,omitempty"` // int encoded as string or null BUG doc: number (not string)
MaxLeftY *float64 `json:"max_left_y,string,omitempty"` // float64 encoded as string or null BUG doc: number (not string)
@@ -152,7 +153,7 @@ type Graph struct {
MinRightY *float64 `json:"min_right_y,string,omitempty"` // float64 encoded as string or null BUG doc: number (not string)
Notes *string `json:"notes,omitempty"` // string or null
OverlaySets *map[string]GraphOverlaySet `json:"overlay_sets,omitempty"` // GroupOverLaySets or null
Style string `json:"style,omitempty"` // string
Style *string `json:"style"` // string or null
Tags []string `json:"tags,omitempty"` // [] len >= 0
Title string `json:"title,omitempty"` // string
}

View File

@@ -26,7 +26,7 @@ type Metric struct {
CheckTags []string `json:"_check_tags,omitempty"` // [] len >= 0
CheckUUID string `json:"_check_uuid,omitempty"` // string
CID string `json:"_cid,omitempty"` // string
Histogram bool `json:"_histogram,omitempty"` // boolean
Histogram string `json:"_histogram,omitempty"` // string
Link *string `json:"link,omitempty"` // string or null
MetricName string `json:"_metric_name,omitempty"` // string
MetricType string `json:"_metric_type,omitempty"` // string

View File

@@ -166,7 +166,7 @@ func (a *API) DeleteRuleSetGroup(cfg *RuleSetGroup) (bool, error) {
return a.DeleteRuleSetGroupByCID(CIDType(&cfg.CID))
}
// DeleteRuleSetGroupByCID deletes rule set group wiht passed cid.
// DeleteRuleSetGroupByCID deletes rule set group with passed cid.
func (a *API) DeleteRuleSetGroupByCID(cid CIDType) (bool, error) {
if cid == nil || *cid == "" {
return false, fmt.Errorf("Invalid rule set group CID [none]")

View File

@@ -30,14 +30,14 @@ type WorksheetSmartQuery struct {
// Worksheet defines a worksheet. See https://login.circonus.com/resources/api/calls/worksheet for more information.
type Worksheet struct {
CID string `json:"_cid,omitempty"` // string
Description *string `json:"description"` // string or null
Favorite bool `json:"favorite"` // boolean
Graphs []WorksheetGraph `json:"worksheets,omitempty"` // [] len >= 0
Notes *string `json:"notes"` // string or null
SmartQueries []WorksheetSmartQuery `json:"smart_queries,omitempty"` // [] len >= 0
Tags []string `json:"tags"` // [] len >= 0
Title string `json:"title"` // string
CID string `json:"_cid,omitempty"` // string
Description *string `json:"description"` // string or null
Favorite bool `json:"favorite"` // boolean
Graphs []WorksheetGraph `json:"graphs"` // [] len >= 0
Notes *string `json:"notes"` // string or null
SmartQueries []WorksheetSmartQuery `json:"smart_queries"` // [] len >= 0
Tags []string `json:"tags"` // [] len >= 0
Title string `json:"title"` // string
}
// NewWorksheet returns a new Worksheet (with defaults, if applicable)

View File

@@ -80,6 +80,7 @@ func (cm *CheckManager) getBrokerCN(broker *api.Broker, submissionURL api.URLTyp
func (cm *CheckManager) selectBroker() (*api.Broker, error) {
var brokerList *[]api.Broker
var err error
enterpriseType := "enterprise"
if len(cm.brokerSelectTag) > 0 {
filter := api.SearchFilterType{
@@ -104,9 +105,10 @@ func (cm *CheckManager) selectBroker() (*api.Broker, error) {
haveEnterprise := false
for _, broker := range *brokerList {
broker := broker
if cm.isValidBroker(&broker) {
validBrokers[broker.CID] = broker
if broker.Type == "enterprise" {
if broker.Type == enterpriseType {
haveEnterprise = true
}
}
@@ -114,7 +116,7 @@ func (cm *CheckManager) selectBroker() (*api.Broker, error) {
if haveEnterprise { // eliminate non-enterprise brokers from valid brokers
for k, v := range validBrokers {
if v.Type != "enterprise" {
if v.Type != enterpriseType {
delete(validBrokers, k)
}
}
@@ -138,8 +140,20 @@ func (cm *CheckManager) selectBroker() (*api.Broker, error) {
// Verify broker supports the check type to be used
func (cm *CheckManager) brokerSupportsCheckType(checkType CheckTypeType, details *api.BrokerDetail) bool {
baseType := string(checkType)
for _, module := range details.Modules {
if CheckTypeType(module) == checkType {
if module == baseType {
return true
}
}
if idx := strings.Index(baseType, ":"); idx > 0 {
baseType = baseType[0:idx]
}
for _, module := range details.Modules {
if module == baseType {
return true
}
}
@@ -152,8 +166,15 @@ func (cm *CheckManager) brokerSupportsCheckType(checkType CheckTypeType, details
func (cm *CheckManager) isValidBroker(broker *api.Broker) bool {
var brokerHost string
var brokerPort string
if broker.Type != "circonus" && broker.Type != "enterprise" {
return false
}
valid := false
for _, detail := range broker.Details {
detail := detail
// broker must be active
if detail.Status != statusActive {
@@ -174,7 +195,7 @@ func (cm *CheckManager) isValidBroker(broker *api.Broker) bool {
if detail.ExternalPort != 0 {
brokerPort = strconv.Itoa(int(detail.ExternalPort))
} else {
if *detail.Port != 0 {
if detail.Port != nil && *detail.Port != 0 {
brokerPort = strconv.Itoa(int(*detail.Port))
} else {
brokerPort = "43191"
@@ -183,10 +204,15 @@ func (cm *CheckManager) isValidBroker(broker *api.Broker) bool {
if detail.ExternalHost != nil && *detail.ExternalHost != "" {
brokerHost = *detail.ExternalHost
} else {
} else if detail.IP != nil && *detail.IP != "" {
brokerHost = *detail.IP
}
if brokerHost == "" {
cm.Log.Printf("[WARN] Broker '%s' instance %s has no IP or external host set", broker.Name, detail.CN)
continue
}
if brokerHost == "trap.noit.circonus.net" && brokerPort != "443" {
brokerPort = "443"
}

View File

@@ -42,17 +42,22 @@ type CACert struct {
}
// loadCACert loads the CA cert for the broker designated by the submission url
func (cm *CheckManager) loadCACert() {
func (cm *CheckManager) loadCACert() error {
if cm.certPool != nil {
return
return nil
}
cm.certPool = x509.NewCertPool()
cert, err := cm.fetchCert()
if err != nil {
if cm.Debug {
cm.Log.Printf("[DEBUG] Unable to fetch ca.crt, using default. %+v\n", err)
var cert []byte
var err error
if cm.enabled {
// only attempt to retrieve broker CA cert if
// the check is being managed.
cert, err = cm.fetchCert()
if err != nil {
return err
}
}
@@ -61,6 +66,8 @@ func (cm *CheckManager) loadCACert() {
}
cm.certPool.AppendCertsFromPEM(cert)
return nil
}
// fetchCert fetches CA certificate using Circonus API

View File

@@ -166,7 +166,7 @@ func (cm *CheckManager) initializeTrapURL() error {
// new search (check.target != instanceid, instanceid encoded in notes field)
searchCriteria := fmt.Sprintf(
"(active:1)(type:\"%s\")(tags:%s)", cm.checkType, strings.Join(cm.checkSearchTag, ","))
filterCriteria := map[string][]string{"f_notes": []string{*cm.getNotes()}}
filterCriteria := map[string][]string{"f_notes": {*cm.getNotes()}}
checkBundle, err = cm.checkBundleSearch(searchCriteria, filterCriteria)
if err != nil {
return err
@@ -243,6 +243,18 @@ func (cm *CheckManager) initializeTrapURL() error {
}
cm.trapCN = BrokerCNType(cn)
if cm.enabled {
u, err := url.Parse(string(cm.trapURL))
if err != nil {
return err
}
if u.Scheme == "https" {
if err := cm.loadCACert(); err != nil {
return err
}
}
}
cm.trapLastUpdate = time.Now()
return nil
@@ -295,12 +307,9 @@ func (cm *CheckManager) createNewCheck() (*api.CheckBundle, *api.Broker, error)
return nil, nil, err
}
config := &api.CheckBundle{
Brokers: []string{broker.CID},
Config: map[config.Key]string{
config.AsyncMetrics: "true",
config.Secret: checkSecret,
},
chkcfg := &api.CheckBundle{
Brokers: []string{broker.CID},
Config: make(map[config.Key]string),
DisplayName: string(cm.checkDisplayName),
Metrics: []api.CheckBundleMetric{},
MetricLimit: config.DefaultCheckBundleMetricLimit,
@@ -313,7 +322,24 @@ func (cm *CheckManager) createNewCheck() (*api.CheckBundle, *api.Broker, error)
Type: string(cm.checkType),
}
checkBundle, err := cm.apih.CreateCheckBundle(config)
if len(cm.customConfigFields) > 0 {
for fld, val := range cm.customConfigFields {
chkcfg.Config[config.Key(fld)] = val
}
}
//
// use the default config settings if these are NOT set by user configuration
//
if val, ok := chkcfg.Config[config.AsyncMetrics]; !ok || val == "" {
chkcfg.Config[config.AsyncMetrics] = "true"
}
if val, ok := chkcfg.Config[config.Secret]; !ok || val == "" {
chkcfg.Config[config.Secret] = checkSecret
}
checkBundle, err := cm.apih.CreateCheckBundle(chkcfg)
if err != nil {
return nil, nil, err
}

View File

@@ -2,25 +2,27 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package checkmgr provides a check management interace to circonus-gometrics
// Package checkmgr provides a check management interface to circonus-gometrics
package checkmgr
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"log"
"net/url"
"os"
"path"
"regexp"
"strconv"
"strings"
"sync"
"time"
"github.com/circonus-labs/circonus-gometrics/api"
"github.com/pkg/errors"
"github.com/tv42/httpunix"
)
// Check management offers:
@@ -35,7 +37,7 @@ import (
// - configuration parameters other than Check.SubmissionUrl, Debug and Log are ignored
// - note: SubmissionUrl is **required** in this case as there is no way to derive w/o api
// configure with api token - check management enabled
// - all otehr configuration parameters affect how the trap url is obtained
// - all other configuration parameters affect how the trap url is obtained
// 1. provided (Check.SubmissionUrl)
// 2. via check lookup (CheckConfig.Id)
// 3. via a search using CheckConfig.InstanceId + CheckConfig.SearchTag
@@ -85,6 +87,10 @@ type CheckConfig struct {
// overrides the behavior and will re-activate the metric when it is
// encountered. "(true|false)", default "false"
ForceMetricActivation string
// Type of check to use (default: httptrap)
Type string
// Custom check config fields (default: none)
CustomConfigFields map[string]string
}
// BrokerConfig options for broker
@@ -97,6 +103,8 @@ type BrokerConfig struct {
// for a broker to be considered viable it must respond to a
// connection attempt within this amount of time e.g. 200ms, 2s, 1m
MaxResponseTime string
// TLS configuration to use when communicating within broker
TLSConfig *tls.Config
}
// Config options
@@ -151,6 +159,7 @@ type CheckManager struct {
checkSearchTag api.TagType
checkSecret CheckSecretType
checkTags api.TagType
customConfigFields map[string]string
checkSubmissionURL api.URLType
checkDisplayName CheckDisplayNameType
forceMetricActivation bool
@@ -164,6 +173,7 @@ type CheckManager struct {
brokerID api.IDType
brokerSelectTag api.TagType
brokerMaxResponseTime time.Duration
brokerTLS *tls.Config
// state
checkBundle *api.CheckBundle
@@ -176,12 +186,15 @@ type CheckManager struct {
trapMaxURLAge time.Duration
trapmu sync.Mutex
certPool *x509.CertPool
sockRx *regexp.Regexp
}
// Trap config
type Trap struct {
URL *url.URL
TLS *tls.Config
URL *url.URL
TLS *tls.Config
IsSocket bool
SockTransport *httpunix.Transport
}
// NewCheckManager returns a new check manager
@@ -208,6 +221,14 @@ func New(cfg *Config) (*CheckManager, error) {
cm.Log = log.New(ioutil.Discard, "", log.LstdFlags)
}
{
rx, err := regexp.Compile(`^http\+unix://(?P<sockfile>.+)/write/(?P<id>.+)$`)
if err != nil {
return nil, errors.Wrap(err, "compiling socket regex")
}
cm.sockRx = rx
}
if cfg.Check.SubmissionURL != "" {
cm.checkSubmissionURL = api.URLType(cfg.Check.SubmissionURL)
}
@@ -227,13 +248,17 @@ func New(cfg *Config) (*CheckManager, error) {
cfg.API.Log = cm.Log
apih, err := api.New(&cfg.API)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "initializing api client")
}
cm.apih = apih
}
// initialize check related data
cm.checkType = defaultCheckType
if cfg.Check.Type != "" {
cm.checkType = CheckTypeType(cfg.Check.Type)
} else {
cm.checkType = defaultCheckType
}
idSetting := "0"
if cfg.Check.ID != "" {
@@ -241,7 +266,7 @@ func New(cfg *Config) (*CheckManager, error) {
}
id, err := strconv.Atoi(idSetting)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "converting check id")
}
cm.checkID = api.IDType(id)
@@ -256,7 +281,7 @@ func New(cfg *Config) (*CheckManager, error) {
}
fm, err := strconv.ParseBool(fma)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing force metric activation")
}
cm.forceMetricActivation = fm
@@ -285,13 +310,20 @@ func New(cfg *Config) (*CheckManager, error) {
cm.checkTags = strings.Split(strings.Replace(cfg.Check.Tags, " ", "", -1), ",")
}
cm.customConfigFields = make(map[string]string)
if len(cfg.Check.CustomConfigFields) > 0 {
for fld, val := range cfg.Check.CustomConfigFields {
cm.customConfigFields[fld] = val
}
}
dur := cfg.Check.MaxURLAge
if dur == "" {
dur = defaultTrapMaxURLAge
}
maxDur, err := time.ParseDuration(dur)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing max url age")
}
cm.trapMaxURLAge = maxDur
@@ -302,7 +334,7 @@ func New(cfg *Config) (*CheckManager, error) {
}
id, err = strconv.Atoi(idSetting)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing broker id")
}
cm.brokerID = api.IDType(id)
@@ -316,10 +348,13 @@ func New(cfg *Config) (*CheckManager, error) {
}
maxDur, err = time.ParseDuration(dur)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing broker max response time")
}
cm.brokerMaxResponseTime = maxDur
// add user specified tls config for broker if provided
cm.brokerTLS = cfg.Broker.TLSConfig
// metrics
cm.availableMetrics = make(map[string]bool)
cm.metricTags = make(map[string][]string)
@@ -368,24 +403,72 @@ func (cm *CheckManager) IsReady() bool {
// GetSubmissionURL returns submission url for circonus
func (cm *CheckManager) GetSubmissionURL() (*Trap, error) {
if cm.trapURL == "" {
return nil, fmt.Errorf("[ERROR] no submission url currently available")
// if err := cm.initializeTrapURL(); err != nil {
// return nil, err
// }
return nil, errors.Errorf("get submission url - submission url unavailable")
}
trap := &Trap{}
u, err := url.Parse(string(cm.trapURL))
if err != nil {
return nil, err
return nil, errors.Wrap(err, "get submission url")
}
trap.URL = u
if u.Scheme == "http+unix" {
service := "circonus-agent"
sockPath := ""
metricID := ""
subNames := cm.sockRx.SubexpNames()
matches := cm.sockRx.FindAllStringSubmatch(string(cm.trapURL), -1)
for _, match := range matches {
for idx, val := range match {
switch subNames[idx] {
case "sockfile":
sockPath = val
case "id":
metricID = val
}
}
}
if sockPath == "" || metricID == "" {
return nil, errors.Errorf("get submission url - invalid socket url (%s)", cm.trapURL)
}
u, err = url.Parse(fmt.Sprintf("http+unix://%s/write/%s", service, metricID))
if err != nil {
return nil, errors.Wrap(err, "get submission url")
}
trap.URL = u
trap.SockTransport = &httpunix.Transport{
DialTimeout: 100 * time.Millisecond,
RequestTimeout: 1 * time.Second,
ResponseHeaderTimeout: 1 * time.Second,
}
trap.SockTransport.RegisterLocation(service, sockPath)
trap.IsSocket = true
}
if u.Scheme == "https" {
// preference user-supplied TLS configuration
if cm.brokerTLS != nil {
trap.TLS = cm.brokerTLS
return trap, nil
}
// api.circonus.com uses a public CA signed certificate
// trap.noit.circonus.net uses Circonus CA private certificate
// enterprise brokers use private CA certificate
if trap.URL.Hostname() == "api.circonus.com" {
return trap, nil
}
if cm.certPool == nil {
cm.loadCACert()
if err := cm.loadCACert(); err != nil {
return nil, errors.Wrap(err, "get submission url")
}
}
t := &tls.Config{
RootCAs: cm.certPool,
@@ -406,18 +489,19 @@ func (cm *CheckManager) ResetTrap() error {
}
cm.trapURL = ""
cm.certPool = nil
err := cm.initializeTrapURL()
return err
cm.certPool = nil // force re-fetching CA cert (if custom TLS config not supplied)
return cm.initializeTrapURL()
}
// RefreshTrap check when the last time the URL was reset, reset if needed
func (cm *CheckManager) RefreshTrap() {
func (cm *CheckManager) RefreshTrap() error {
if cm.trapURL == "" {
return
return nil
}
if time.Since(cm.trapLastUpdate) >= cm.trapMaxURLAge {
cm.ResetTrap()
return cm.ResetTrap()
}
return nil
}

View File

@@ -13,8 +13,7 @@ func (cm *CheckManager) IsMetricActive(name string) bool {
cm.availableMetricsmu.Lock()
defer cm.availableMetricsmu.Unlock()
active, _ := cm.availableMetrics[name]
return active
return cm.availableMetrics[name]
}
// ActivateMetric determines if a given metric should be activated

View File

@@ -30,22 +30,35 @@
package circonusgometrics
import (
"errors"
"bufio"
"bytes"
"fmt"
"io/ioutil"
"log"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/circonus-labs/circonus-gometrics/api"
"github.com/circonus-labs/circonus-gometrics/checkmgr"
"github.com/pkg/errors"
)
const (
defaultFlushInterval = "10s" // 10 * time.Second
)
// Metric defines an individual metric
type Metric struct {
Type string `json:"_type"`
Value interface{} `json:"_value"`
}
// Metrics holds host metrics
type Metrics map[string]Metric
// Config options for circonus-gometrics
type Config struct {
Log *log.Logger
@@ -63,6 +76,12 @@ type Config struct {
Interval string
}
type prevMetrics struct {
metrics *Metrics
metricsmu sync.Mutex
ts time.Time
}
// CirconusMetrics state
type CirconusMetrics struct {
Log *log.Logger
@@ -75,7 +94,9 @@ type CirconusMetrics struct {
flushInterval time.Duration
flushing bool
flushmu sync.Mutex
packagingmu sync.Mutex
check *checkmgr.CheckManager
lastMetrics *prevMetrics
counters map[string]uint64
cm sync.Mutex
@@ -83,7 +104,7 @@ type CirconusMetrics struct {
counterFuncs map[string]func() uint64
cfm sync.Mutex
gauges map[string]string
gauges map[string]interface{}
gm sync.Mutex
gaugeFuncs map[string]func() int64
@@ -114,11 +135,12 @@ func New(cfg *Config) (*CirconusMetrics, error) {
cm := &CirconusMetrics{
counters: make(map[string]uint64),
counterFuncs: make(map[string]func() uint64),
gauges: make(map[string]string),
gauges: make(map[string]interface{}),
gaugeFuncs: make(map[string]func() int64),
histograms: make(map[string]*Histogram),
text: make(map[string]string),
textFuncs: make(map[string]func() string),
lastMetrics: &prevMetrics{},
}
// Logging
@@ -143,7 +165,7 @@ func New(cfg *Config) (*CirconusMetrics, error) {
dur, err := time.ParseDuration(fi)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing flush interval")
}
cm.flushInterval = dur
}
@@ -154,7 +176,7 @@ func New(cfg *Config) (*CirconusMetrics, error) {
if cfg.ResetCounters != "" {
setting, err := strconv.ParseBool(cfg.ResetCounters)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing reset counters")
}
cm.resetCounters = setting
}
@@ -163,7 +185,7 @@ func New(cfg *Config) (*CirconusMetrics, error) {
if cfg.ResetGauges != "" {
setting, err := strconv.ParseBool(cfg.ResetGauges)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing reset gauges")
}
cm.resetGauges = setting
}
@@ -172,7 +194,7 @@ func New(cfg *Config) (*CirconusMetrics, error) {
if cfg.ResetHistograms != "" {
setting, err := strconv.ParseBool(cfg.ResetHistograms)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing reset histograms")
}
cm.resetHistograms = setting
}
@@ -181,7 +203,7 @@ func New(cfg *Config) (*CirconusMetrics, error) {
if cfg.ResetText != "" {
setting, err := strconv.ParseBool(cfg.ResetText)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "parsing reset text")
}
cm.resetText = setting
}
@@ -193,7 +215,7 @@ func New(cfg *Config) (*CirconusMetrics, error) {
check, err := checkmgr.New(&cfg.CheckManager)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "creating new check manager")
}
cm.check = check
}
@@ -202,10 +224,10 @@ func New(cfg *Config) (*CirconusMetrics, error) {
cm.check.Initialize()
// if automatic flush is enabled, start it.
// note: submit will jettison metrics until initialization has completed.
// NOTE: submit will jettison metrics until initialization has completed.
if cm.flushInterval > time.Duration(0) {
go func() {
for _ = range time.NewTicker(cm.flushInterval).C {
for range time.NewTicker(cm.flushInterval).C {
cm.Flush()
}
}()
@@ -216,7 +238,7 @@ func New(cfg *Config) (*CirconusMetrics, error) {
// Start deprecated NOP, automatic flush is started in New if flush interval > 0.
func (m *CirconusMetrics) Start() {
return
// nop
}
// Ready returns true or false indicating if the check is ready to accept metrics
@@ -224,24 +246,18 @@ func (m *CirconusMetrics) Ready() bool {
return m.check.IsReady()
}
// Flush metrics kicks off the process of sending metrics to Circonus
func (m *CirconusMetrics) Flush() {
if m.flushing {
return
}
m.flushmu.Lock()
m.flushing = true
m.flushmu.Unlock()
func (m *CirconusMetrics) packageMetrics() (map[string]*api.CheckBundleMetric, Metrics) {
m.packagingmu.Lock()
defer m.packagingmu.Unlock()
if m.Debug {
m.Log.Println("[DEBUG] Flushing metrics")
m.Log.Println("[DEBUG] Packaging metrics")
}
// check for new metrics and enable them automatically
newMetrics := make(map[string]*api.CheckBundleMetric)
counters, gauges, histograms, text := m.snapshot()
output := make(map[string]interface{})
newMetrics := make(map[string]*api.CheckBundleMetric)
output := make(Metrics, len(counters)+len(gauges)+len(histograms)+len(text))
for name, value := range counters {
send := m.check.IsMetricActive(name)
if !send && m.check.ActivateMetric(name) {
@@ -253,10 +269,7 @@ func (m *CirconusMetrics) Flush() {
}
}
if send {
output[name] = map[string]interface{}{
"_type": "n",
"_value": value,
}
output[name] = Metric{Type: "L", Value: value}
}
}
@@ -271,10 +284,7 @@ func (m *CirconusMetrics) Flush() {
}
}
if send {
output[name] = map[string]interface{}{
"_type": "n",
"_value": value,
}
output[name] = Metric{Type: m.getGaugeType(value), Value: value}
}
}
@@ -289,10 +299,7 @@ func (m *CirconusMetrics) Flush() {
}
}
if send {
output[name] = map[string]interface{}{
"_type": "n",
"_value": value.DecStrings(),
}
output[name] = Metric{Type: "n", Value: value.DecStrings()}
}
}
@@ -307,13 +314,85 @@ func (m *CirconusMetrics) Flush() {
}
}
if send {
output[name] = map[string]interface{}{
"_type": "s",
"_value": value,
}
output[name] = Metric{Type: "s", Value: value}
}
}
m.lastMetrics.metricsmu.Lock()
defer m.lastMetrics.metricsmu.Unlock()
m.lastMetrics.metrics = &output
m.lastMetrics.ts = time.Now()
return newMetrics, output
}
// PromOutput returns lines of metrics in prom format
func (m *CirconusMetrics) PromOutput() (*bytes.Buffer, error) {
m.lastMetrics.metricsmu.Lock()
defer m.lastMetrics.metricsmu.Unlock()
if m.lastMetrics.metrics == nil {
return nil, errors.New("no metrics available")
}
var b bytes.Buffer
w := bufio.NewWriter(&b)
ts := m.lastMetrics.ts.UnixNano() / int64(time.Millisecond)
for name, metric := range *m.lastMetrics.metrics {
switch metric.Type {
case "n":
if strings.HasPrefix(fmt.Sprintf("%v", metric.Value), "[H[") {
continue // circonus histogram != prom "histogram" (aka percentile)
}
case "s":
continue // text metrics unsupported
}
fmt.Fprintf(w, "%s %v %d\n", name, metric.Value, ts)
}
err := w.Flush()
if err != nil {
return nil, errors.Wrap(err, "flushing metric buffer")
}
return &b, err
}
// FlushMetrics flushes current metrics to a structure and returns it (does NOT send to Circonus)
func (m *CirconusMetrics) FlushMetrics() *Metrics {
m.flushmu.Lock()
if m.flushing {
m.flushmu.Unlock()
return &Metrics{}
}
m.flushing = true
m.flushmu.Unlock()
_, output := m.packageMetrics()
m.flushmu.Lock()
m.flushing = false
m.flushmu.Unlock()
return &output
}
// Flush metrics kicks off the process of sending metrics to Circonus
func (m *CirconusMetrics) Flush() {
m.flushmu.Lock()
if m.flushing {
m.flushmu.Unlock()
return
}
m.flushing = true
m.flushmu.Unlock()
newMetrics, output := m.packageMetrics()
if len(output) > 0 {
m.submit(output, newMetrics)
} else {

View File

@@ -4,6 +4,8 @@
package circonusgometrics
import "fmt"
// A Counter is a monotonically increasing unsigned integer.
//
// Use a counter to derive rates (e.g., record total number of requests, derive
@@ -40,6 +42,19 @@ func (m *CirconusMetrics) RemoveCounter(metric string) {
delete(m.counters, metric)
}
// GetCounterTest returns the current value for a counter. (note: it is a function specifically for "testing", disable automatic submission during testing.)
func (m *CirconusMetrics) GetCounterTest(metric string) (uint64, error) {
m.cm.Lock()
defer m.cm.Unlock()
if val, ok := m.counters[metric]; ok {
return val, nil
}
return 0, fmt.Errorf("Counter metric '%s' not found", metric)
}
// SetCounterFunc set counter to a function [called at flush interval]
func (m *CirconusMetrics) SetCounterFunc(metric string, fn func() uint64) {
m.cfm.Lock()

View File

@@ -22,7 +22,48 @@ func (m *CirconusMetrics) Gauge(metric string, val interface{}) {
func (m *CirconusMetrics) SetGauge(metric string, val interface{}) {
m.gm.Lock()
defer m.gm.Unlock()
m.gauges[metric] = m.gaugeValString(val)
m.gauges[metric] = val
}
// AddGauge adds value to existing gauge
func (m *CirconusMetrics) AddGauge(metric string, val interface{}) {
m.gm.Lock()
defer m.gm.Unlock()
v, ok := m.gauges[metric]
if !ok {
m.gauges[metric] = val
return
}
switch val.(type) {
default:
// ignore it, unsupported type
case int:
m.gauges[metric] = v.(int) + val.(int)
case int8:
m.gauges[metric] = v.(int8) + val.(int8)
case int16:
m.gauges[metric] = v.(int16) + val.(int16)
case int32:
m.gauges[metric] = v.(int32) + val.(int32)
case int64:
m.gauges[metric] = v.(int64) + val.(int64)
case uint:
m.gauges[metric] = v.(uint) + val.(uint)
case uint8:
m.gauges[metric] = v.(uint8) + val.(uint8)
case uint16:
m.gauges[metric] = v.(uint16) + val.(uint16)
case uint32:
m.gauges[metric] = v.(uint32) + val.(uint32)
case uint64:
m.gauges[metric] = v.(uint64) + val.(uint64)
case float32:
m.gauges[metric] = v.(float32) + val.(float32)
case float64:
m.gauges[metric] = v.(float64) + val.(float64)
}
}
// RemoveGauge removes a gauge
@@ -32,6 +73,18 @@ func (m *CirconusMetrics) RemoveGauge(metric string) {
delete(m.gauges, metric)
}
// GetGaugeTest returns the current value for a gauge. (note: it is a function specifically for "testing", disable automatic submission during testing.)
func (m *CirconusMetrics) GetGaugeTest(metric string) (interface{}, error) {
m.gm.Lock()
defer m.gm.Unlock()
if val, ok := m.gauges[metric]; ok {
return val, nil
}
return nil, fmt.Errorf("Gauge metric '%s' not found", metric)
}
// SetGaugeFunc sets a gauge to a function [called at flush interval]
func (m *CirconusMetrics) SetGaugeFunc(metric string, fn func() int64) {
m.gfm.Lock()
@@ -46,36 +99,31 @@ func (m *CirconusMetrics) RemoveGaugeFunc(metric string) {
delete(m.gaugeFuncs, metric)
}
// gaugeValString converts an interface value (of a supported type) to a string
func (m *CirconusMetrics) gaugeValString(val interface{}) string {
vs := ""
switch v := val.(type) {
default:
// ignore it, unsupported type
// getGaugeType returns accurate resmon type for underlying type of gauge value
func (m *CirconusMetrics) getGaugeType(v interface{}) string {
mt := "n"
switch v.(type) {
case int:
vs = fmt.Sprintf("%d", v)
mt = "i"
case int8:
vs = fmt.Sprintf("%d", v)
mt = "i"
case int16:
vs = fmt.Sprintf("%d", v)
mt = "i"
case int32:
vs = fmt.Sprintf("%d", v)
case int64:
vs = fmt.Sprintf("%d", v)
mt = "i"
case uint:
vs = fmt.Sprintf("%d", v)
mt = "I"
case uint8:
vs = fmt.Sprintf("%d", v)
mt = "I"
case uint16:
vs = fmt.Sprintf("%d", v)
mt = "I"
case uint32:
vs = fmt.Sprintf("%d", v)
mt = "I"
case int64:
mt = "l"
case uint64:
vs = fmt.Sprintf("%d", v)
case float32:
vs = fmt.Sprintf("%f", v)
case float64:
vs = fmt.Sprintf("%f", v)
mt = "L"
}
return vs
return mt
}

View File

@@ -5,6 +5,7 @@
package circonusgometrics
import (
"fmt"
"sync"
"github.com/circonus-labs/circonusllhist"
@@ -27,6 +28,17 @@ func (m *CirconusMetrics) RecordValue(metric string, val float64) {
m.SetHistogramValue(metric, val)
}
// RecordCountForValue adds count n for value to a histogram
func (m *CirconusMetrics) RecordCountForValue(metric string, val float64, n int64) {
hist := m.NewHistogram(metric)
m.hm.Lock()
hist.rw.Lock()
hist.hist.RecordValues(val, n)
hist.rw.Unlock()
m.hm.Unlock()
}
// SetHistogramValue adds a value to a histogram
func (m *CirconusMetrics) SetHistogramValue(metric string, val float64) {
hist := m.NewHistogram(metric)
@@ -38,6 +50,18 @@ func (m *CirconusMetrics) SetHistogramValue(metric string, val float64) {
m.hm.Unlock()
}
// GetHistogramTest returns the current value for a gauge. (note: it is a function specifically for "testing", disable automatic submission during testing.)
func (m *CirconusMetrics) GetHistogramTest(metric string) ([]string, error) {
m.hm.Lock()
defer m.hm.Unlock()
if hist, ok := m.histograms[metric]; ok {
return hist.hist.DecStrings(), nil
}
return []string{""}, fmt.Errorf("Histogram metric '%s' not found", metric)
}
// RemoveHistogram removes a histogram
func (m *CirconusMetrics) RemoveHistogram(metric string) {
m.hm.Lock()

View File

@@ -6,8 +6,8 @@ package circonusgometrics
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
@@ -18,9 +18,10 @@ import (
"github.com/circonus-labs/circonus-gometrics/api"
"github.com/hashicorp/go-retryablehttp"
"github.com/pkg/errors"
)
func (m *CirconusMetrics) submit(output map[string]interface{}, newMetrics map[string]*api.CheckBundleMetric) {
func (m *CirconusMetrics) submit(output Metrics, newMetrics map[string]*api.CheckBundleMetric) {
// if there is nowhere to send metrics to, just return.
if !m.check.IsReady() {
@@ -43,6 +44,12 @@ func (m *CirconusMetrics) submit(output map[string]interface{}, newMetrics map[s
return
}
// OK response from circonus-agent does not
// indicate how many metrics were received
if numStats == -1 {
numStats = len(output)
}
if m.Debug {
m.Log.Printf("[DEBUG] %d stats sent\n", numStats)
}
@@ -51,7 +58,7 @@ func (m *CirconusMetrics) submit(output map[string]interface{}, newMetrics map[s
func (m *CirconusMetrics) trapCall(payload []byte) (int, error) {
trap, err := m.check.GetSubmissionURL()
if err != nil {
return 0, err
return 0, errors.Wrap(err, "trap call")
}
dataReader := bytes.NewReader(payload)
@@ -65,10 +72,14 @@ func (m *CirconusMetrics) trapCall(payload []byte) (int, error) {
// keep last HTTP error in the event of retry failure
var lastHTTPError error
retryPolicy := func(resp *http.Response, err error) (bool, error) {
retryPolicy := func(ctx context.Context, resp *http.Response, err error) (bool, error) {
if ctxErr := ctx.Err(); ctxErr != nil {
return false, ctxErr
}
if err != nil {
lastHTTPError = err
return true, err
return true, errors.Wrap(err, "retry policy")
}
// Check the response code. We retry on 500-range responses to allow
// the server time to recover, as 500's are typically not permanent
@@ -98,20 +109,24 @@ func (m *CirconusMetrics) trapCall(payload []byte) (int, error) {
TLSClientConfig: trap.TLS,
DisableKeepAlives: true,
MaxIdleConnsPerHost: -1,
DisableCompression: true,
DisableCompression: false,
}
} else {
} else if trap.URL.Scheme == "http" {
client.HTTPClient.Transport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
DisableKeepAlives: true,
MaxIdleConnsPerHost: -1,
DisableCompression: true,
DisableCompression: false,
}
} else if trap.IsSocket {
m.Log.Println("using socket transport")
client.HTTPClient.Transport = trap.SockTransport
} else {
return 0, errors.Errorf("unknown scheme (%s), skipping submission", trap.URL.Scheme)
}
client.RetryWaitMin = 1 * time.Second
client.RetryWaitMax = 5 * time.Second
@@ -138,10 +153,17 @@ func (m *CirconusMetrics) trapCall(payload []byte) (int, error) {
if attempts == client.RetryMax {
m.check.RefreshTrap()
}
return 0, err
return 0, errors.Wrap(err, "trap call")
}
defer resp.Body.Close()
// no content - expected result from
// circonus-agent when metrics accepted
if resp.StatusCode == http.StatusNoContent {
return -1, nil
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
m.Log.Printf("[ERROR] reading body, proceeding. %s\n", err)
@@ -152,7 +174,7 @@ func (m *CirconusMetrics) trapCall(payload []byte) (int, error) {
m.Log.Printf("[ERROR] parsing body, proceeding. %v (%s)\n", err, body)
}
if resp.StatusCode != 200 {
if resp.StatusCode != http.StatusOK {
return 0, errors.New("[ERROR] bad response code: " + strconv.Itoa(resp.StatusCode))
}
switch v := response["stats"].(type) {

View File

@@ -17,7 +17,6 @@ func (m *CirconusMetrics) TrackHTTPLatency(name string, handler func(http.Respon
start := time.Now().UnixNano()
handler(rw, req)
elapsed := time.Now().UnixNano() - start
//hist := m.NewHistogram("go`HTTP`" + req.Method + "`" + name + "`latency")
m.RecordValue("go`HTTP`"+req.Method+"`"+name+"`latency", float64(elapsed)/float64(time.Second))
}
}

View File

@@ -33,7 +33,7 @@ func (m *CirconusMetrics) Reset() {
m.counters = make(map[string]uint64)
m.counterFuncs = make(map[string]func() uint64)
m.gauges = make(map[string]string)
m.gauges = make(map[string]interface{})
m.gaugeFuncs = make(map[string]func() int64)
m.histograms = make(map[string]*Histogram)
m.text = make(map[string]string)
@@ -41,7 +41,7 @@ func (m *CirconusMetrics) Reset() {
}
// snapshot returns a copy of the values of all registered counters and gauges.
func (m *CirconusMetrics) snapshot() (c map[string]uint64, g map[string]string, h map[string]*circonusllhist.Histogram, t map[string]string) {
func (m *CirconusMetrics) snapshot() (c map[string]uint64, g map[string]interface{}, h map[string]*circonusllhist.Histogram, t map[string]string) {
c = m.snapCounters()
g = m.snapGauges()
h = m.snapHistograms()
@@ -68,33 +68,27 @@ func (m *CirconusMetrics) snapCounters() map[string]uint64 {
for n, f := range m.counterFuncs {
c[n] = f()
}
if m.resetCounters && len(c) > 0 {
m.counterFuncs = make(map[string]func() uint64)
}
return c
}
func (m *CirconusMetrics) snapGauges() map[string]string {
func (m *CirconusMetrics) snapGauges() map[string]interface{} {
m.gm.Lock()
defer m.gm.Unlock()
m.gfm.Lock()
defer m.gfm.Unlock()
g := make(map[string]string, len(m.gauges)+len(m.gaugeFuncs))
g := make(map[string]interface{}, len(m.gauges)+len(m.gaugeFuncs))
for n, v := range m.gauges {
g[n] = v
}
if m.resetGauges && len(g) > 0 {
m.gauges = make(map[string]string)
m.gauges = make(map[string]interface{})
}
for n, f := range m.gaugeFuncs {
g[n] = m.gaugeValString(f())
}
if m.resetGauges && len(g) > 0 {
m.gaugeFuncs = make(map[string]func() int64)
g[n] = f()
}
return g
@@ -136,9 +130,6 @@ func (m *CirconusMetrics) snapText() map[string]string {
for n, f := range m.textFuncs {
t[n] = f()
}
if m.resetText && len(t) > 0 {
m.textFuncs = make(map[string]func() string)
}
return t
}

9
vendor/vendor.json vendored
View File

@@ -53,10 +53,10 @@
{"path":"github.com/bgentry/speakeasy/example","revision":"36e9cfdd690967f4f690c6edcc9ffacd006014a0"},
{"path":"github.com/boltdb/bolt","checksumSHA1":"R1Q34Pfnt197F/nCOO9kG8c+Z90=","comment":"v1.2.0","revision":"2f1ce7a837dcb8da3ec595b1dac9d0632f0f99e8","revisionTime":"2017-07-17T17:11:48Z","version":"v1.3.1","versionExact":"v1.3.1"},
{"path":"github.com/burntsushi/toml","checksumSHA1":"InIrfOI7Ys1QqZpCgTB4yW1G32M=","revision":"99064174e013895bbd9b025c31100bd1d9b590ca","revisionTime":"2016-07-17T15:07:09Z"},
{"path":"github.com/circonus-labs/circonus-gometrics","checksumSHA1":"vhCArnFcQRM84iZcfMXka+2OzrE=","revision":"a2c28f079ec3d4fdc17ed577cca75bee88a2da25","revisionTime":"2017-01-31T13:03:52Z"},
{"path":"github.com/circonus-labs/circonus-gometrics/api","checksumSHA1":"sInms3AjZrjG/WCRcmS/NSzLUT4=","revision":"a2c28f079ec3d4fdc17ed577cca75bee88a2da25","revisionTime":"2017-01-31T13:03:52Z"},
{"path":"github.com/circonus-labs/circonus-gometrics/api/config","checksumSHA1":"bQhz/fcyZPmuHSH2qwC4ZtATy5c=","revision":"a2c28f079ec3d4fdc17ed577cca75bee88a2da25","revisionTime":"2017-01-31T13:03:52Z"},
{"path":"github.com/circonus-labs/circonus-gometrics/checkmgr","checksumSHA1":"6hvd+YFb1eWFkc3pVcnOPPa2OVw=","revision":"a2c28f079ec3d4fdc17ed577cca75bee88a2da25","revisionTime":"2017-01-31T13:03:52Z"},
{"path":"github.com/circonus-labs/circonus-gometrics","checksumSHA1":"/qvtQq5y0RZCsRyOOsan87V2AL0=","revision":"dd698dc110872f6e554abf74a7740fc363354086","revisionTime":"2018-08-20T20:09:38Z"},
{"path":"github.com/circonus-labs/circonus-gometrics/api","checksumSHA1":"Lll5SHEsVto8Eqbrj7NVj7BfgDI=","revision":"dd698dc110872f6e554abf74a7740fc363354086","revisionTime":"2018-08-20T20:09:38Z"},
{"path":"github.com/circonus-labs/circonus-gometrics/api/config","checksumSHA1":"bQhz/fcyZPmuHSH2qwC4ZtATy5c=","revision":"dd698dc110872f6e554abf74a7740fc363354086","revisionTime":"2018-08-20T20:09:38Z"},
{"path":"github.com/circonus-labs/circonus-gometrics/checkmgr","checksumSHA1":"Ij8yB33E0Kk+GfTkNRoF1mG26dc=","revision":"dd698dc110872f6e554abf74a7740fc363354086","revisionTime":"2018-08-20T20:09:38Z"},
{"path":"github.com/circonus-labs/circonusllhist","checksumSHA1":"VbfeVqeOM+dTNxCmpvmYS0LwQn0=","revision":"7d649b46cdc2cd2ed102d350688a75a4fd7778c6","revisionTime":"2016-11-21T13:51:53Z"},
{"path":"github.com/containernetworking/cni/pkg/types","checksumSHA1":"NeAp/3+Hedu9tnMai+LihERPj84=","revision":"5c3c17164270150467498a32c71436c7cd5501be","revisionTime":"2016-06-02T16:00:07Z"},
{"path":"github.com/coreos/go-semver/semver","checksumSHA1":"97BsbXOiZ8+Kr+LIuZkQFtSj7H4=","revision":"1817cd4bea52af76542157eeabd74b057d1a199e","revisionTime":"2017-06-13T09:22:38Z"},
@@ -307,6 +307,7 @@
{"path":"github.com/stretchr/testify/require","checksumSHA1":"KqYmXUcuGwsvBL6XVsQnXsFb3LI=","revision":"c679ae2cc0cb27ec3293fea7e254e47386f05d69","revisionTime":"2018-03-14T08:05:35Z"},
{"path":"github.com/syndtr/gocapability/capability","checksumSHA1":"PgEklGW56c5RLHqQhORxt6jS3fY=","revision":"db04d3cc01c8b54962a58ec7e491717d06cfcc16","revisionTime":"2017-07-04T07:02:18Z"},
{"path":"github.com/tonnerre/golang-text","checksumSHA1":"t24KnvC9jRxiANVhpw2pqFpmEu8=","revision":"048ed3d792f7104850acbc8cfc01e5a6070f4c04","revisionTime":"2013-09-25T19:58:46Z"},
{"path":"github.com/tv42/httpunix","checksumSHA1":"2xcr/mhxBFlDjpxe/Mc2Wb4RGR8=","revision":"b75d8614f926c077e48d85f1f8f7885b758c6225","revisionTime":"2015-04-27T01:28:21Z"},
{"path":"github.com/ugorji/go/codec","checksumSHA1":"8G1zvpE4gTtWQRuP/x2HPVDmflo=","revision":"0053ebfd9d0ee06ccefbfe17072021e1d4acebee","revisionTime":"2017-06-20T06:01:02Z"},
{"path":"github.com/ugorji/go/codec/codecgen","checksumSHA1":"OgParimNuU2CJqr3pcTympeQZUc=","revision":"5efa3251c7f7d05e5d9704a69a984ec9f1386a40","revisionTime":"2017-06-20T10:48:52Z"},
{"path":"github.com/ulikunitz/xz","checksumSHA1":"z2kAtVle4NFV2OExI85fZoTcsI4=","revision":"0c6b41e72360850ca4f98dc341fd999726ea007f","revisionTime":"2017-06-05T21:53:11Z"},