github.com/netdata/go.d.plugin@v0.58.1/modules/couchdb/collect.go (about)

     1  // SPDX-License-Identifier: GPL-3.0-or-later
     2  
     3  package couchdb
     4  
     5  import (
     6  	"bytes"
     7  	"encoding/json"
     8  	"errors"
     9  	"fmt"
    10  	"io"
    11  	"math"
    12  	"net/http"
    13  	"strings"
    14  	"sync"
    15  
    16  	"github.com/netdata/go.d.plugin/pkg/stm"
    17  	"github.com/netdata/go.d.plugin/pkg/web"
    18  )
    19  
    20  const (
    21  	urlPathActiveTasks   = "/_active_tasks"
    22  	urlPathOverviewStats = "/_node/%s/_stats"
    23  	urlPathSystemStats   = "/_node/%s/_system"
    24  	urlPathDatabases     = "/_dbs_info"
    25  
    26  	httpStatusCodePrefix    = "couchdb_httpd_status_codes_"
    27  	httpStatusCodePrefixLen = len(httpStatusCodePrefix)
    28  )
    29  
    30  func (cdb *CouchDB) collect() (map[string]int64, error) {
    31  	ms := cdb.scrapeCouchDB()
    32  	if ms.empty() {
    33  		return nil, nil
    34  	}
    35  
    36  	collected := make(map[string]int64)
    37  	cdb.collectNodeStats(collected, ms)
    38  	cdb.collectSystemStats(collected, ms)
    39  	cdb.collectActiveTasks(collected, ms)
    40  	cdb.collectDBStats(collected, ms)
    41  
    42  	return collected, nil
    43  }
    44  
    45  func (CouchDB) collectNodeStats(collected map[string]int64, ms *cdbMetrics) {
    46  	if !ms.hasNodeStats() {
    47  		return
    48  	}
    49  
    50  	for metric, value := range stm.ToMap(ms.NodeStats) {
    51  		collected[metric] = value
    52  		if strings.HasPrefix(metric, httpStatusCodePrefix) {
    53  			code := metric[httpStatusCodePrefixLen:]
    54  			collected["couchdb_httpd_status_codes_"+string(code[0])+"xx"] += value
    55  		}
    56  	}
    57  }
    58  
    59  func (CouchDB) collectSystemStats(collected map[string]int64, ms *cdbMetrics) {
    60  	if !ms.hasNodeSystem() {
    61  		return
    62  	}
    63  
    64  	for metric, value := range stm.ToMap(ms.NodeSystem) {
    65  		collected[metric] = value
    66  	}
    67  
    68  	collected["peak_msg_queue"] = findMaxMQSize(ms.NodeSystem.MessageQueues)
    69  }
    70  
    71  func (CouchDB) collectActiveTasks(collected map[string]int64, ms *cdbMetrics) {
    72  	collected["active_tasks_indexer"] = 0
    73  	collected["active_tasks_database_compaction"] = 0
    74  	collected["active_tasks_replication"] = 0
    75  	collected["active_tasks_view_compaction"] = 0
    76  
    77  	if !ms.hasActiveTasks() {
    78  		return
    79  	}
    80  
    81  	for _, task := range ms.ActiveTasks {
    82  		collected["active_tasks_"+task.Type]++
    83  	}
    84  }
    85  
    86  func (cdb *CouchDB) collectDBStats(collected map[string]int64, ms *cdbMetrics) {
    87  	if !ms.hasDBStats() {
    88  		return
    89  	}
    90  
    91  	for _, dbStats := range ms.DBStats {
    92  		if dbStats.Error != "" {
    93  			cdb.Warning("database '", dbStats.Key, "' doesn't exist")
    94  			continue
    95  		}
    96  		merge(collected, stm.ToMap(dbStats.Info), "db_"+dbStats.Key)
    97  	}
    98  }
    99  
   100  func (cdb *CouchDB) scrapeCouchDB() *cdbMetrics {
   101  	ms := &cdbMetrics{}
   102  	wg := &sync.WaitGroup{}
   103  
   104  	wg.Add(1)
   105  	go func() { defer wg.Done(); cdb.scrapeNodeStats(ms) }()
   106  
   107  	wg.Add(1)
   108  	go func() { defer wg.Done(); cdb.scrapeSystemStats(ms) }()
   109  
   110  	wg.Add(1)
   111  	go func() { defer wg.Done(); cdb.scrapeActiveTasks(ms) }()
   112  
   113  	if len(cdb.databases) > 0 {
   114  		wg.Add(1)
   115  		go func() { defer wg.Done(); cdb.scrapeDBStats(ms) }()
   116  	}
   117  
   118  	wg.Wait()
   119  	return ms
   120  }
   121  
   122  func (cdb *CouchDB) scrapeNodeStats(ms *cdbMetrics) {
   123  	req, _ := web.NewHTTPRequest(cdb.Request)
   124  	req.URL.Path = fmt.Sprintf(urlPathOverviewStats, cdb.Config.Node)
   125  
   126  	var stats cdbNodeStats
   127  	if err := cdb.doOKDecode(req, &stats); err != nil {
   128  		cdb.Warning(err)
   129  		return
   130  	}
   131  	ms.NodeStats = &stats
   132  }
   133  
   134  func (cdb *CouchDB) scrapeSystemStats(ms *cdbMetrics) {
   135  	req, _ := web.NewHTTPRequest(cdb.Request)
   136  	req.URL.Path = fmt.Sprintf(urlPathSystemStats, cdb.Config.Node)
   137  
   138  	var stats cdbNodeSystem
   139  	if err := cdb.doOKDecode(req, &stats); err != nil {
   140  		cdb.Warning(err)
   141  		return
   142  	}
   143  	ms.NodeSystem = &stats
   144  }
   145  
   146  func (cdb *CouchDB) scrapeActiveTasks(ms *cdbMetrics) {
   147  	req, _ := web.NewHTTPRequest(cdb.Request)
   148  	req.URL.Path = urlPathActiveTasks
   149  
   150  	var stats []cdbActiveTask
   151  	if err := cdb.doOKDecode(req, &stats); err != nil {
   152  		cdb.Warning(err)
   153  		return
   154  	}
   155  	ms.ActiveTasks = stats
   156  }
   157  
   158  func (cdb *CouchDB) scrapeDBStats(ms *cdbMetrics) {
   159  	req, _ := web.NewHTTPRequest(cdb.Request)
   160  	req.URL.Path = urlPathDatabases
   161  	req.Method = http.MethodPost
   162  	req.Header.Add("Accept", "application/json")
   163  	req.Header.Add("Content-Type", "application/json")
   164  
   165  	var q struct {
   166  		Keys []string `json:"keys"`
   167  	}
   168  	q.Keys = cdb.databases
   169  	body, err := json.Marshal(q)
   170  	if err != nil {
   171  		cdb.Error(err)
   172  		return
   173  	}
   174  	req.Body = io.NopCloser(bytes.NewReader(body))
   175  
   176  	var stats []cdbDBStats
   177  	if err := cdb.doOKDecode(req, &stats); err != nil {
   178  		cdb.Warning(err)
   179  		return
   180  	}
   181  	ms.DBStats = stats
   182  }
   183  
   184  func findMaxMQSize(MessageQueues map[string]interface{}) int64 {
   185  	var max float64
   186  	for _, mq := range MessageQueues {
   187  		switch mqSize := mq.(type) {
   188  		case float64:
   189  			max = math.Max(max, mqSize)
   190  		case map[string]interface{}:
   191  			if v, ok := mqSize["count"].(float64); ok {
   192  				max = math.Max(max, v)
   193  			}
   194  		}
   195  	}
   196  	return int64(max)
   197  }
   198  
   199  func (cdb *CouchDB) pingCouchDB() error {
   200  	req, _ := web.NewHTTPRequest(cdb.Request)
   201  
   202  	var info struct{ Couchdb string }
   203  	if err := cdb.doOKDecode(req, &info); err != nil {
   204  		return err
   205  	}
   206  
   207  	if info.Couchdb != "Welcome" {
   208  		return errors.New("not a CouchDB endpoint")
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func (cdb *CouchDB) doOKDecode(req *http.Request, in interface{}) error {
   215  	resp, err := cdb.httpClient.Do(req)
   216  	if err != nil {
   217  		return fmt.Errorf("error on HTTP request '%s': %v", req.URL, err)
   218  	}
   219  	defer closeBody(resp)
   220  
   221  	// TODO: read resp body, it contains reason
   222  	// ex.: {"error":"bad_request","reason":"`keys` member must exist."} (400)
   223  	if resp.StatusCode != http.StatusOK {
   224  		return fmt.Errorf("'%s' returned HTTP status code: %d", req.URL, resp.StatusCode)
   225  	}
   226  
   227  	if err := json.NewDecoder(resp.Body).Decode(in); err != nil {
   228  		return fmt.Errorf("error on decoding response from '%s': %v", req.URL, err)
   229  	}
   230  	return nil
   231  }
   232  
   233  func closeBody(resp *http.Response) {
   234  	if resp != nil && resp.Body != nil {
   235  		_, _ = io.Copy(io.Discard, resp.Body)
   236  		_ = resp.Body.Close()
   237  	}
   238  }
   239  
   240  func merge(dst, src map[string]int64, prefix string) {
   241  	for k, v := range src {
   242  		dst[prefix+"_"+k] = v
   243  	}
   244  }