1// Copyright 2016 Circonus, Inc. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package checkmgr provides a check management interace to circonus-gometrics
6package checkmgr
7
8import (
9	"crypto/tls"
10	"crypto/x509"
11	"errors"
12	"fmt"
13	"io/ioutil"
14	"log"
15	"net/url"
16	"os"
17	"path"
18	"strconv"
19	"strings"
20	"sync"
21	"time"
22
23	"github.com/circonus-labs/circonus-gometrics/api"
24)
25
26// Check management offers:
27//
28// Create a check if one cannot be found matching specific criteria
29// Manage metrics in the supplied check (enabling new metrics as they are submitted)
30//
31// To disable check management, leave Config.Api.Token.Key blank
32//
33// use cases:
34// configure without api token - check management disabled
35//  - configuration parameters other than Check.SubmissionUrl, Debug and Log are ignored
36//  - note: SubmissionUrl is **required** in this case as there is no way to derive w/o api
37// configure with api token - check management enabled
38//  - all otehr configuration parameters affect how the trap url is obtained
39//    1. provided (Check.SubmissionUrl)
40//    2. via check lookup (CheckConfig.Id)
41//    3. via a search using CheckConfig.InstanceId + CheckConfig.SearchTag
42//    4. a new check is created
43
44const (
45	defaultCheckType             = "httptrap"
46	defaultTrapMaxURLAge         = "60s"   // 60 seconds
47	defaultBrokerMaxResponseTime = "500ms" // 500 milliseconds
48	defaultForceMetricActivation = "false"
49	statusActive                 = "active"
50)
51
52// CheckConfig options for check
53type CheckConfig struct {
54	// a specific submission url
55	SubmissionURL string
56	// a specific check id (not check bundle id)
57	ID string
58	// unique instance id string
59	// used to search for a check to use
60	// used as check.target when creating a check
61	InstanceID string
62	// unique check searching tag (or tags)
63	// used to search for a check to use (combined with instanceid)
64	// used as a regular tag when creating a check
65	SearchTag string
66	// a custom display name for the check (as viewed in UI Checks)
67	DisplayName string
68	// httptrap check secret (for creating a check)
69	Secret string
70	// additional tags to add to a check (when creating a check)
71	// these tags will not be added to an existing check
72	Tags string
73	// max amount of time to to hold on to a submission url
74	// when a given submission fails (due to retries) if the
75	// time the url was last updated is > than this, the trap
76	// url will be refreshed (e.g. if the broker is changed
77	// in the UI) **only relevant when check management is enabled**
78	// e.g. 5m, 30m, 1h, etc.
79	MaxURLAge string
80	// force metric activation - if a metric has been disabled via the UI
81	// the default behavior is to *not* re-activate the metric; this setting
82	// overrides the behavior and will re-activate the metric when it is
83	// encountered. "(true|false)", default "false"
84	ForceMetricActivation string
85}
86
87// BrokerConfig options for broker
88type BrokerConfig struct {
89	// a specific broker id (numeric portion of cid)
90	ID string
91	// one or more tags used to select 1-n brokers from which to select
92	// when creating a new check (e.g. datacenter:abc or loc:dfw,dc:abc)
93	SelectTag string
94	// for a broker to be considered viable it must respond to a
95	// connection attempt within this amount of time e.g. 200ms, 2s, 1m
96	MaxResponseTime string
97}
98
99// Config options
100type Config struct {
101	Log   *log.Logger
102	Debug bool
103
104	// Circonus API config
105	API api.Config
106	// Check specific configuration options
107	Check CheckConfig
108	// Broker specific configuration options
109	Broker BrokerConfig
110}
111
112// CheckTypeType check type
113type CheckTypeType string
114
115// CheckInstanceIDType check instance id
116type CheckInstanceIDType string
117
118// CheckSecretType check secret
119type CheckSecretType string
120
121// CheckTagsType check tags
122type CheckTagsType string
123
124// CheckDisplayNameType check display name
125type CheckDisplayNameType string
126
127// BrokerCNType broker common name
128type BrokerCNType string
129
130// CheckManager settings
131type CheckManager struct {
132	enabled bool
133	Log     *log.Logger
134	Debug   bool
135	apih    *api.API
136
137	// check
138	checkType             CheckTypeType
139	checkID               api.IDType
140	checkInstanceID       CheckInstanceIDType
141	checkTarget           string
142	checkSearchTag        api.TagType
143	checkSecret           CheckSecretType
144	checkTags             api.TagType
145	checkSubmissionURL    api.URLType
146	checkDisplayName      CheckDisplayNameType
147	forceMetricActivation bool
148	forceCheckUpdate      bool
149
150	// metric tags
151	metricTags map[string][]string
152	mtmu       sync.Mutex
153
154	// broker
155	brokerID              api.IDType
156	brokerSelectTag       api.TagType
157	brokerMaxResponseTime time.Duration
158
159	// state
160	checkBundle      *api.CheckBundle
161	cbmu             sync.Mutex
162	availableMetrics map[string]bool
163	trapURL          api.URLType
164	trapCN           BrokerCNType
165	trapLastUpdate   time.Time
166	trapMaxURLAge    time.Duration
167	trapmu           sync.Mutex
168	certPool         *x509.CertPool
169}
170
171// Trap config
172type Trap struct {
173	URL *url.URL
174	TLS *tls.Config
175}
176
177// NewCheckManager returns a new check manager
178func NewCheckManager(cfg *Config) (*CheckManager, error) {
179
180	if cfg == nil {
181		return nil, errors.New("Invalid Check Manager configuration (nil).")
182	}
183
184	cm := &CheckManager{
185		enabled: false,
186	}
187
188	cm.Debug = cfg.Debug
189	cm.Log = cfg.Log
190	if cm.Debug && cm.Log == nil {
191		cm.Log = log.New(os.Stderr, "", log.LstdFlags)
192	}
193	if cm.Log == nil {
194		cm.Log = log.New(ioutil.Discard, "", log.LstdFlags)
195	}
196
197	if cfg.Check.SubmissionURL != "" {
198		cm.checkSubmissionURL = api.URLType(cfg.Check.SubmissionURL)
199	}
200	// Blank API Token *disables* check management
201	if cfg.API.TokenKey == "" {
202		if cm.checkSubmissionURL == "" {
203			return nil, errors.New("Invalid check manager configuration (no API token AND no submission url).")
204		}
205		if err := cm.initializeTrapURL(); err != nil {
206			return nil, err
207		}
208		return cm, nil
209	}
210
211	// enable check manager
212
213	cm.enabled = true
214
215	// initialize api handle
216
217	cfg.API.Debug = cm.Debug
218	cfg.API.Log = cm.Log
219
220	apih, err := api.NewAPI(&cfg.API)
221	if err != nil {
222		return nil, err
223	}
224	cm.apih = apih
225
226	// initialize check related data
227
228	cm.checkType = defaultCheckType
229
230	idSetting := "0"
231	if cfg.Check.ID != "" {
232		idSetting = cfg.Check.ID
233	}
234	id, err := strconv.Atoi(idSetting)
235	if err != nil {
236		return nil, err
237	}
238	cm.checkID = api.IDType(id)
239
240	cm.checkInstanceID = CheckInstanceIDType(cfg.Check.InstanceID)
241	cm.checkDisplayName = CheckDisplayNameType(cfg.Check.DisplayName)
242	cm.checkSecret = CheckSecretType(cfg.Check.Secret)
243
244	fma := defaultForceMetricActivation
245	if cfg.Check.ForceMetricActivation != "" {
246		fma = cfg.Check.ForceMetricActivation
247	}
248	fm, err := strconv.ParseBool(fma)
249	if err != nil {
250		return nil, err
251	}
252	cm.forceMetricActivation = fm
253
254	_, an := path.Split(os.Args[0])
255	hn, err := os.Hostname()
256	if err != nil {
257		hn = "unknown"
258	}
259	if cm.checkInstanceID == "" {
260		cm.checkInstanceID = CheckInstanceIDType(fmt.Sprintf("%s:%s", hn, an))
261	}
262	cm.checkTarget = hn
263
264	if cfg.Check.SearchTag == "" {
265		cm.checkSearchTag = []string{fmt.Sprintf("service:%s", an)}
266	} else {
267		cm.checkSearchTag = strings.Split(strings.Replace(cfg.Check.SearchTag, " ", "", -1), ",")
268	}
269
270	if cfg.Check.Tags != "" {
271		cm.checkTags = strings.Split(strings.Replace(cfg.Check.Tags, " ", "", -1), ",")
272	}
273
274	if cm.checkDisplayName == "" {
275		cm.checkDisplayName = CheckDisplayNameType(fmt.Sprintf("%s", string(cm.checkInstanceID)))
276	}
277
278	dur := cfg.Check.MaxURLAge
279	if dur == "" {
280		dur = defaultTrapMaxURLAge
281	}
282	maxDur, err := time.ParseDuration(dur)
283	if err != nil {
284		return nil, err
285	}
286	cm.trapMaxURLAge = maxDur
287
288	// setup broker
289
290	idSetting = "0"
291	if cfg.Broker.ID != "" {
292		idSetting = cfg.Broker.ID
293	}
294	id, err = strconv.Atoi(idSetting)
295	if err != nil {
296		return nil, err
297	}
298	cm.brokerID = api.IDType(id)
299
300	if cfg.Broker.SelectTag != "" {
301		cm.brokerSelectTag = strings.Split(strings.Replace(cfg.Broker.SelectTag, " ", "", -1), ",")
302	}
303
304	dur = cfg.Broker.MaxResponseTime
305	if dur == "" {
306		dur = defaultBrokerMaxResponseTime
307	}
308	maxDur, err = time.ParseDuration(dur)
309	if err != nil {
310		return nil, err
311	}
312	cm.brokerMaxResponseTime = maxDur
313
314	// metrics
315	cm.availableMetrics = make(map[string]bool)
316	cm.metricTags = make(map[string][]string)
317
318	if err := cm.initializeTrapURL(); err != nil {
319		return nil, err
320	}
321
322	return cm, nil
323}
324
325// GetTrap return the trap url
326func (cm *CheckManager) GetTrap() (*Trap, error) {
327	if cm.trapURL == "" {
328		if err := cm.initializeTrapURL(); err != nil {
329			return nil, err
330		}
331	}
332
333	trap := &Trap{}
334
335	u, err := url.Parse(string(cm.trapURL))
336	if err != nil {
337		return nil, err
338	}
339
340	trap.URL = u
341
342	if u.Scheme == "https" {
343		if cm.certPool == nil {
344			cm.loadCACert()
345		}
346		t := &tls.Config{
347			RootCAs: cm.certPool,
348		}
349		if cm.trapCN != "" {
350			t.ServerName = string(cm.trapCN)
351		}
352		trap.TLS = t
353	}
354
355	return trap, nil
356}
357
358// ResetTrap URL, force request to the API for the submission URL and broker ca cert
359func (cm *CheckManager) ResetTrap() error {
360	if cm.trapURL == "" {
361		return nil
362	}
363
364	cm.trapURL = ""
365	cm.certPool = nil
366	err := cm.initializeTrapURL()
367	return err
368}
369
370// RefreshTrap check when the last time the URL was reset, reset if needed
371func (cm *CheckManager) RefreshTrap() {
372	if cm.trapURL == "" {
373		return
374	}
375
376	if time.Since(cm.trapLastUpdate) >= cm.trapMaxURLAge {
377		cm.ResetTrap()
378	}
379}
380