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 }