| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | package traefik |
| 4 | |
| 5 | import ( |
| 6 | "errors" |
| 7 | "strings" |
| 8 | |
| 9 | "github.com/netdata/netdata/go/plugins/pkg/prometheus" |
| 10 | "github.com/netdata/netdata/go/plugins/plugin/framework/collectorapi" |
| 11 | ) |
| 12 | |
| 13 | const ( |
| 14 | metricEntrypointRequestsTotal = "traefik_entrypoint_requests_total" |
| 15 | metricEntrypointRequestDurationSecondsSum = "traefik_entrypoint_request_duration_seconds_sum" |
| 16 | metricEntrypointRequestDurationSecondsCount = "traefik_entrypoint_request_duration_seconds_count" |
| 17 | metricEntrypointOpenConnections = "traefik_entrypoint_open_connections" |
| 18 | ) |
| 19 | |
| 20 | const ( |
| 21 | prefixEntrypointRequests = "entrypoint_requests_" |
| 22 | prefixEntrypointReqDurAvg = "entrypoint_request_duration_average_" |
| 23 | prefixEntrypointOpenConn = "entrypoint_open_connections_" |
| 24 | ) |
| 25 | |
| 26 | func isTraefikMetrics(pms prometheus.Series) bool { |
| 27 | for _, pm := range pms { |
| 28 | if strings.HasPrefix(pm.Name(), "traefik_") { |
| 29 | return true |
| 30 | } |
| 31 | } |
| 32 | return false |
| 33 | } |
| 34 | |
| 35 | func (c *Collector) collect() (map[string]int64, error) { |
| 36 | pms, err := c.prom.ScrapeSeries() |
| 37 | if err != nil { |
| 38 | return nil, err |
| 39 | } |
| 40 | |
| 41 | if c.checkMetrics && !isTraefikMetrics(pms) { |
| 42 | return nil, errors.New("unexpected metrics (not Traefik)") |
| 43 | } |
| 44 | c.checkMetrics = false |
| 45 | |
| 46 | mx := make(map[string]int64) |
| 47 | |
| 48 | c.collectEntrypointRequestsTotal(mx, pms) |
| 49 | c.collectEntrypointRequestDuration(mx, pms) |
| 50 | c.collectEntrypointOpenConnections(mx, pms) |
| 51 | c.updateCodeClassMetrics(mx) |
| 52 | |
| 53 | return mx, nil |
| 54 | } |
| 55 | |
| 56 | func (c *Collector) collectEntrypointRequestsTotal(mx map[string]int64, pms prometheus.Series) { |
| 57 | if pms = pms.FindByName(metricEntrypointRequestsTotal); pms.Len() == 0 { |
| 58 | return |
| 59 | } |
| 60 | |
| 61 | for _, pm := range pms { |
| 62 | code := pm.Labels.Get("code") |
| 63 | ep := pm.Labels.Get("entrypoint") |
| 64 | proto := pm.Labels.Get("protocol") |
| 65 | codeClass := getCodeClass(code) |
| 66 | if code == "" || ep == "" || proto == "" || codeClass == "" { |
| 67 | continue |
| 68 | } |
| 69 | |
| 70 | key := prefixEntrypointRequests + ep + "_" + proto + "_" + codeClass |
| 71 | mx[key] += int64(pm.Value) |
| 72 | |
| 73 | id := ep + "_" + proto |
| 74 | ce := c.cacheGetOrPutEntrypoint(id) |
| 75 | if ce.requests == nil { |
| 76 | chart := newChartEntrypointRequests(ep, proto) |
| 77 | ce.requests = chart |
| 78 | if err := c.Charts().Add(chart); err != nil { |
| 79 | c.Warning(err) |
| 80 | } |
| 81 | } |
| 82 | } |
| 83 | } |
| 84 | |
| 85 | func (c *Collector) collectEntrypointRequestDuration(mx map[string]int64, pms prometheus.Series) { |
| 86 | if pms = pms.FindByNames( |
| 87 | metricEntrypointRequestDurationSecondsCount, |
| 88 | metricEntrypointRequestDurationSecondsSum, |
| 89 | ); pms.Len() == 0 { |
| 90 | return |
| 91 | } |
| 92 | |
| 93 | for _, pm := range pms { |
| 94 | code := pm.Labels.Get("code") |
| 95 | ep := pm.Labels.Get("entrypoint") |
| 96 | proto := pm.Labels.Get("protocol") |
| 97 | codeClass := getCodeClass(code) |
| 98 | if code == "" || ep == "" || proto == "" || codeClass == "" { |
| 99 | continue |
| 100 | } |
| 101 | |
| 102 | id := ep + "_" + proto |
| 103 | ce := c.cacheGetOrPutEntrypoint(id) |
| 104 | v := ce.reqDurData[codeClass] |
| 105 | if pm.Name() == metricEntrypointRequestDurationSecondsSum { |
| 106 | v.cur.secs += pm.Value |
| 107 | } else { |
| 108 | v.cur.reqs += pm.Value |
| 109 | } |
| 110 | ce.reqDurData[codeClass] = v |
| 111 | } |
| 112 | |
| 113 | for id, ce := range c.cache.entrypoints { |
| 114 | if ce.reqDur == nil { |
| 115 | chart := newChartEntrypointRequestDuration(ce.name, ce.proto) |
| 116 | ce.reqDur = chart |
| 117 | if err := c.Charts().Add(chart); err != nil { |
| 118 | c.Warning(err) |
| 119 | } |
| 120 | } |
| 121 | for codeClass, v := range ce.reqDurData { |
| 122 | secs, reqs, seen := v.cur.secs-v.prev.secs, v.cur.reqs-v.prev.reqs, v.seen |
| 123 | v.prev.secs, v.prev.reqs, v.seen = v.cur.secs, v.cur.reqs, true |
| 124 | v.cur.secs, v.cur.reqs = 0, 0 |
| 125 | ce.reqDurData[codeClass] = v |
| 126 | |
| 127 | key := prefixEntrypointReqDurAvg + id + "_" + codeClass |
| 128 | if secs <= 0 || reqs <= 0 || !seen { |
| 129 | mx[key] = 0 |
| 130 | } else { |
| 131 | mx[key] = int64(secs * 1000 / reqs) |
| 132 | } |
| 133 | } |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | func (c *Collector) collectEntrypointOpenConnections(mx map[string]int64, pms prometheus.Series) { |
| 138 | if pms = pms.FindByName(metricEntrypointOpenConnections); pms.Len() == 0 { |
| 139 | return |
| 140 | } |
| 141 | |
| 142 | for _, pm := range pms { |
| 143 | method := pm.Labels.Get("method") |
| 144 | ep := pm.Labels.Get("entrypoint") |
| 145 | proto := pm.Labels.Get("protocol") |
| 146 | if method == "" || ep == "" || proto == "" { |
| 147 | continue |
| 148 | } |
| 149 | |
| 150 | key := prefixEntrypointOpenConn + ep + "_" + proto + "_" + method |
| 151 | mx[key] += int64(pm.Value) |
| 152 | |
| 153 | id := ep + "_" + proto |
| 154 | ce := c.cacheGetOrPutEntrypoint(id) |
| 155 | if ce.openConn == nil { |
| 156 | chart := newChartEntrypointOpenConnections(ep, proto) |
| 157 | ce.openConn = chart |
| 158 | if err := c.Charts().Add(chart); err != nil { |
| 159 | c.Warning(err) |
| 160 | } |
| 161 | } |
| 162 | |
| 163 | if !ce.openConnMethods[method] { |
| 164 | ce.openConnMethods[method] = true |
| 165 | dim := &collectorapi.Dim{ID: key, Name: method} |
| 166 | if err := ce.openConn.AddDim(dim); err != nil { |
| 167 | c.Warning(err) |
| 168 | } |
| 169 | ce.openConn.MarkNotCreated() |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | |
| 174 | var httpRespCodeClasses = []string{"1xx", "2xx", "3xx", "4xx", "5xx"} |
| 175 | |
| 176 | func (c *Collector) updateCodeClassMetrics(mx map[string]int64) { |
| 177 | for id, ce := range c.cache.entrypoints { |
| 178 | if ce.requests != nil { |
| 179 | for _, c := range httpRespCodeClasses { |
| 180 | key := prefixEntrypointRequests + id + "_" + c |
| 181 | mx[key] += 0 |
| 182 | } |
| 183 | } |
| 184 | if ce.reqDur != nil { |
| 185 | for _, c := range httpRespCodeClasses { |
| 186 | key := prefixEntrypointReqDurAvg + id + "_" + c |
| 187 | mx[key] += 0 |
| 188 | } |
| 189 | } |
| 190 | } |
| 191 | } |
| 192 | |
| 193 | func getCodeClass(code string) string { |
| 194 | if len(code) != 3 { |
| 195 | return "" |
| 196 | } |
| 197 | return string(code[0]) + "xx" |
| 198 | } |
| 199 | |
| 200 | func (c *Collector) cacheGetOrPutEntrypoint(id string) *cacheEntrypoint { |
| 201 | if _, ok := c.cache.entrypoints[id]; !ok { |
| 202 | name, proto := id, id |
| 203 | if idx := strings.LastIndexByte(id, '_'); idx != -1 { |
| 204 | name, proto = id[:idx], id[idx+1:] |
| 205 | } |
| 206 | c.cache.entrypoints[id] = &cacheEntrypoint{ |
| 207 | name: name, |
| 208 | proto: proto, |
| 209 | reqDurData: make(map[string]cacheEntrypointReqDur), |
| 210 | openConnMethods: make(map[string]bool), |
| 211 | } |
| 212 | } |
| 213 | return c.cache.entrypoints[id] |
| 214 | } |