diff --git a/README.md b/README.md index b834d9ca..32c7b051 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ Usage of ./nginx-prometheus-exporter: Perform SSL certificate verification. The default value can be overwritten by SSL_VERIFY environment variable. (default true) -nginx.timeout duration A timeout for scraping metrics from NGINX or NGINX Plus. The default value can be overwritten by TIMEOUT environment variable. (default 5s) + -prometheus.const-labels value + A comma separated list of constant labels that will be used in every metric. Format is label1=value1,label2=value2... The default value can be overwritten by CONST_LABELS environment variable. -web.listen-address string An address or unix domain socket path to listen on for web interface and telemetry. The default value can be overwritten by LISTEN_ADDRESS environment variable. (default ":9113") -web.telemetry-path string diff --git a/collector/helper.go b/collector/helper.go index 6fcd67e3..aeea5c98 100644 --- a/collector/helper.go +++ b/collector/helper.go @@ -1,12 +1,14 @@ package collector -import "github.com/prometheus/client_golang/prometheus" +import ( + "github.com/prometheus/client_golang/prometheus" +) const nginxUp = 1 const nginxDown = 0 -func newGlobalMetric(namespace string, metricName string, docString string) *prometheus.Desc { - return prometheus.NewDesc(namespace+"_"+metricName, docString, nil, nil) +func newGlobalMetric(namespace string, metricName string, docString string, constLabels map[string]string) *prometheus.Desc { + return prometheus.NewDesc(namespace+"_"+metricName, docString, nil, constLabels) } func newUpMetric(namespace string) prometheus.Gauge { @@ -16,3 +18,17 @@ func newUpMetric(namespace string) prometheus.Gauge { Help: "Status of the last metric scrape", }) } + +// MergeLabels merges two maps of labels. +func MergeLabels(a map[string]string, b map[string]string) map[string]string { + c := make(map[string]string) + + for k, v := range a { + c[k] = v + } + for k, v := range b { + c[k] = v + } + + return c +} diff --git a/collector/helper_test.go b/collector/helper_test.go new file mode 100644 index 00000000..cfbfec75 --- /dev/null +++ b/collector/helper_test.go @@ -0,0 +1,39 @@ +package collector + +import ( + "reflect" + "testing" +) + +func TestMergeLabels(t *testing.T) { + tests := []struct { + name string + mapA, mapB, want map[string]string + }{ + { + name: "base case", + mapA: map[string]string{"a": "is here"}, + mapB: map[string]string{"b": "is here"}, + want: map[string]string{"a": "is here", "b": "is here"}, + }, + { + name: "overwrite key case", + mapA: map[string]string{"a": "is here"}, + mapB: map[string]string{"b": "is here", "a": "is now here"}, + want: map[string]string{"a": "is now here", "b": "is here"}, + }, + { + name: "empty maps case", + mapA: nil, + mapB: nil, + want: map[string]string{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := MergeLabels(tt.mapA, tt.mapB); !reflect.DeepEqual(got, tt.want) { + t.Errorf("mergeLabels() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/collector/nginx.go b/collector/nginx.go index 2ac0bca0..c6576dbb 100644 --- a/collector/nginx.go +++ b/collector/nginx.go @@ -17,17 +17,17 @@ type NginxCollector struct { } // NewNginxCollector creates an NginxCollector. -func NewNginxCollector(nginxClient *client.NginxClient, namespace string) *NginxCollector { +func NewNginxCollector(nginxClient *client.NginxClient, namespace string, constLabels map[string]string) *NginxCollector { return &NginxCollector{ nginxClient: nginxClient, metrics: map[string]*prometheus.Desc{ - "connections_active": newGlobalMetric(namespace, "connections_active", "Active client connections"), - "connections_accepted": newGlobalMetric(namespace, "connections_accepted", "Accepted client connections"), - "connections_handled": newGlobalMetric(namespace, "connections_handled", "Handled client connections"), - "connections_reading": newGlobalMetric(namespace, "connections_reading", "Connections where NGINX is reading the request header"), - "connections_writing": newGlobalMetric(namespace, "connections_writing", "Connections where NGINX is writing the response back to the client"), - "connections_waiting": newGlobalMetric(namespace, "connections_waiting", "Idle client connections"), - "http_requests_total": newGlobalMetric(namespace, "http_requests_total", "Total http requests"), + "connections_active": newGlobalMetric(namespace, "connections_active", "Active client connections", constLabels), + "connections_accepted": newGlobalMetric(namespace, "connections_accepted", "Accepted client connections", constLabels), + "connections_handled": newGlobalMetric(namespace, "connections_handled", "Handled client connections", constLabels), + "connections_reading": newGlobalMetric(namespace, "connections_reading", "Connections where NGINX is reading the request header", constLabels), + "connections_writing": newGlobalMetric(namespace, "connections_writing", "Connections where NGINX is writing the response back to the client", constLabels), + "connections_waiting": newGlobalMetric(namespace, "connections_waiting", "Idle client connections", constLabels), + "http_requests_total": newGlobalMetric(namespace, "http_requests_total", "Total http requests", constLabels), }, upMetric: newUpMetric(namespace), } diff --git a/collector/nginx_plus.go b/collector/nginx_plus.go index cde35911..40c89cbe 100644 --- a/collector/nginx_plus.go +++ b/collector/nginx_plus.go @@ -26,115 +26,115 @@ type NginxPlusCollector struct { } // NewNginxPlusCollector creates an NginxPlusCollector. -func NewNginxPlusCollector(nginxClient *plusclient.NginxClient, namespace string) *NginxPlusCollector { +func NewNginxPlusCollector(nginxClient *plusclient.NginxClient, namespace string, constLabels map[string]string) *NginxPlusCollector { return &NginxPlusCollector{ nginxClient: nginxClient, totalMetrics: map[string]*prometheus.Desc{ - "connections_accepted": newGlobalMetric(namespace, "connections_accepted", "Accepted client connections"), - "connections_dropped": newGlobalMetric(namespace, "connections_dropped", "Dropped client connections"), - "connections_active": newGlobalMetric(namespace, "connections_active", "Active client connections"), - "connections_idle": newGlobalMetric(namespace, "connections_idle", "Idle client connections"), - "http_requests_total": newGlobalMetric(namespace, "http_requests_total", "Total http requests"), - "http_requests_current": newGlobalMetric(namespace, "http_requests_current", "Current http requests"), - "ssl_handshakes": newGlobalMetric(namespace, "ssl_handshakes", "Successful SSL handshakes"), - "ssl_handshakes_failed": newGlobalMetric(namespace, "ssl_handshakes_failed", "Failed SSL handshakes"), - "ssl_session_reuses": newGlobalMetric(namespace, "ssl_session_reuses", "Session reuses during SSL handshake"), + "connections_accepted": newGlobalMetric(namespace, "connections_accepted", "Accepted client connections", constLabels), + "connections_dropped": newGlobalMetric(namespace, "connections_dropped", "Dropped client connections", constLabels), + "connections_active": newGlobalMetric(namespace, "connections_active", "Active client connections", constLabels), + "connections_idle": newGlobalMetric(namespace, "connections_idle", "Idle client connections", constLabels), + "http_requests_total": newGlobalMetric(namespace, "http_requests_total", "Total http requests", constLabels), + "http_requests_current": newGlobalMetric(namespace, "http_requests_current", "Current http requests", constLabels), + "ssl_handshakes": newGlobalMetric(namespace, "ssl_handshakes", "Successful SSL handshakes", constLabels), + "ssl_handshakes_failed": newGlobalMetric(namespace, "ssl_handshakes_failed", "Failed SSL handshakes", constLabels), + "ssl_session_reuses": newGlobalMetric(namespace, "ssl_session_reuses", "Session reuses during SSL handshake", constLabels), }, serverZoneMetrics: map[string]*prometheus.Desc{ - "processing": newServerZoneMetric(namespace, "processing", "Client requests that are currently being processed", nil), - "requests": newServerZoneMetric(namespace, "requests", "Total client requests", nil), - "responses_1xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "1xx"}), - "responses_2xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "2xx"}), - "responses_3xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "3xx"}), - "responses_4xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "4xx"}), - "responses_5xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "5xx"}), - "discarded": newServerZoneMetric(namespace, "discarded", "Requests completed without sending a response", nil), - "received": newServerZoneMetric(namespace, "received", "Bytes received from clients", nil), - "sent": newServerZoneMetric(namespace, "sent", "Bytes sent to clients", nil), + "processing": newServerZoneMetric(namespace, "processing", "Client requests that are currently being processed", constLabels), + "requests": newServerZoneMetric(namespace, "requests", "Total client requests", constLabels), + "responses_1xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "1xx"})), + "responses_2xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "2xx"})), + "responses_3xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "3xx"})), + "responses_4xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "4xx"})), + "responses_5xx": newServerZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "5xx"})), + "discarded": newServerZoneMetric(namespace, "discarded", "Requests completed without sending a response", constLabels), + "received": newServerZoneMetric(namespace, "received", "Bytes received from clients", constLabels), + "sent": newServerZoneMetric(namespace, "sent", "Bytes sent to clients", constLabels), }, streamServerZoneMetrics: map[string]*prometheus.Desc{ - "processing": newStreamServerZoneMetric(namespace, "processing", "Client connections that are currently being processed", nil), - "connections": newStreamServerZoneMetric(namespace, "connections", "Total connections", nil), - "sessions_2xx": newStreamServerZoneMetric(namespace, "sessions", "Total sessions completed", prometheus.Labels{"code": "2xx"}), - "sessions_4xx": newStreamServerZoneMetric(namespace, "sessions", "Total sessions completed", prometheus.Labels{"code": "4xx"}), - "sessions_5xx": newStreamServerZoneMetric(namespace, "sessions", "Total sessions completed", prometheus.Labels{"code": "5xx"}), - "discarded": newStreamServerZoneMetric(namespace, "discarded", "Connections completed without creating a session", nil), - "received": newStreamServerZoneMetric(namespace, "received", "Bytes received from clients", nil), - "sent": newStreamServerZoneMetric(namespace, "sent", "Bytes sent to clients", nil), + "processing": newStreamServerZoneMetric(namespace, "processing", "Client connections that are currently being processed", constLabels), + "connections": newStreamServerZoneMetric(namespace, "connections", "Total connections", constLabels), + "sessions_2xx": newStreamServerZoneMetric(namespace, "sessions", "Total sessions completed", MergeLabels(constLabels, prometheus.Labels{"code": "2xx"})), + "sessions_4xx": newStreamServerZoneMetric(namespace, "sessions", "Total sessions completed", MergeLabels(constLabels, prometheus.Labels{"code": "4xx"})), + "sessions_5xx": newStreamServerZoneMetric(namespace, "sessions", "Total sessions completed", MergeLabels(constLabels, prometheus.Labels{"code": "5xx"})), + "discarded": newStreamServerZoneMetric(namespace, "discarded", "Connections completed without creating a session", constLabels), + "received": newStreamServerZoneMetric(namespace, "received", "Bytes received from clients", constLabels), + "sent": newStreamServerZoneMetric(namespace, "sent", "Bytes sent to clients", constLabels), }, upstreamMetrics: map[string]*prometheus.Desc{ - "keepalives": newUpstreamMetric(namespace, "keepalives", "Idle keepalive connections"), - "zombies": newUpstreamMetric(namespace, "zombies", "Servers removed from the group but still processing active client requests"), + "keepalives": newUpstreamMetric(namespace, "keepalives", "Idle keepalive connections", constLabels), + "zombies": newUpstreamMetric(namespace, "zombies", "Servers removed from the group but still processing active client requests", constLabels), }, streamUpstreamMetrics: map[string]*prometheus.Desc{ - "zombies": newStreamUpstreamMetric(namespace, "zombies", "Servers removed from the group but still processing active client connections"), + "zombies": newStreamUpstreamMetric(namespace, "zombies", "Servers removed from the group but still processing active client connections", constLabels), }, upstreamServerMetrics: map[string]*prometheus.Desc{ - "state": newUpstreamServerMetric(namespace, "state", "Current state", nil), - "active": newUpstreamServerMetric(namespace, "active", "Active connections", nil), - "requests": newUpstreamServerMetric(namespace, "requests", "Total client requests", nil), - "responses_1xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "1xx"}), - "responses_2xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "2xx"}), - "responses_3xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "3xx"}), - "responses_4xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "4xx"}), - "responses_5xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "5xx"}), - "sent": newUpstreamServerMetric(namespace, "sent", "Bytes sent to this server", nil), - "received": newUpstreamServerMetric(namespace, "received", "Bytes received to this server", nil), - "fails": newUpstreamServerMetric(namespace, "fails", "Active connections", nil), - "unavail": newUpstreamServerMetric(namespace, "unavail", "How many times the server became unavailable for client requests (state 'unavail') due to the number of unsuccessful attempts reaching the max_fails threshold", nil), - "header_time": newUpstreamServerMetric(namespace, "header_time", "Average time to get the response header from the server", nil), - "response_time": newUpstreamServerMetric(namespace, "response_time", "Average time to get the full response from the server", nil), - "health_checks_checks": newUpstreamServerMetric(namespace, "health_checks_checks", "Total health check requests", nil), - "health_checks_fails": newUpstreamServerMetric(namespace, "health_checks_fails", "Failed health checks", nil), - "health_checks_unhealthy": newUpstreamServerMetric(namespace, "health_checks_unhealthy", "How many times the server became unhealthy (state 'unhealthy')", nil), + "state": newUpstreamServerMetric(namespace, "state", "Current state", constLabels), + "active": newUpstreamServerMetric(namespace, "active", "Active connections", constLabels), + "requests": newUpstreamServerMetric(namespace, "requests", "Total client requests", constLabels), + "responses_1xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "1xx"})), + "responses_2xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "2xx"})), + "responses_3xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "3xx"})), + "responses_4xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "4xx"})), + "responses_5xx": newUpstreamServerMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "5xx"})), + "sent": newUpstreamServerMetric(namespace, "sent", "Bytes sent to this server", constLabels), + "received": newUpstreamServerMetric(namespace, "received", "Bytes received to this server", constLabels), + "fails": newUpstreamServerMetric(namespace, "fails", "Active connections", constLabels), + "unavail": newUpstreamServerMetric(namespace, "unavail", "How many times the server became unavailable for client requests (state 'unavail') due to the number of unsuccessful attempts reaching the max_fails threshold", constLabels), + "header_time": newUpstreamServerMetric(namespace, "header_time", "Average time to get the response header from the server", constLabels), + "response_time": newUpstreamServerMetric(namespace, "response_time", "Average time to get the full response from the server", constLabels), + "health_checks_checks": newUpstreamServerMetric(namespace, "health_checks_checks", "Total health check requests", constLabels), + "health_checks_fails": newUpstreamServerMetric(namespace, "health_checks_fails", "Failed health checks", constLabels), + "health_checks_unhealthy": newUpstreamServerMetric(namespace, "health_checks_unhealthy", "How many times the server became unhealthy (state 'unhealthy')", constLabels), }, streamUpstreamServerMetrics: map[string]*prometheus.Desc{ - "state": newStreamUpstreamServerMetric(namespace, "state", "Current state"), - "active": newStreamUpstreamServerMetric(namespace, "active", "Active connections"), - "sent": newStreamUpstreamServerMetric(namespace, "sent", "Bytes sent to this server"), - "received": newStreamUpstreamServerMetric(namespace, "received", "Bytes received from this server"), - "fails": newStreamUpstreamServerMetric(namespace, "fails", "Number of unsuccessful attempts to communicate with the server"), - "unavail": newStreamUpstreamServerMetric(namespace, "unavail", "How many times the server became unavailable for client connections (state 'unavail') due to the number of unsuccessful attempts reaching the max_fails threshold"), - "connections": newStreamUpstreamServerMetric(namespace, "connections", "Total number of client connections forwarded to this server"), - "connect_time": newStreamUpstreamServerMetric(namespace, "connect_time", "Average time to connect to the upstream server"), - "first_byte_time": newStreamUpstreamServerMetric(namespace, "first_byte_time", "Average time to receive the first byte of data"), - "response_time": newStreamUpstreamServerMetric(namespace, "response_time", "Average time to receive the last byte of data"), - "health_checks_checks": newStreamUpstreamServerMetric(namespace, "health_checks_checks", "Total health check requests"), - "health_checks_fails": newStreamUpstreamServerMetric(namespace, "health_checks_fails", "Failed health checks"), - "health_checks_unhealthy": newStreamUpstreamServerMetric(namespace, "health_checks_unhealthy", "How many times the server became unhealthy (state 'unhealthy')"), + "state": newStreamUpstreamServerMetric(namespace, "state", "Current state", constLabels), + "active": newStreamUpstreamServerMetric(namespace, "active", "Active connections", constLabels), + "sent": newStreamUpstreamServerMetric(namespace, "sent", "Bytes sent to this server", constLabels), + "received": newStreamUpstreamServerMetric(namespace, "received", "Bytes received from this server", constLabels), + "fails": newStreamUpstreamServerMetric(namespace, "fails", "Number of unsuccessful attempts to communicate with the server", constLabels), + "unavail": newStreamUpstreamServerMetric(namespace, "unavail", "How many times the server became unavailable for client connections (state 'unavail') due to the number of unsuccessful attempts reaching the max_fails threshold", constLabels), + "connections": newStreamUpstreamServerMetric(namespace, "connections", "Total number of client connections forwarded to this server", constLabels), + "connect_time": newStreamUpstreamServerMetric(namespace, "connect_time", "Average time to connect to the upstream server", constLabels), + "first_byte_time": newStreamUpstreamServerMetric(namespace, "first_byte_time", "Average time to receive the first byte of data", constLabels), + "response_time": newStreamUpstreamServerMetric(namespace, "response_time", "Average time to receive the last byte of data", constLabels), + "health_checks_checks": newStreamUpstreamServerMetric(namespace, "health_checks_checks", "Total health check requests", constLabels), + "health_checks_fails": newStreamUpstreamServerMetric(namespace, "health_checks_fails", "Failed health checks", constLabels), + "health_checks_unhealthy": newStreamUpstreamServerMetric(namespace, "health_checks_unhealthy", "How many times the server became unhealthy (state 'unhealthy')", constLabels), }, streamZoneSyncMetrics: map[string]*prometheus.Desc{ - "bytes_in": newStreamZoneSyncMetric(namespace, "bytes_in", "Bytes received by this node"), - "bytes_out": newStreamZoneSyncMetric(namespace, "bytes_out", "Bytes sent by this node"), - "msgs_in": newStreamZoneSyncMetric(namespace, "msgs_in", "Total messages received by this node"), - "msgs_out": newStreamZoneSyncMetric(namespace, "msgs_out", "Total messages sent by this node"), - "nodes_online": newStreamZoneSyncMetric(namespace, "nodes_online", "Number of peers this node is connected to"), - "records_pending": newStreamZoneSyncZoneMetric(namespace, "records_pending", "The number of records that need to be sent to the cluster"), - "records_total": newStreamZoneSyncZoneMetric(namespace, "records_total", "The total number of records stored in the shared memory zone"), + "bytes_in": newStreamZoneSyncMetric(namespace, "bytes_in", "Bytes received by this node", constLabels), + "bytes_out": newStreamZoneSyncMetric(namespace, "bytes_out", "Bytes sent by this node", constLabels), + "msgs_in": newStreamZoneSyncMetric(namespace, "msgs_in", "Total messages received by this node", constLabels), + "msgs_out": newStreamZoneSyncMetric(namespace, "msgs_out", "Total messages sent by this node", constLabels), + "nodes_online": newStreamZoneSyncMetric(namespace, "nodes_online", "Number of peers this node is connected to", constLabels), + "records_pending": newStreamZoneSyncZoneMetric(namespace, "records_pending", "The number of records that need to be sent to the cluster", constLabels), + "records_total": newStreamZoneSyncZoneMetric(namespace, "records_total", "The total number of records stored in the shared memory zone", constLabels), }, locationZoneMetrics: map[string]*prometheus.Desc{ - "requests": newLocationZoneMetric(namespace, "requests", "Total client requests", nil), - "responses_1xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "1xx"}), - "responses_2xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "2xx"}), - "responses_3xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "3xx"}), - "responses_4xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "4xx"}), - "responses_5xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", prometheus.Labels{"code": "5xx"}), - "discarded": newLocationZoneMetric(namespace, "discarded", "Requests completed without sending a response", nil), - "received": newLocationZoneMetric(namespace, "received", "Bytes received from clients", nil), - "sent": newLocationZoneMetric(namespace, "sent", "Bytes sent to clients", nil), + "requests": newLocationZoneMetric(namespace, "requests", "Total client requests", constLabels), + "responses_1xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "1xx"})), + "responses_2xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "2xx"})), + "responses_3xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "3xx"})), + "responses_4xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "4xx"})), + "responses_5xx": newLocationZoneMetric(namespace, "responses", "Total responses sent to clients", MergeLabels(constLabels, prometheus.Labels{"code": "5xx"})), + "discarded": newLocationZoneMetric(namespace, "discarded", "Requests completed without sending a response", constLabels), + "received": newLocationZoneMetric(namespace, "received", "Bytes received from clients", constLabels), + "sent": newLocationZoneMetric(namespace, "sent", "Bytes sent to clients", constLabels), }, resolverMetrics: map[string]*prometheus.Desc{ - "name": newResolverMetric(namespace, "name", "Total requests to resolve names to addresses"), - "srv": newResolverMetric(namespace, "srv", "Total requests to resolve SRV records"), - "addr": newResolverMetric(namespace, "addr", "Total requests to resolve addresses to names"), - "noerror": newResolverMetric(namespace, "noerror", "Total number of successful responses"), - "formerr": newResolverMetric(namespace, "formerr", "Total number of FORMERR responses"), - "servfail": newResolverMetric(namespace, "servfail", "Total number of SERVFAIL responses"), - "nxdomain": newResolverMetric(namespace, "nxdomain", "Total number of NXDOMAIN responses"), - "notimp": newResolverMetric(namespace, "notimp", "Total number of NOTIMP responses"), - "refused": newResolverMetric(namespace, "refused", "Total number of REFUSED responses"), - "timedout": newResolverMetric(namespace, "timedout", "Total number of timed out requests"), - "unknown": newResolverMetric(namespace, "unknown", "Total requests completed with an unknown error"), + "name": newResolverMetric(namespace, "name", "Total requests to resolve names to addresses", constLabels), + "srv": newResolverMetric(namespace, "srv", "Total requests to resolve SRV records", constLabels), + "addr": newResolverMetric(namespace, "addr", "Total requests to resolve addresses to names", constLabels), + "noerror": newResolverMetric(namespace, "noerror", "Total number of successful responses", constLabels), + "formerr": newResolverMetric(namespace, "formerr", "Total number of FORMERR responses", constLabels), + "servfail": newResolverMetric(namespace, "servfail", "Total number of SERVFAIL responses", constLabels), + "nxdomain": newResolverMetric(namespace, "nxdomain", "Total number of NXDOMAIN responses", constLabels), + "notimp": newResolverMetric(namespace, "notimp", "Total number of NOTIMP responses", constLabels), + "refused": newResolverMetric(namespace, "refused", "Total number of REFUSED responses", constLabels), + "timedout": newResolverMetric(namespace, "timedout", "Total number of timed out requests", constLabels), + "unknown": newResolverMetric(namespace, "unknown", "Total requests completed with an unknown error", constLabels), }, upMetric: newUpMetric(namespace), } @@ -419,34 +419,34 @@ func newStreamServerZoneMetric(namespace string, metricName string, docString st return prometheus.NewDesc(prometheus.BuildFQName(namespace, "stream_server_zone", metricName), docString, []string{"server_zone"}, constLabels) } -func newUpstreamMetric(namespace string, metricName string, docString string) *prometheus.Desc { - return prometheus.NewDesc(prometheus.BuildFQName(namespace, "upstream", metricName), docString, []string{"upstream"}, nil) +func newUpstreamMetric(namespace string, metricName string, docString string, constLabels prometheus.Labels) *prometheus.Desc { + return prometheus.NewDesc(prometheus.BuildFQName(namespace, "upstream", metricName), docString, []string{"upstream"}, constLabels) } -func newStreamUpstreamMetric(namespace string, metricName string, docString string) *prometheus.Desc { - return prometheus.NewDesc(prometheus.BuildFQName(namespace, "stream_upstream", metricName), docString, []string{"upstream"}, nil) +func newStreamUpstreamMetric(namespace string, metricName string, docString string, constLabels prometheus.Labels) *prometheus.Desc { + return prometheus.NewDesc(prometheus.BuildFQName(namespace, "stream_upstream", metricName), docString, []string{"upstream"}, constLabels) } func newUpstreamServerMetric(namespace string, metricName string, docString string, constLabels prometheus.Labels) *prometheus.Desc { return prometheus.NewDesc(prometheus.BuildFQName(namespace, "upstream_server", metricName), docString, []string{"upstream", "server"}, constLabels) } -func newStreamUpstreamServerMetric(namespace string, metricName string, docString string) *prometheus.Desc { - return prometheus.NewDesc(prometheus.BuildFQName(namespace, "stream_upstream_server", metricName), docString, []string{"upstream", "server"}, nil) +func newStreamUpstreamServerMetric(namespace string, metricName string, docString string, constLabels prometheus.Labels) *prometheus.Desc { + return prometheus.NewDesc(prometheus.BuildFQName(namespace, "stream_upstream_server", metricName), docString, []string{"upstream", "server"}, constLabels) } -func newStreamZoneSyncMetric(namespace string, metricName string, docString string) *prometheus.Desc { - return prometheus.NewDesc(prometheus.BuildFQName(namespace, "stream_zone_sync_status", metricName), docString, nil, nil) +func newStreamZoneSyncMetric(namespace string, metricName string, docString string, constLabels prometheus.Labels) *prometheus.Desc { + return prometheus.NewDesc(prometheus.BuildFQName(namespace, "stream_zone_sync_status", metricName), docString, nil, constLabels) } -func newStreamZoneSyncZoneMetric(namespace string, metricName string, docString string) *prometheus.Desc { - return prometheus.NewDesc(prometheus.BuildFQName(namespace, "stream_zone_sync_zone", metricName), docString, []string{"zone"}, nil) +func newStreamZoneSyncZoneMetric(namespace string, metricName string, docString string, constLabels prometheus.Labels) *prometheus.Desc { + return prometheus.NewDesc(prometheus.BuildFQName(namespace, "stream_zone_sync_zone", metricName), docString, []string{"zone"}, constLabels) } func newLocationZoneMetric(namespace string, metricName string, docString string, constLabels prometheus.Labels) *prometheus.Desc { return prometheus.NewDesc(prometheus.BuildFQName(namespace, "location_zone", metricName), docString, []string{"location_zone"}, constLabels) } -func newResolverMetric(namespace string, metricName string, docString string) *prometheus.Desc { - return prometheus.NewDesc(prometheus.BuildFQName(namespace, "resolver", metricName), docString, []string{"resolver"}, nil) +func newResolverMetric(namespace string, metricName string, docString string, constLabels prometheus.Labels) *prometheus.Desc { + return prometheus.NewDesc(prometheus.BuildFQName(namespace, "resolver", metricName), docString, []string{"resolver"}, constLabels) } diff --git a/exporter.go b/exporter.go index 3f8c7ddf..b503f665 100644 --- a/exporter.go +++ b/exporter.go @@ -20,6 +20,7 @@ import ( "github.com/nginxinc/nginx-prometheus-exporter/collector" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/prometheus/common/model" ) func getEnv(key, defaultValue string) string { @@ -67,6 +68,19 @@ func getEnvPositiveDuration(key string, defaultValue time.Duration) positiveDura return posDur } +func getEnvConstLabels(key string, defaultValue map[string]string) constLabel { + value, ok := os.LookupEnv(key) + if !ok { + return constLabel{defaultValue} + } + + cLabel, err := parseConstLabels(value) + if err != nil { + log.Fatalf("Environment variable value for %s must be a const label or a list of const labels: %v", key, err) + } + return cLabel +} + // positiveDuration is a wrapper of time.Duration to ensure only positive values are accepted type positiveDuration struct{ time.Duration } @@ -96,6 +110,56 @@ func createPositiveDurationFlag(name string, value positiveDuration, usage strin return &value } +type constLabel struct{ labels map[string]string } + +func (cl *constLabel) Set(s string) error { + labelList, err := parseConstLabels(s) + if err != nil { + return err + } + + cl.labels = labelList.labels + return nil +} + +func (cl *constLabel) String() string { + return fmt.Sprint(cl.labels) +} + +func parseConstLabels(labels string) (constLabel, error) { + if labels == "" { + return constLabel{}, nil + } + + constLabels := make(map[string]string) + labelList := strings.Split(labels, ",") + + for _, l := range labelList { + dat := strings.Split(l, "=") + if len(dat) != 2 { + return constLabel{}, fmt.Errorf("const label %s has wrong format. Example valid input 'labelName=labelValue'", l) + } + + labelName := model.LabelName(dat[0]) + if !labelName.IsValid() { + return constLabel{}, fmt.Errorf("const label %s has wrong format. %s contains invalid characters", l, labelName) + } + + labelValue := model.LabelValue(dat[1]) + if !labelValue.IsValid() { + return constLabel{}, fmt.Errorf("const label %s has wrong format. %s contains invalid characters", l, labelValue) + } + + constLabels[dat[0]] = dat[1] + } + return constLabel{labels: constLabels}, nil +} + +func createConstLabelsFlag(name string, value constLabel, usage string) *constLabel { + flag.Var(&value, name, usage) + return &value +} + func createClientWithRetries(getClient func() (interface{}, error), retries uint, retryInterval time.Duration) (interface{}, error) { var err error var nginxClient interface{} @@ -164,6 +228,7 @@ var ( defaultTimeout = getEnvPositiveDuration("TIMEOUT", time.Second*5) defaultNginxRetries = getEnvUint("NGINX_RETRIES", 0) defaultNginxRetryInterval = getEnvPositiveDuration("NGINX_RETRY_INTERVAL", time.Second*5) + defaultConstLabels = getEnvConstLabels("CONST_LABELS", map[string]string{}) // Command-line flags listenAddr = flag.String("web.listen-address", @@ -194,6 +259,10 @@ For NGINX, the stub_status page must be available through the URI. For NGINX Plu nginxRetryInterval = createPositiveDurationFlag("nginx.retry-interval", defaultNginxRetryInterval, "An interval between retries to connect to the NGINX stub_status page/NGINX Plus API on start. The default value can be overwritten by NGINX_RETRY_INTERVAL environment variable.") + + constLabels = createConstLabelsFlag("prometheus.const-labels", + defaultConstLabels, + "A comma separated list of constant labels that will be used in every metric. Format is label1=value1,label2=value2... The default value can be overwritten by CONST_LABELS environment variable.") ) func main() { @@ -207,10 +276,13 @@ func main() { prometheus.GaugeOpts{ Name: "nginxexporter_build_info", Help: "Exporter build information", - ConstLabels: prometheus.Labels{ - "version": version, - "gitCommit": gitCommit, - }, + ConstLabels: collector.MergeLabels( + constLabels.labels, + prometheus.Labels{ + "version": version, + "gitCommit": gitCommit, + }, + ), }, ) buildInfoMetric.Set(1) @@ -236,7 +308,7 @@ func main() { userAgent := fmt.Sprintf("NGINX-Prometheus-Exporter/v%v", version) userAgentRT := &userAgentRoundTripper{ agent: userAgent, - rt: transport, + rt: transport, } httpClient := &http.Client{ @@ -264,7 +336,7 @@ func main() { if err != nil { log.Fatalf("Could not create Nginx Plus Client: %v", err) } - registry.MustRegister(collector.NewNginxPlusCollector(plusClient.(*plusclient.NginxClient), "nginxplus")) + registry.MustRegister(collector.NewNginxPlusCollector(plusClient.(*plusclient.NginxClient), "nginxplus", constLabels.labels)) } else { ossClient, err := createClientWithRetries(func() (interface{}, error) { return client.NewNginxClient(httpClient, *scrapeURI) @@ -272,7 +344,7 @@ func main() { if err != nil { log.Fatalf("Could not create Nginx Client: %v", err) } - registry.MustRegister(collector.NewNginxCollector(ossClient.(*client.NginxClient), "nginx")) + registry.MustRegister(collector.NewNginxCollector(ossClient.(*client.NginxClient), "nginx", constLabels.labels)) } http.Handle(*metricsPath, promhttp.HandlerFor(registry, promhttp.HandlerOpts{})) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { diff --git a/exporter_test.go b/exporter_test.go index 38407c49..99eb3837 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -175,3 +175,61 @@ func TestParseUnixSocketAddress(t *testing.T) { }) } } + +func TestParseConstLabels(t *testing.T) { + tests := []struct { + name string + labels string + want constLabel + wantErr bool + }{ + { + name: "Const labels with no labels", + labels: "", + want: constLabel{}, + wantErr: false, + }, + { + name: "Const labels with one label with valid format", + labels: "label=valid", + want: constLabel{labels: map[string]string{"label": "valid"}}, + wantErr: false, + }, + { + name: "Const labels with one label with invalid format", + labels: "label: invalid", + want: constLabel{}, + wantErr: true, + }, + { + name: "Const labels with invalid format for multiple labels", + labels: "label=valid,,label2=wrongformat", + want: constLabel{}, + wantErr: true, + }, + { + name: "Const labels with multiple labels, one label with invalid format", + labels: "label=valid,label2:wrongformat", + want: constLabel{}, + wantErr: true, + }, + { + name: "Const labels with label name containing invalid char", + labels: "l bel=invalid", + want: constLabel{}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := parseConstLabels(tt.labels) + if (err != nil) != tt.wantErr { + t.Errorf("parseConstLabels() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseConstLabels() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/go.mod b/go.mod index c6e6d6f3..1e8b0ca3 100644 --- a/go.mod +++ b/go.mod @@ -5,4 +5,5 @@ go 1.13 require ( github.com/nginxinc/nginx-plus-go-client v0.5.0 github.com/prometheus/client_golang v0.9.2 + github.com/prometheus/common v0.0.0-20181126121408-4724e9255275 )