check.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420
  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. package checkmgr
  5. import (
  6. "crypto/rand"
  7. "crypto/sha256"
  8. "encoding/hex"
  9. "errors"
  10. "fmt"
  11. "net/url"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "github.com/circonus-labs/circonus-gometrics/api"
  16. "github.com/circonus-labs/circonus-gometrics/api/config"
  17. )
  18. // UpdateCheck determines if the check needs to be updated (new metrics, tags, etc.)
  19. func (cm *CheckManager) UpdateCheck(newMetrics map[string]*api.CheckBundleMetric) {
  20. // only if check manager is enabled
  21. if !cm.enabled {
  22. return
  23. }
  24. // only if checkBundle has been populated
  25. if cm.checkBundle == nil {
  26. return
  27. }
  28. // only if there is *something* to update
  29. if !cm.forceCheckUpdate && len(newMetrics) == 0 && len(cm.metricTags) == 0 {
  30. return
  31. }
  32. // refresh check bundle (in case there were changes made by other apps or in UI)
  33. cid := cm.checkBundle.CID
  34. checkBundle, err := cm.apih.FetchCheckBundle(api.CIDType(&cid))
  35. if err != nil {
  36. cm.Log.Printf("[ERROR] unable to fetch up-to-date check bundle %v", err)
  37. return
  38. }
  39. cm.cbmu.Lock()
  40. cm.checkBundle = checkBundle
  41. cm.cbmu.Unlock()
  42. // check metric_limit and see if it’s 0, if so, don't even bother to try to update the check.
  43. cm.addNewMetrics(newMetrics)
  44. if len(cm.metricTags) > 0 {
  45. // note: if a tag has been added (queued) for a metric which never gets sent
  46. // the tags will be discarded. (setting tags does not *create* metrics.)
  47. for metricName, metricTags := range cm.metricTags {
  48. for metricIdx, metric := range cm.checkBundle.Metrics {
  49. if metric.Name == metricName {
  50. cm.checkBundle.Metrics[metricIdx].Tags = metricTags
  51. break
  52. }
  53. }
  54. cm.mtmu.Lock()
  55. delete(cm.metricTags, metricName)
  56. cm.mtmu.Unlock()
  57. }
  58. cm.forceCheckUpdate = true
  59. }
  60. if cm.forceCheckUpdate {
  61. newCheckBundle, err := cm.apih.UpdateCheckBundle(cm.checkBundle)
  62. if err != nil {
  63. cm.Log.Printf("[ERROR] updating check bundle %v", err)
  64. return
  65. }
  66. cm.forceCheckUpdate = false
  67. cm.cbmu.Lock()
  68. cm.checkBundle = newCheckBundle
  69. cm.cbmu.Unlock()
  70. cm.inventoryMetrics()
  71. }
  72. }
  73. // Initialize CirconusMetrics instance. Attempt to find a check otherwise create one.
  74. // use cases:
  75. //
  76. // check [bundle] by submission url
  77. // check [bundle] by *check* id (note, not check_bundle id)
  78. // check [bundle] by search
  79. // create check [bundle]
  80. func (cm *CheckManager) initializeTrapURL() error {
  81. if cm.trapURL != "" {
  82. return nil
  83. }
  84. cm.trapmu.Lock()
  85. defer cm.trapmu.Unlock()
  86. // special case short-circuit: just send to a url, no check management
  87. // up to user to ensure that if url is https that it will work (e.g. not self-signed)
  88. if cm.checkSubmissionURL != "" {
  89. if !cm.enabled {
  90. cm.trapURL = cm.checkSubmissionURL
  91. cm.trapLastUpdate = time.Now()
  92. return nil
  93. }
  94. }
  95. if !cm.enabled {
  96. return errors.New("unable to initialize trap, check manager is disabled")
  97. }
  98. var err error
  99. var check *api.Check
  100. var checkBundle *api.CheckBundle
  101. var broker *api.Broker
  102. if cm.checkSubmissionURL != "" {
  103. check, err = cm.fetchCheckBySubmissionURL(cm.checkSubmissionURL)
  104. if err != nil {
  105. return err
  106. }
  107. if !check.Active {
  108. return fmt.Errorf("[ERROR] Check ID %v is not active", check.CID)
  109. }
  110. // extract check id from check object returned from looking up using submission url
  111. // set m.CheckId to the id
  112. // set m.SubmissionUrl to "" to prevent trying to search on it going forward
  113. // use case: if the broker is changed in the UI metrics would stop flowing
  114. // unless the new submission url can be fetched with the API (which is no
  115. // longer possible using the original submission url)
  116. var id int
  117. id, err = strconv.Atoi(strings.Replace(check.CID, "/check/", "", -1))
  118. if err == nil {
  119. cm.checkID = api.IDType(id)
  120. cm.checkSubmissionURL = ""
  121. } else {
  122. cm.Log.Printf(
  123. "[WARN] SubmissionUrl check to Check ID: unable to convert %s to int %q\n",
  124. check.CID, err)
  125. }
  126. } else if cm.checkID > 0 {
  127. cid := fmt.Sprintf("/check/%d", cm.checkID)
  128. check, err = cm.apih.FetchCheck(api.CIDType(&cid))
  129. if err != nil {
  130. return err
  131. }
  132. if !check.Active {
  133. return fmt.Errorf("[ERROR] Check ID %v is not active", check.CID)
  134. }
  135. } else {
  136. if checkBundle == nil {
  137. // old search (instanceid as check.target)
  138. searchCriteria := fmt.Sprintf(
  139. "(active:1)(type:\"%s\")(host:\"%s\")(tags:%s)", cm.checkType, cm.checkTarget, strings.Join(cm.checkSearchTag, ","))
  140. checkBundle, err = cm.checkBundleSearch(searchCriteria, map[string][]string{})
  141. if err != nil {
  142. return err
  143. }
  144. }
  145. if checkBundle == nil {
  146. // new search (check.target != instanceid, instanceid encoded in notes field)
  147. searchCriteria := fmt.Sprintf(
  148. "(active:1)(type:\"%s\")(tags:%s)", cm.checkType, strings.Join(cm.checkSearchTag, ","))
  149. filterCriteria := map[string][]string{"f_notes": {*cm.getNotes()}}
  150. checkBundle, err = cm.checkBundleSearch(searchCriteria, filterCriteria)
  151. if err != nil {
  152. return err
  153. }
  154. }
  155. if checkBundle == nil {
  156. // err==nil && checkBundle==nil is "no check bundles matched"
  157. // an error *should* be returned for any other invalid scenario
  158. checkBundle, broker, err = cm.createNewCheck()
  159. if err != nil {
  160. return err
  161. }
  162. }
  163. }
  164. if checkBundle == nil {
  165. if check != nil {
  166. cid := check.CheckBundleCID
  167. checkBundle, err = cm.apih.FetchCheckBundle(api.CIDType(&cid))
  168. if err != nil {
  169. return err
  170. }
  171. } else {
  172. return fmt.Errorf("[ERROR] Unable to retrieve, find, or create check")
  173. }
  174. }
  175. if broker == nil {
  176. cid := checkBundle.Brokers[0]
  177. broker, err = cm.apih.FetchBroker(api.CIDType(&cid))
  178. if err != nil {
  179. return err
  180. }
  181. }
  182. // retain to facilitate metric management (adding new metrics specifically)
  183. cm.checkBundle = checkBundle
  184. cm.inventoryMetrics()
  185. // determine the trap url to which metrics should be PUT
  186. if checkBundle.Type == "httptrap" {
  187. if turl, found := checkBundle.Config[config.SubmissionURL]; found {
  188. cm.trapURL = api.URLType(turl)
  189. } else {
  190. if cm.Debug {
  191. cm.Log.Printf("Missing config.%s %+v", config.SubmissionURL, checkBundle)
  192. }
  193. return fmt.Errorf("[ERROR] Unable to use check, no %s in config", config.SubmissionURL)
  194. }
  195. } else {
  196. // build a submission_url for non-httptrap checks out of mtev_reverse url
  197. if len(checkBundle.ReverseConnectURLs) == 0 {
  198. return fmt.Errorf("%s is not an HTTPTRAP check and no reverse connection urls found", checkBundle.Checks[0])
  199. }
  200. mtevURL := checkBundle.ReverseConnectURLs[0]
  201. mtevURL = strings.Replace(mtevURL, "mtev_reverse", "https", 1)
  202. mtevURL = strings.Replace(mtevURL, "check", "module/httptrap", 1)
  203. if rs, found := checkBundle.Config[config.ReverseSecretKey]; found {
  204. cm.trapURL = api.URLType(fmt.Sprintf("%s/%s", mtevURL, rs))
  205. } else {
  206. if cm.Debug {
  207. cm.Log.Printf("Missing config.%s %+v", config.ReverseSecretKey, checkBundle)
  208. }
  209. return fmt.Errorf("[ERROR] Unable to use check, no %s in config", config.ReverseSecretKey)
  210. }
  211. }
  212. // used when sending as "ServerName" get around certs not having IP SANS
  213. // (cert created with server name as CN but IP used in trap url)
  214. cn, err := cm.getBrokerCN(broker, cm.trapURL)
  215. if err != nil {
  216. return err
  217. }
  218. cm.trapCN = BrokerCNType(cn)
  219. if cm.enabled {
  220. u, err := url.Parse(string(cm.trapURL))
  221. if err != nil {
  222. return err
  223. }
  224. if u.Scheme == "https" {
  225. if err := cm.loadCACert(); err != nil {
  226. return err
  227. }
  228. }
  229. }
  230. cm.trapLastUpdate = time.Now()
  231. return nil
  232. }
  233. // Search for a check bundle given a predetermined set of criteria
  234. func (cm *CheckManager) checkBundleSearch(criteria string, filter map[string][]string) (*api.CheckBundle, error) {
  235. search := api.SearchQueryType(criteria)
  236. checkBundles, err := cm.apih.SearchCheckBundles(&search, &filter)
  237. if err != nil {
  238. return nil, err
  239. }
  240. if len(*checkBundles) == 0 {
  241. return nil, nil // trigger creation of a new check
  242. }
  243. numActive := 0
  244. checkID := -1
  245. for idx, check := range *checkBundles {
  246. if check.Status == statusActive {
  247. numActive++
  248. checkID = idx
  249. }
  250. }
  251. if numActive > 1 {
  252. return nil, fmt.Errorf("[ERROR] multiple check bundles match criteria %s", criteria)
  253. }
  254. bundle := (*checkBundles)[checkID]
  255. return &bundle, nil
  256. }
  257. // Create a new check to receive metrics
  258. func (cm *CheckManager) createNewCheck() (*api.CheckBundle, *api.Broker, error) {
  259. checkSecret := string(cm.checkSecret)
  260. if checkSecret == "" {
  261. secret, err := cm.makeSecret()
  262. if err != nil {
  263. secret = "myS3cr3t"
  264. }
  265. checkSecret = secret
  266. }
  267. broker, err := cm.getBroker()
  268. if err != nil {
  269. return nil, nil, err
  270. }
  271. chkcfg := &api.CheckBundle{
  272. Brokers: []string{broker.CID},
  273. Config: make(map[config.Key]string),
  274. DisplayName: string(cm.checkDisplayName),
  275. Metrics: []api.CheckBundleMetric{},
  276. MetricLimit: config.DefaultCheckBundleMetricLimit,
  277. Notes: cm.getNotes(),
  278. Period: 60,
  279. Status: statusActive,
  280. Tags: append(cm.checkSearchTag, cm.checkTags...),
  281. Target: string(cm.checkTarget),
  282. Timeout: 10,
  283. Type: string(cm.checkType),
  284. }
  285. if len(cm.customConfigFields) > 0 {
  286. for fld, val := range cm.customConfigFields {
  287. chkcfg.Config[config.Key(fld)] = val
  288. }
  289. }
  290. //
  291. // use the default config settings if these are NOT set by user configuration
  292. //
  293. if val, ok := chkcfg.Config[config.AsyncMetrics]; !ok || val == "" {
  294. chkcfg.Config[config.AsyncMetrics] = "true"
  295. }
  296. if val, ok := chkcfg.Config[config.Secret]; !ok || val == "" {
  297. chkcfg.Config[config.Secret] = checkSecret
  298. }
  299. checkBundle, err := cm.apih.CreateCheckBundle(chkcfg)
  300. if err != nil {
  301. return nil, nil, err
  302. }
  303. return checkBundle, broker, nil
  304. }
  305. // Create a dynamic secret to use with a new check
  306. func (cm *CheckManager) makeSecret() (string, error) {
  307. hash := sha256.New()
  308. x := make([]byte, 2048)
  309. if _, err := rand.Read(x); err != nil {
  310. return "", err
  311. }
  312. hash.Write(x)
  313. return hex.EncodeToString(hash.Sum(nil))[0:16], nil
  314. }
  315. func (cm *CheckManager) getNotes() *string {
  316. notes := fmt.Sprintf("cgm_instanceid|%s", cm.checkInstanceID)
  317. return &notes
  318. }
  319. // FetchCheckBySubmissionURL fetch a check configuration by submission_url
  320. func (cm *CheckManager) fetchCheckBySubmissionURL(submissionURL api.URLType) (*api.Check, error) {
  321. if string(submissionURL) == "" {
  322. return nil, errors.New("[ERROR] Invalid submission URL (blank)")
  323. }
  324. u, err := url.Parse(string(submissionURL))
  325. if err != nil {
  326. return nil, err
  327. }
  328. // valid trap url: scheme://host[:port]/module/httptrap/UUID/secret
  329. // does it smell like a valid trap url path
  330. if !strings.Contains(u.Path, "/module/httptrap/") {
  331. return nil, fmt.Errorf("[ERROR] Invalid submission URL '%s', unrecognized path", submissionURL)
  332. }
  333. // extract uuid
  334. pathParts := strings.Split(strings.Replace(u.Path, "/module/httptrap/", "", 1), "/")
  335. if len(pathParts) != 2 {
  336. return nil, fmt.Errorf("[ERROR] Invalid submission URL '%s', UUID not where expected", submissionURL)
  337. }
  338. uuid := pathParts[0]
  339. filter := api.SearchFilterType{"f__check_uuid": []string{uuid}}
  340. checks, err := cm.apih.SearchChecks(nil, &filter)
  341. if err != nil {
  342. return nil, err
  343. }
  344. if len(*checks) == 0 {
  345. return nil, fmt.Errorf("[ERROR] No checks found with UUID %s", uuid)
  346. }
  347. numActive := 0
  348. checkID := -1
  349. for idx, check := range *checks {
  350. if check.Active {
  351. numActive++
  352. checkID = idx
  353. }
  354. }
  355. if numActive > 1 {
  356. return nil, fmt.Errorf("[ERROR] Multiple checks with same UUID %s", uuid)
  357. }
  358. check := (*checks)[checkID]
  359. return &check, nil
  360. }