package nginx_plus_api import ( "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/url" "strconv" "strings" "github.com/influxdata/telegraf" ) var ( // errNotFound signals that the NGINX API routes does not exist. errNotFound = errors.New("not found") ) func (n *NginxPlusAPI) gatherMetrics(addr *url.URL, acc telegraf.Accumulator) { addError(acc, n.gatherProcessesMetrics(addr, acc)) addError(acc, n.gatherConnectionsMetrics(addr, acc)) addError(acc, n.gatherSlabsMetrics(addr, acc)) addError(acc, n.gatherSslMetrics(addr, acc)) addError(acc, n.gatherHTTPRequestsMetrics(addr, acc)) addError(acc, n.gatherHTTPServerZonesMetrics(addr, acc)) addError(acc, n.gatherHTTPUpstreamsMetrics(addr, acc)) addError(acc, n.gatherHTTPCachesMetrics(addr, acc)) addError(acc, n.gatherStreamServerZonesMetrics(addr, acc)) addError(acc, n.gatherStreamUpstreamsMetrics(addr, acc)) if n.APIVersion >= 5 { addError(acc, n.gatherHTTPLocationZonesMetrics(addr, acc)) addError(acc, n.gatherResolverZonesMetrics(addr, acc)) } if n.APIVersion >= 6 { addError(acc, n.gatherHTTPLimitReqsMetrics(addr, acc)) } } func addError(acc telegraf.Accumulator, err error) { // This plugin has hardcoded API resource paths it checks that may not // be in the nginx.conf. Currently, this is to prevent logging of // paths that are not configured. // // The correct solution is to do a GET to /api to get the available paths // on the server rather than simply ignore. if !errors.Is(err, errNotFound) { acc.AddError(err) } } func (n *NginxPlusAPI) gatherURL(addr *url.URL, path string) ([]byte, error) { address := fmt.Sprintf("%s/%d/%s", addr.String(), n.APIVersion, path) resp, err := n.client.Get(address) if err != nil { return nil, fmt.Errorf("error making HTTP request to %q: %w", address, err) } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: case http.StatusNotFound: // format as special error to catch and ignore as some nginx API // features are either optional, or only available in some versions return nil, errNotFound default: return nil, fmt.Errorf("%s returned HTTP status %s", address, resp.Status) } contentType := strings.Split(resp.Header.Get("Content-Type"), ";")[0] switch contentType { case "application/json": body, err := io.ReadAll(resp.Body) if err != nil { return nil, err } return body, nil default: return nil, fmt.Errorf("%s returned unexpected content type %s", address, contentType) } } func (n *NginxPlusAPI) gatherProcessesMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, processesPath) if err != nil { return err } var processes = &processes{} if err := json.Unmarshal(body, processes); err != nil { return err } acc.AddFields( "nginx_plus_api_processes", map[string]interface{}{ "respawned": processes.Respawned, }, getTags(addr), ) return nil } func (n *NginxPlusAPI) gatherConnectionsMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, connectionsPath) if err != nil { return err } var connections = &connections{} if err := json.Unmarshal(body, connections); err != nil { return err } acc.AddFields( "nginx_plus_api_connections", map[string]interface{}{ "accepted": connections.Accepted, "dropped": connections.Dropped, "active": connections.Active, "idle": connections.Idle, }, getTags(addr), ) return nil } func (n *NginxPlusAPI) gatherSlabsMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, slabsPath) if err != nil { return err } var slabs slabs if err := json.Unmarshal(body, &slabs); err != nil { return err } tags := getTags(addr) for zoneName, slab := range slabs { slabTags := make(map[string]string, len(tags)+1) for k, v := range tags { slabTags[k] = v } slabTags["zone"] = zoneName acc.AddFields( "nginx_plus_api_slabs_pages", map[string]interface{}{ "used": slab.Pages.Used, "free": slab.Pages.Free, }, slabTags, ) for slotID, slot := range slab.Slots { slotTags := make(map[string]string, len(slabTags)+1) for k, v := range slabTags { slotTags[k] = v } slotTags["slot"] = slotID acc.AddFields( "nginx_plus_api_slabs_slots", map[string]interface{}{ "used": slot.Used, "free": slot.Free, "reqs": slot.Reqs, "fails": slot.Fails, }, slotTags, ) } } return nil } func (n *NginxPlusAPI) gatherSslMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, sslPath) if err != nil { return err } var ssl = &ssl{} if err := json.Unmarshal(body, ssl); err != nil { return err } acc.AddFields( "nginx_plus_api_ssl", map[string]interface{}{ "handshakes": ssl.Handshakes, "handshakes_failed": ssl.HandshakesFailed, "session_reuses": ssl.SessionReuses, }, getTags(addr), ) return nil } func (n *NginxPlusAPI) gatherHTTPRequestsMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, httpRequestsPath) if err != nil { return err } var httpRequests = &httpRequests{} if err := json.Unmarshal(body, httpRequests); err != nil { return err } acc.AddFields( "nginx_plus_api_http_requests", map[string]interface{}{ "total": httpRequests.Total, "current": httpRequests.Current, }, getTags(addr), ) return nil } func (n *NginxPlusAPI) gatherHTTPServerZonesMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, httpServerZonesPath) if err != nil { return err } var httpServerZones httpServerZones if err := json.Unmarshal(body, &httpServerZones); err != nil { return err } tags := getTags(addr) for zoneName, zone := range httpServerZones { zoneTags := make(map[string]string, len(tags)+1) for k, v := range tags { zoneTags[k] = v } zoneTags["zone"] = zoneName acc.AddFields( "nginx_plus_api_http_server_zones", func() map[string]interface{} { result := map[string]interface{}{ "processing": zone.Processing, "requests": zone.Requests, "responses_1xx": zone.Responses.Responses1xx, "responses_2xx": zone.Responses.Responses2xx, "responses_3xx": zone.Responses.Responses3xx, "responses_4xx": zone.Responses.Responses4xx, "responses_5xx": zone.Responses.Responses5xx, "responses_total": zone.Responses.Total, "received": zone.Received, "sent": zone.Sent, } if zone.Discarded != nil { result["discarded"] = *zone.Discarded } return result }(), zoneTags, ) } return nil } // Added in 5 API version func (n *NginxPlusAPI) gatherHTTPLocationZonesMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, httpLocationZonesPath) if err != nil { return err } var httpLocationZones httpLocationZones if err := json.Unmarshal(body, &httpLocationZones); err != nil { return err } tags := getTags(addr) for zoneName, zone := range httpLocationZones { zoneTags := make(map[string]string, len(tags)+1) for k, v := range tags { zoneTags[k] = v } zoneTags["zone"] = zoneName acc.AddFields( "nginx_plus_api_http_location_zones", func() map[string]interface{} { result := map[string]interface{}{ "requests": zone.Requests, "responses_1xx": zone.Responses.Responses1xx, "responses_2xx": zone.Responses.Responses2xx, "responses_3xx": zone.Responses.Responses3xx, "responses_4xx": zone.Responses.Responses4xx, "responses_5xx": zone.Responses.Responses5xx, "responses_total": zone.Responses.Total, "received": zone.Received, "sent": zone.Sent, } if zone.Discarded != nil { result["discarded"] = *zone.Discarded } return result }(), zoneTags, ) } return nil } func (n *NginxPlusAPI) gatherHTTPUpstreamsMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, httpUpstreamsPath) if err != nil { return err } var httpUpstreams httpUpstreams if err := json.Unmarshal(body, &httpUpstreams); err != nil { return err } tags := getTags(addr) for upstreamName, upstream := range httpUpstreams { upstreamTags := make(map[string]string, len(tags)+1) for k, v := range tags { upstreamTags[k] = v } upstreamTags["upstream"] = upstreamName upstreamFields := map[string]interface{}{ "keepalive": upstream.Keepalive, "zombies": upstream.Zombies, } if upstream.Queue != nil { upstreamFields["queue_size"] = upstream.Queue.Size upstreamFields["queue_max_size"] = upstream.Queue.MaxSize upstreamFields["queue_overflows"] = upstream.Queue.Overflows } acc.AddFields( "nginx_plus_api_http_upstreams", upstreamFields, upstreamTags, ) for _, peer := range upstream.Peers { peerFields := map[string]interface{}{ "backup": peer.Backup, "weight": peer.Weight, "state": peer.State, "active": peer.Active, "requests": peer.Requests, "responses_1xx": peer.Responses.Responses1xx, "responses_2xx": peer.Responses.Responses2xx, "responses_3xx": peer.Responses.Responses3xx, "responses_4xx": peer.Responses.Responses4xx, "responses_5xx": peer.Responses.Responses5xx, "responses_total": peer.Responses.Total, "sent": peer.Sent, "received": peer.Received, "fails": peer.Fails, "unavail": peer.Unavail, "healthchecks_checks": peer.HealthChecks.Checks, "healthchecks_fails": peer.HealthChecks.Fails, "healthchecks_unhealthy": peer.HealthChecks.Unhealthy, "downtime": peer.Downtime, // "selected": peer.Selected.toInt64, // "downstart": peer.Downstart.toInt64, } if peer.HealthChecks.LastPassed != nil { peerFields["healthchecks_last_passed"] = *peer.HealthChecks.LastPassed } if peer.HeaderTime != nil { peerFields["header_time"] = *peer.HeaderTime } if peer.ResponseTime != nil { peerFields["response_time"] = *peer.ResponseTime } if peer.MaxConns != nil { peerFields["max_conns"] = *peer.MaxConns } peerTags := make(map[string]string, len(upstreamTags)+2) for k, v := range upstreamTags { peerTags[k] = v } peerTags["upstream_address"] = peer.Server if peer.ID != nil { peerTags["id"] = strconv.Itoa(*peer.ID) } acc.AddFields("nginx_plus_api_http_upstream_peers", peerFields, peerTags) } } return nil } func (n *NginxPlusAPI) gatherHTTPCachesMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, httpCachesPath) if err != nil { return err } var httpCaches httpCaches if err := json.Unmarshal(body, &httpCaches); err != nil { return err } tags := getTags(addr) for cacheName, cache := range httpCaches { cacheTags := make(map[string]string, len(tags)+1) for k, v := range tags { cacheTags[k] = v } cacheTags["cache"] = cacheName acc.AddFields( "nginx_plus_api_http_caches", map[string]interface{}{ "size": cache.Size, "max_size": cache.MaxSize, "cold": cache.Cold, "hit_responses": cache.Hit.Responses, "hit_bytes": cache.Hit.Bytes, "stale_responses": cache.Stale.Responses, "stale_bytes": cache.Stale.Bytes, "updating_responses": cache.Updating.Responses, "updating_bytes": cache.Updating.Bytes, "revalidated_responses": cache.Revalidated.Responses, "revalidated_bytes": cache.Revalidated.Bytes, "miss_responses": cache.Miss.Responses, "miss_bytes": cache.Miss.Bytes, "miss_responses_written": cache.Miss.ResponsesWritten, "miss_bytes_written": cache.Miss.BytesWritten, "expired_responses": cache.Expired.Responses, "expired_bytes": cache.Expired.Bytes, "expired_responses_written": cache.Expired.ResponsesWritten, "expired_bytes_written": cache.Expired.BytesWritten, "bypass_responses": cache.Bypass.Responses, "bypass_bytes": cache.Bypass.Bytes, "bypass_responses_written": cache.Bypass.ResponsesWritten, "bypass_bytes_written": cache.Bypass.BytesWritten, }, cacheTags, ) } return nil } func (n *NginxPlusAPI) gatherStreamServerZonesMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, streamServerZonesPath) if err != nil { return err } var streamServerZones streamServerZones if err := json.Unmarshal(body, &streamServerZones); err != nil { return err } tags := getTags(addr) for zoneName, zone := range streamServerZones { zoneTags := make(map[string]string, len(tags)+1) for k, v := range tags { zoneTags[k] = v } zoneTags["zone"] = zoneName acc.AddFields( "nginx_plus_api_stream_server_zones", map[string]interface{}{ "processing": zone.Processing, "connections": zone.Connections, "received": zone.Received, "sent": zone.Sent, }, zoneTags, ) } return nil } // Added in 5 API version func (n *NginxPlusAPI) gatherResolverZonesMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, resolverZonesPath) if err != nil { return err } var resolverZones resolverZones if err := json.Unmarshal(body, &resolverZones); err != nil { return err } tags := getTags(addr) for zoneName, resolver := range resolverZones { zoneTags := make(map[string]string, len(tags)+1) for k, v := range tags { zoneTags[k] = v } zoneTags["zone"] = zoneName acc.AddFields( "nginx_plus_api_resolver_zones", map[string]interface{}{ "name": resolver.Requests.Name, "srv": resolver.Requests.Srv, "addr": resolver.Requests.Addr, "noerror": resolver.Responses.Noerror, "formerr": resolver.Responses.Formerr, "servfail": resolver.Responses.Servfail, "nxdomain": resolver.Responses.Nxdomain, "notimp": resolver.Responses.Notimp, "refused": resolver.Responses.Refused, "timedout": resolver.Responses.Timedout, "unknown": resolver.Responses.Unknown, }, zoneTags, ) } return nil } func (n *NginxPlusAPI) gatherStreamUpstreamsMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, streamUpstreamsPath) if err != nil { return err } var streamUpstreams streamUpstreams if err := json.Unmarshal(body, &streamUpstreams); err != nil { return err } tags := getTags(addr) for upstreamName, upstream := range streamUpstreams { upstreamTags := make(map[string]string, len(tags)+1) for k, v := range tags { upstreamTags[k] = v } upstreamTags["upstream"] = upstreamName acc.AddFields( "nginx_plus_api_stream_upstreams", map[string]interface{}{ "zombies": upstream.Zombies, }, upstreamTags, ) for _, peer := range upstream.Peers { peerFields := map[string]interface{}{ "backup": peer.Backup, "weight": peer.Weight, "state": peer.State, "active": peer.Active, "connections": peer.Connections, "sent": peer.Sent, "received": peer.Received, "fails": peer.Fails, "unavail": peer.Unavail, "healthchecks_checks": peer.HealthChecks.Checks, "healthchecks_fails": peer.HealthChecks.Fails, "healthchecks_unhealthy": peer.HealthChecks.Unhealthy, "downtime": peer.Downtime, } if peer.HealthChecks.LastPassed != nil { peerFields["healthchecks_last_passed"] = *peer.HealthChecks.LastPassed } if peer.ConnectTime != nil { peerFields["connect_time"] = *peer.ConnectTime } if peer.FirstByteTime != nil { peerFields["first_byte_time"] = *peer.FirstByteTime } if peer.ResponseTime != nil { peerFields["response_time"] = *peer.ResponseTime } peerTags := make(map[string]string, len(upstreamTags)+2) for k, v := range upstreamTags { peerTags[k] = v } peerTags["upstream_address"] = peer.Server peerTags["id"] = strconv.Itoa(peer.ID) acc.AddFields("nginx_plus_api_stream_upstream_peers", peerFields, peerTags) } } return nil } // Added in 6 API version func (n *NginxPlusAPI) gatherHTTPLimitReqsMetrics(addr *url.URL, acc telegraf.Accumulator) error { body, err := n.gatherURL(addr, httpLimitReqsPath) if err != nil { return err } var httpLimitReqs httpLimitReqs if err := json.Unmarshal(body, &httpLimitReqs); err != nil { return err } tags := getTags(addr) for limitReqName, limit := range httpLimitReqs { limitReqsTags := make(map[string]string, len(tags)+1) for k, v := range tags { limitReqsTags[k] = v } limitReqsTags["limit"] = limitReqName acc.AddFields( "nginx_plus_api_http_limit_reqs", map[string]interface{}{ "passed": limit.Passed, "delayed": limit.Delayed, "rejected": limit.Rejected, "delayed_dry_run": limit.DelayedDryRun, "rejected_dry_run": limit.RejectedDryRun, }, limitReqsTags, ) } return nil } func getTags(addr *url.URL) map[string]string { h := addr.Host host, port, err := net.SplitHostPort(h) if err != nil { host = addr.Host if addr.Scheme == "http" { port = "80" } else if addr.Scheme == "https" { port = "443" } else { port = "" } } return map[string]string{"source": host, "port": port} }