diff options
Diffstat (limited to '')
56 files changed, 7103 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/nginx/README.md b/src/go/collectors/go.d.plugin/modules/nginx/README.md new file mode 120000 index 000000000..7b19fe44f --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/README.md @@ -0,0 +1 @@ +integrations/nginx.md
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/nginx/apiclient.go b/src/go/collectors/go.d.plugin/modules/nginx/apiclient.go new file mode 100644 index 000000000..8e1003b44 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/apiclient.go @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginx + +import ( + "bufio" + "fmt" + "io" + "net/http" + "regexp" + "strconv" + "strings" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +const ( + connActive = "connActive" + connAccepts = "connAccepts" + connHandled = "connHandled" + requests = "requests" + requestTime = "requestTime" + connReading = "connReading" + connWriting = "connWriting" + connWaiting = "connWaiting" +) + +var ( + nginxSeq = []string{ + connActive, + connAccepts, + connHandled, + requests, + connReading, + connWriting, + connWaiting, + } + tengineSeq = []string{ + connActive, + connAccepts, + connHandled, + requests, + requestTime, + connReading, + connWriting, + connWaiting, + } + + reStatus = regexp.MustCompile(`^Active connections: ([0-9]+)\n[^\d]+([0-9]+) ([0-9]+) ([0-9]+) ?([0-9]+)?\nReading: ([0-9]+) Writing: ([0-9]+) Waiting: ([0-9]+)`) +) + +func newAPIClient(client *http.Client, request web.Request) *apiClient { + return &apiClient{httpClient: client, request: request} +} + +type apiClient struct { + httpClient *http.Client + request web.Request +} + +func (a apiClient) getStubStatus() (*stubStatus, error) { + req, err := web.NewHTTPRequest(a.request) + if err != nil { + return nil, fmt.Errorf("error on creating request : %v", err) + } + + resp, err := a.doRequestOK(req) + defer closeBody(resp) + if err != nil { + return nil, err + } + + status, err := parseStubStatus(resp.Body) + if err != nil { + return nil, fmt.Errorf("error on parsing response : %v", err) + } + + return status, nil +} + +func (a apiClient) doRequestOK(req *http.Request) (*http.Response, error) { + resp, err := a.httpClient.Do(req) + if err != nil { + return resp, fmt.Errorf("error on request : %v", err) + } + + if resp.StatusCode != http.StatusOK { + return resp, fmt.Errorf("%s returned HTTP status %d", req.URL, resp.StatusCode) + } + + return resp, err +} + +func closeBody(resp *http.Response) { + if resp != nil && resp.Body != nil { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + } +} + +func parseStubStatus(r io.Reader) (*stubStatus, error) { + sc := bufio.NewScanner(r) + var lines []string + + for sc.Scan() { + lines = append(lines, strings.Trim(sc.Text(), "\r\n ")) + } + + parsed := reStatus.FindStringSubmatch(strings.Join(lines, "\n")) + + if len(parsed) == 0 { + return nil, fmt.Errorf("can't parse '%v'", lines) + } + + parsed = parsed[1:] + + var ( + seq []string + status stubStatus + ) + + switch len(parsed) { + default: + return nil, fmt.Errorf("invalid number of fields, got %d, expect %d or %d", len(parsed), len(nginxSeq), len(tengineSeq)) + case len(nginxSeq): + seq = nginxSeq + case len(tengineSeq): + seq = tengineSeq + } + + for i, key := range seq { + strValue := parsed[i] + if strValue == "" { + continue + } + value := mustParseInt(strValue) + switch key { + default: + return nil, fmt.Errorf("unknown key in seq : %s", key) + case connActive: + status.Connections.Active = value + case connAccepts: + status.Connections.Accepts = value + case connHandled: + status.Connections.Handled = value + case requests: + status.Requests.Total = value + case connReading: + status.Connections.Reading = value + case connWriting: + status.Connections.Writing = value + case connWaiting: + status.Connections.Waiting = value + case requestTime: + status.Requests.Time = &value + } + } + + return &status, nil +} + +func mustParseInt(value string) int64 { + v, err := strconv.ParseInt(value, 10, 64) + if err != nil { + panic(err) + } + return v +} diff --git a/src/go/collectors/go.d.plugin/modules/nginx/charts.go b/src/go/collectors/go.d.plugin/modules/nginx/charts.go new file mode 100644 index 000000000..95f9d8aaf --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/charts.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginx + +import "github.com/netdata/netdata/go/go.d.plugin/agent/module" + +type ( + // Charts is an alias for module.Charts + Charts = module.Charts + // Dims is an alias for module.Dims + Dims = module.Dims +) + +var charts = Charts{ + { + ID: "connections", + Title: "Active Client Connections Including Waiting Connections", + Units: "connections", + Fam: "connections", + Ctx: "nginx.connections", + Dims: Dims{ + {ID: "active"}, + }, + }, + { + ID: "connections_statuses", + Title: "Active Connections Per Status", + Units: "connections", + Fam: "connections", + Ctx: "nginx.connections_status", + Dims: Dims{ + {ID: "reading"}, + {ID: "writing"}, + {ID: "waiting", Name: "idle"}, + }, + }, + { + ID: "connections_accepted_handled", + Title: "Accepted And Handled Connections", + Units: "connections/s", + Fam: "connections", + Ctx: "nginx.connections_accepted_handled", + Dims: Dims{ + {ID: "accepts", Name: "accepted", Algo: module.Incremental}, + {ID: "handled", Algo: module.Incremental}, + }, + }, + { + ID: "requests", + Title: "Client Requests", + Units: "requests/s", + Fam: "requests", + Ctx: "nginx.requests", + Dims: Dims{ + {ID: "requests", Algo: module.Incremental}, + }, + }, +} diff --git a/src/go/collectors/go.d.plugin/modules/nginx/collect.go b/src/go/collectors/go.d.plugin/modules/nginx/collect.go new file mode 100644 index 000000000..533f98808 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/collect.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginx + +import ( + "github.com/netdata/netdata/go/go.d.plugin/pkg/stm" +) + +func (n *Nginx) collect() (map[string]int64, error) { + status, err := n.apiClient.getStubStatus() + + if err != nil { + return nil, err + } + + return stm.ToMap(status), nil +} diff --git a/src/go/collectors/go.d.plugin/modules/nginx/config_schema.json b/src/go/collectors/go.d.plugin/modules/nginx/config_schema.json new file mode 100644 index 000000000..ed361b420 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/config_schema.json @@ -0,0 +1,177 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NGINX collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "url": { + "title": "URL", + "description": "The URL of the NGINX [status page](https://nginx.org/en/docs/http/ngx_http_stub_status_module.html).", + "type": "string", + "default": "http://127.0.0.1/stub_status", + "format": "uri" + }, + "timeout": { + "title": "Timeout", + "description": "The timeout in seconds for the HTTP request.", + "type": "number", + "minimum": 0.5, + "default": 1 + }, + "not_follow_redirects": { + "title": "Not follow redirects", + "description": "If set, the client will not follow HTTP redirects automatically.", + "type": "boolean" + }, + "username": { + "title": "Username", + "description": "The username for basic authentication.", + "type": "string", + "sensitive": true + }, + "password": { + "title": "Password", + "description": "The password for basic authentication.", + "type": "string", + "sensitive": true + }, + "proxy_url": { + "title": "Proxy URL", + "description": "The URL of the proxy server.", + "type": "string" + }, + "proxy_username": { + "title": "Proxy username", + "description": "The username for proxy authentication.", + "type": "string", + "sensitive": true + }, + "proxy_password": { + "title": "Proxy password", + "description": "The password for proxy authentication.", + "type": "string", + "sensitive": true + }, + "headers": { + "title": "Headers", + "description": "Additional HTTP headers to include in the request.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "tls_skip_verify": { + "title": "Skip TLS verification", + "description": "If set, TLS certificate verification will be skipped.", + "type": "boolean" + }, + "tls_ca": { + "title": "TLS CA", + "description": "The path to the CA certificate file for TLS verification.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_cert": { + "title": "TLS certificate", + "description": "The path to the client certificate file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_key": { + "title": "TLS key", + "description": "The path to the client key file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + }, + "body": { + "title": "Body", + "type": "string" + }, + "method": { + "title": "Method", + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "ui:flavour": "tabs", + "ui:options": { + "tabs": [ + { + "title": "Base", + "fields": [ + "update_every", + "url", + "timeout", + "not_follow_redirects" + ] + }, + { + "title": "Auth", + "fields": [ + "username", + "password" + ] + }, + { + "title": "TLS", + "fields": [ + "tls_skip_verify", + "tls_ca", + "tls_cert", + "tls_key" + ] + }, + { + "title": "Proxy", + "fields": [ + "proxy_url", + "proxy_username", + "proxy_password" + ] + }, + { + "title": "Headers", + "fields": [ + "headers" + ] + } + ] + }, + "uiOptions": { + "fullPage": true + }, + "body": { + "ui:widget": "hidden" + }, + "method": { + "ui:widget": "hidden" + }, + "timeout": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + }, + "password": { + "ui:widget": "password" + }, + "proxy_password": { + "ui:widget": "password" + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginx/integrations/nginx.md b/src/go/collectors/go.d.plugin/modules/nginx/integrations/nginx.md new file mode 100644 index 000000000..63b580992 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/integrations/nginx.md @@ -0,0 +1,232 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/nginx/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/nginx/metadata.yaml" +sidebar_label: "NGINX" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/Web Servers and Web Proxies" +most_popular: True +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# NGINX + + +<img src="https://netdata.cloud/img/nginx.svg" width="150"/> + + +Plugin: go.d.plugin +Module: nginx + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors the activity and performance of NGINX servers, and collects metrics such as the number of connections, their status, and client requests. + + +It sends HTTP requests to the NGINX location [stub-status](https://nginx.org/en/docs/http/ngx_http_stub_status_module.html), which is a built-in location that provides metrics about the NGINX server. + + +This collector is supported on all platforms. + +This collector supports collecting metrics from multiple instances of this integration, including remote instances. + + +### Default Behavior + +#### Auto-Detection + +By default, it detects NGINX instances running on localhost that are listening on port 80. +On startup, it tries to collect metrics from: + +- http://127.0.0.1/basic_status +- http://localhost/stub_status +- http://127.0.0.1/stub_status +- http://127.0.0.1/nginx_status +- http://127.0.0.1/status + + +#### Limits + +The default configuration for this integration does not impose any limits on data collection. + +#### Performance Impact + +The default configuration for this integration is not expected to impose a significant performance impact on the system. + + +## Metrics + +Metrics grouped by *scope*. + +The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels. + + + +### Per NGINX instance + +These metrics refer to the entire monitored application. + +This scope has no labels. + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginx.connections | active | connections | +| nginx.connections_status | reading, writing, idle | connections | +| nginx.connections_accepted_handled | accepted, handled | connections/s | +| nginx.requests | requests | requests/s | + + + +## Alerts + +There are no alerts configured by default for this integration. + + +## Setup + +### Prerequisites + +#### Enable status support + +Configure [ngx_http_stub_status_module](https://nginx.org/en/docs/http/ngx_http_stub_status_module.html). + + + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/nginx.conf`. + + +You can edit the configuration file using the `edit-config` script from the +Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory). + +```bash +cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata +sudo ./edit-config go.d/nginx.conf +``` +#### Options + +The following options can be defined globally: update_every, autodetection_retry. + + +<details open><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 1 | no | +| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no | +| url | Server URL. | http://127.0.0.1/stub_status | yes | +| timeout | HTTP request timeout. | 1 | no | +| username | Username for basic HTTP authentication. | | no | +| password | Password for basic HTTP authentication. | | no | +| proxy_url | Proxy URL. | | no | +| proxy_username | Username for proxy basic HTTP authentication. | | no | +| proxy_password | Password for proxy basic HTTP authentication. | | no | +| method | HTTP request method. | GET | no | +| body | HTTP request body. | | no | +| headers | HTTP request headers. | | no | +| not_follow_redirects | Redirect handling policy. Controls whether the client follows redirects. | no | no | +| tls_skip_verify | Server certificate chain and hostname validation policy. Controls whether the client performs this check. | no | no | +| tls_ca | Certification authority that the client uses when verifying the server's certificates. | | no | +| tls_cert | Client TLS certificate. | | no | +| tls_key | Client TLS key. | | no | + +</details> + +#### Examples + +##### Basic + +A basic example configuration. + +```yaml +jobs: + - name: local + url: http://127.0.0.1/stub_status + +``` +##### HTTP authentication + +Basic HTTP authentication. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1/stub_status + username: username + password: password + +``` +</details> + +##### HTTPS with self-signed certificate + +NGINX with enabled HTTPS and self-signed certificate. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1/stub_status + tls_skip_verify: yes + +``` +</details> + +##### Multi-instance + +> **Note**: When you define multiple jobs, their names must be unique. + +Collecting metrics from local and remote instances. + + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1/stub_status + + - name: remote + url: http://192.0.2.1/stub_status + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +To troubleshoot issues with the `nginx` collector, run the `go.d.plugin` with the debug option enabled. The output +should give you clues as to why the collector isn't working. + +- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on + your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`. + + ```bash + cd /usr/libexec/netdata/plugins.d/ + ``` + +- Switch to the `netdata` user. + + ```bash + sudo -u netdata -s + ``` + +- Run the `go.d.plugin` to debug the collector: + + ```bash + ./go.d.plugin -d -m nginx + ``` + + diff --git a/src/go/collectors/go.d.plugin/modules/nginx/metadata.yaml b/src/go/collectors/go.d.plugin/modules/nginx/metadata.yaml new file mode 100644 index 000000000..49b12c4ec --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/metadata.yaml @@ -0,0 +1,226 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-nginx + plugin_name: go.d.plugin + module_name: nginx + monitored_instance: + name: NGINX + link: https://www.nginx.com/ + categories: + - data-collection.web-servers-and-web-proxies + icon_filename: nginx.svg + related_resources: + integrations: + list: + - plugin_name: go.d.plugin + module_name: httpcheck + - plugin_name: go.d.plugin + module_name: web_log + - plugin_name: apps.plugin + module_name: apps + - plugin_name: cgroups.plugin + module_name: cgroups + alternative_monitored_instances: [] + info_provided_to_referring_integrations: + description: "" + keywords: + - nginx + - web + - webserver + - http + - proxy + most_popular: true + overview: + data_collection: + metrics_description: | + This collector monitors the activity and performance of NGINX servers, and collects metrics such as the number of connections, their status, and client requests. + method_description: | + It sends HTTP requests to the NGINX location [stub-status](https://nginx.org/en/docs/http/ngx_http_stub_status_module.html), which is a built-in location that provides metrics about the NGINX server. + default_behavior: + auto_detection: + description: | + By default, it detects NGINX instances running on localhost that are listening on port 80. + On startup, it tries to collect metrics from: + + - http://127.0.0.1/basic_status + - http://localhost/stub_status + - http://127.0.0.1/stub_status + - http://127.0.0.1/nginx_status + - http://127.0.0.1/status + limits: + description: "" + performance_impact: + description: "" + additional_permissions: + description: "" + multi_instance: true + supported_platforms: + include: [] + exclude: [] + setup: + prerequisites: + list: + - title: Enable status support + description: | + Configure [ngx_http_stub_status_module](https://nginx.org/en/docs/http/ngx_http_stub_status_module.html). + configuration: + file: + name: go.d/nginx.conf + options: + description: | + The following options can be defined globally: update_every, autodetection_retry. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 1 + required: false + - name: autodetection_retry + description: Recheck interval in seconds. Zero means no recheck will be scheduled. + default_value: 0 + required: false + - name: url + description: Server URL. + default_value: http://127.0.0.1/stub_status + required: true + - name: timeout + description: HTTP request timeout. + default_value: 1 + required: false + - name: username + description: Username for basic HTTP authentication. + default_value: "" + required: false + - name: password + description: Password for basic HTTP authentication. + default_value: "" + required: false + - name: proxy_url + description: Proxy URL. + default_value: "" + required: false + - name: proxy_username + description: Username for proxy basic HTTP authentication. + default_value: "" + required: false + - name: proxy_password + description: Password for proxy basic HTTP authentication. + default_value: "" + required: false + - name: method + description: HTTP request method. + default_value: GET + required: false + - name: body + description: HTTP request body. + default_value: "" + required: false + - name: headers + description: HTTP request headers. + default_value: "" + required: false + - name: not_follow_redirects + description: Redirect handling policy. Controls whether the client follows redirects. + default_value: false + required: false + - name: tls_skip_verify + description: Server certificate chain and hostname validation policy. Controls whether the client performs this check. + default_value: false + required: false + - name: tls_ca + description: Certification authority that the client uses when verifying the server's certificates. + default_value: "" + required: false + - name: tls_cert + description: Client TLS certificate. + default_value: "" + required: false + - name: tls_key + description: Client TLS key. + default_value: "" + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Basic + description: A basic example configuration. + folding: + enabled: false + config: | + jobs: + - name: local + url: http://127.0.0.1/stub_status + - name: HTTP authentication + description: Basic HTTP authentication. + config: | + jobs: + - name: local + url: http://127.0.0.1/stub_status + username: username + password: password + - name: HTTPS with self-signed certificate + description: NGINX with enabled HTTPS and self-signed certificate. + config: | + jobs: + - name: local + url: http://127.0.0.1/stub_status + tls_skip_verify: yes + - name: Multi-instance + description: | + > **Note**: When you define multiple jobs, their names must be unique. + + Collecting metrics from local and remote instances. + config: | + jobs: + - name: local + url: http://127.0.0.1/stub_status + + - name: remote + url: http://192.0.2.1/stub_status + troubleshooting: + problems: + list: [] + alerts: [] + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: global + description: These metrics refer to the entire monitored application. + labels: [] + metrics: + - name: nginx.connections + description: Active Client Connections Including Waiting Connections + unit: connections + chart_type: line + dimensions: + - name: active + - name: nginx.connections_status + description: Active Connections Per Status + unit: connections + chart_type: line + dimensions: + - name: reading + - name: writing + - name: idle + - name: nginx.connections_accepted_handled + description: Accepted And Handled Connections + unit: connections/s + chart_type: line + dimensions: + - name: accepted + - name: handled + - name: nginx.requests + description: Client Requests + unit: requests/s + chart_type: line + dimensions: + - name: requests diff --git a/src/go/collectors/go.d.plugin/modules/nginx/metrics.go b/src/go/collectors/go.d.plugin/modules/nginx/metrics.go new file mode 100644 index 000000000..66e6a160e --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/metrics.go @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginx + +type stubStatus struct { + Connections struct { + // The current number of active client connections including Waiting connections. + Active int64 `stm:"active"` + + // The total number of accepted client connections. + Accepts int64 `stm:"accepts"` + + // The total number of handled connections. + // Generally, the parameter value is the same as accepts unless some resource limits have been reached. + Handled int64 `stm:"handled"` + + // The current number of connections where nginx is reading the request header. + Reading int64 `stm:"reading"` + + // The current number of connections where nginx is writing the response back to the client. + Writing int64 `stm:"writing"` + + // The current number of idle client connections waiting for a request. + Waiting int64 `stm:"waiting"` + } `stm:""` + Requests struct { + // The total number of client requests. + Total int64 `stm:"requests"` + + // Note: tengine specific + // The total requests' response time, which is in millisecond + Time *int64 `stm:"request_time"` + } `stm:""` +} diff --git a/src/go/collectors/go.d.plugin/modules/nginx/nginx.go b/src/go/collectors/go.d.plugin/modules/nginx/nginx.go new file mode 100644 index 000000000..2feb6bb0b --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/nginx.go @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginx + +import ( + _ "embed" + "errors" + "time" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("nginx", module.Creator{ + JobConfigSchema: configSchema, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *Nginx { + return &Nginx{ + Config: Config{ + HTTP: web.HTTP{ + Request: web.Request{ + URL: "http://127.0.0.1/stub_status", + }, + Client: web.Client{ + Timeout: web.Duration(time.Second * 1), + }, + }, + }} +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + web.HTTP `yaml:",inline" json:""` +} + +type Nginx struct { + module.Base + Config `yaml:",inline" json:""` + + apiClient *apiClient +} + +func (n *Nginx) Configuration() any { + return n.Config +} + +func (n *Nginx) Init() error { + if n.URL == "" { + n.Error("URL not set") + return errors.New("url not set") + } + + client, err := web.NewHTTPClient(n.Client) + if err != nil { + n.Error(err) + return err + } + + n.apiClient = newAPIClient(client, n.Request) + + n.Debugf("using URL %s", n.URL) + n.Debugf("using timeout: %s", n.Timeout) + + return nil +} + +func (n *Nginx) Check() error { + mx, err := n.collect() + if err != nil { + n.Error(err) + return err + } + if len(mx) == 0 { + return errors.New("no metrics collected") + + } + return nil +} + +func (n *Nginx) Charts() *Charts { + return charts.Copy() +} + +func (n *Nginx) Collect() map[string]int64 { + mx, err := n.collect() + if err != nil { + n.Error(err) + return nil + } + + return mx +} + +func (n *Nginx) Cleanup() { + if n.apiClient != nil && n.apiClient.httpClient != nil { + n.apiClient.httpClient.CloseIdleConnections() + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginx/nginx_test.go b/src/go/collectors/go.d.plugin/modules/nginx/nginx_test.go new file mode 100644 index 000000000..68308d141 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/nginx_test.go @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginx + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") + + dataStatusMetrics, _ = os.ReadFile("testdata/status.txt") + dataTengineStatusMetrics, _ = os.ReadFile("testdata/tengine-status.txt") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + "dataStatusMetrics": dataStatusMetrics, + "dataTengineStatusMetrics": dataTengineStatusMetrics, + } { + require.NotNil(t, data, name) + } +} + +func TestNginx_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &Nginx{}, dataConfigJSON, dataConfigYAML) +} + +func TestNginx_Cleanup(t *testing.T) { + New().Cleanup() +} + +func TestNginx_Init(t *testing.T) { + job := New() + + require.NoError(t, job.Init()) + assert.NotNil(t, job.apiClient) +} + +func TestNginx_Check(t *testing.T) { + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(dataStatusMetrics) + })) + defer ts.Close() + + job := New() + job.URL = ts.URL + require.NoError(t, job.Init()) + assert.NoError(t, job.Check()) +} + +func TestNginx_CheckNG(t *testing.T) { + job := New() + + job.URL = "http://127.0.0.1:38001/us" + require.NoError(t, job.Init()) + assert.Error(t, job.Check()) +} + +func TestNginx_Charts(t *testing.T) { + assert.NotNil(t, New().Charts()) +} + +func TestNginx_Collect(t *testing.T) { + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(dataStatusMetrics) + })) + defer ts.Close() + + job := New() + job.URL = ts.URL + require.NoError(t, job.Init()) + require.NoError(t, job.Check()) + + expected := map[string]int64{ + "accepts": 36, + "active": 1, + "handled": 36, + "reading": 0, + "requests": 126, + "waiting": 0, + "writing": 1, + } + + assert.Equal(t, expected, job.Collect()) +} + +func TestNginx_CollectTengine(t *testing.T) { + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(dataTengineStatusMetrics) + })) + defer ts.Close() + + job := New() + job.URL = ts.URL + require.NoError(t, job.Init()) + require.NoError(t, job.Check()) + + expected := map[string]int64{ + "accepts": 1140, + "active": 1, + "handled": 1140, + "reading": 0, + "request_time": 75806, + "requests": 1140, + "waiting": 0, + "writing": 1, + } + + assert.Equal(t, expected, job.Collect()) +} + +func TestNginx_InvalidData(t *testing.T) { + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("hello and goodbye")) + })) + defer ts.Close() + + job := New() + job.URL = ts.URL + require.NoError(t, job.Init()) + assert.Error(t, job.Check()) +} + +func TestNginx_404(t *testing.T) { + ts := httptest.NewServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + defer ts.Close() + + job := New() + job.URL = ts.URL + require.NoError(t, job.Init()) + assert.Error(t, job.Check()) +} diff --git a/src/go/collectors/go.d.plugin/modules/nginx/testdata/config.json b/src/go/collectors/go.d.plugin/modules/nginx/testdata/config.json new file mode 100644 index 000000000..984c3ed6e --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/testdata/config.json @@ -0,0 +1,20 @@ +{ + "update_every": 123, + "url": "ok", + "body": "ok", + "method": "ok", + "headers": { + "ok": "ok" + }, + "username": "ok", + "password": "ok", + "proxy_url": "ok", + "proxy_username": "ok", + "proxy_password": "ok", + "timeout": 123.123, + "not_follow_redirects": true, + "tls_ca": "ok", + "tls_cert": "ok", + "tls_key": "ok", + "tls_skip_verify": true +} diff --git a/src/go/collectors/go.d.plugin/modules/nginx/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/nginx/testdata/config.yaml new file mode 100644 index 000000000..8558b61cc --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/testdata/config.yaml @@ -0,0 +1,17 @@ +update_every: 123 +url: "ok" +body: "ok" +method: "ok" +headers: + ok: "ok" +username: "ok" +password: "ok" +proxy_url: "ok" +proxy_username: "ok" +proxy_password: "ok" +timeout: 123.123 +not_follow_redirects: yes +tls_ca: "ok" +tls_cert: "ok" +tls_key: "ok" +tls_skip_verify: yes diff --git a/src/go/collectors/go.d.plugin/modules/nginx/testdata/status.txt b/src/go/collectors/go.d.plugin/modules/nginx/testdata/status.txt new file mode 100644 index 000000000..f4835bef4 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/testdata/status.txt @@ -0,0 +1,4 @@ +Active connections: 1 +server accepts handled requests +36 36 126 +Reading: 0 Writing: 1 Waiting: 0
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/nginx/testdata/tengine-status.txt b/src/go/collectors/go.d.plugin/modules/nginx/testdata/tengine-status.txt new file mode 100644 index 000000000..1e6a62c21 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginx/testdata/tengine-status.txt @@ -0,0 +1,4 @@ +Active connections: 1 +server accepts handled requests request_time +1140 1140 1140 75806 +Reading: 0 Writing: 1 Waiting: 0
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/README.md b/src/go/collectors/go.d.plugin/modules/nginxplus/README.md new file mode 120000 index 000000000..16cb6c1b7 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/README.md @@ -0,0 +1 @@ +integrations/nginx_plus.md
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/cache.go b/src/go/collectors/go.d.plugin/modules/nginxplus/cache.go new file mode 100644 index 000000000..af58f3a55 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/cache.go @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxplus + +func newCache() *cache { + return &cache{ + httpCaches: make(map[string]*cacheHTTPCacheEntry), + httpServerZones: make(map[string]*cacheZoneEntry), + httpLocationZones: make(map[string]*cacheZoneEntry), + httpUpstreams: make(map[string]*cacheUpstreamEntry), + httpUpstreamServers: make(map[string]*cacheUpstreamServerEntry), + streamServerZones: make(map[string]*cacheZoneEntry), + streamUpstreams: make(map[string]*cacheUpstreamEntry), + streamUpstreamServers: make(map[string]*cacheUpstreamServerEntry), + resolvers: make(map[string]*cacheResolverEntry), + } +} + +type ( + cache struct { + httpCaches map[string]*cacheHTTPCacheEntry + httpServerZones map[string]*cacheZoneEntry + httpLocationZones map[string]*cacheZoneEntry + httpUpstreams map[string]*cacheUpstreamEntry + httpUpstreamServers map[string]*cacheUpstreamServerEntry + streamServerZones map[string]*cacheZoneEntry + streamUpstreams map[string]*cacheUpstreamEntry + streamUpstreamServers map[string]*cacheUpstreamServerEntry + resolvers map[string]*cacheResolverEntry + } + cacheEntry struct { + hasCharts bool + updated bool + notSeenTimes int + } + cacheHTTPCacheEntry struct { + name string + cacheEntry + } + cacheResolverEntry struct { + zone string + cacheEntry + } + cacheZoneEntry struct { + zone string + cacheEntry + } + cacheUpstreamEntry struct { + name string + zone string + cacheEntry + } + cacheUpstreamServerEntry struct { + name string + zone string + serverAddr string + serverName string + cacheEntry + } +) + +func (c *cache) resetUpdated() { + for _, v := range c.httpCaches { + v.updated = false + } + for _, v := range c.httpServerZones { + v.updated = false + } + for _, v := range c.httpLocationZones { + v.updated = false + } + for _, v := range c.httpUpstreams { + v.updated = false + } + for _, v := range c.httpUpstreamServers { + v.updated = false + } + for _, v := range c.streamServerZones { + v.updated = false + } + for _, v := range c.streamUpstreams { + v.updated = false + } + for _, v := range c.streamUpstreamServers { + v.updated = false + } + for _, v := range c.resolvers { + v.updated = false + } +} + +func (c *cache) putHTTPCache(cache string) { + v, ok := c.httpCaches[cache] + if !ok { + v = &cacheHTTPCacheEntry{name: cache} + c.httpCaches[cache] = v + } + v.updated, v.notSeenTimes = true, 0 +} + +func (c *cache) putHTTPServerZone(zone string) { + v, ok := c.httpServerZones[zone] + if !ok { + v = &cacheZoneEntry{zone: zone} + c.httpServerZones[zone] = v + } + v.updated, v.notSeenTimes = true, 0 +} + +func (c *cache) putHTTPLocationZone(zone string) { + v, ok := c.httpLocationZones[zone] + if !ok { + v = &cacheZoneEntry{zone: zone} + c.httpLocationZones[zone] = v + } + v.updated, v.notSeenTimes = true, 0 +} + +func (c *cache) putHTTPUpstream(name, zone string) { + v, ok := c.httpUpstreams[name+"_"+zone] + if !ok { + v = &cacheUpstreamEntry{name: name, zone: zone} + c.httpUpstreams[name+"_"+zone] = v + } + v.updated, v.notSeenTimes = true, 0 +} + +func (c *cache) putHTTPUpstreamServer(name, serverAddr, serverName, zone string) { + v, ok := c.httpUpstreamServers[name+"_"+serverAddr+"_"+zone] + if !ok { + v = &cacheUpstreamServerEntry{name: name, zone: zone, serverAddr: serverAddr, serverName: serverName} + c.httpUpstreamServers[name+"_"+serverAddr+"_"+zone] = v + } + v.updated, v.notSeenTimes = true, 0 +} + +func (c *cache) putStreamServerZone(zone string) { + v, ok := c.streamServerZones[zone] + if !ok { + v = &cacheZoneEntry{zone: zone} + c.streamServerZones[zone] = v + } + v.updated, v.notSeenTimes = true, 0 + +} + +func (c *cache) putStreamUpstream(name, zone string) { + v, ok := c.streamUpstreams[name+"_"+zone] + if !ok { + v = &cacheUpstreamEntry{name: name, zone: zone} + c.streamUpstreams[name+"_"+zone] = v + } + v.updated, v.notSeenTimes = true, 0 +} + +func (c *cache) putStreamUpstreamServer(name, serverAddr, serverName, zone string) { + v, ok := c.streamUpstreamServers[name+"_"+serverAddr+"_"+zone] + if !ok { + v = &cacheUpstreamServerEntry{name: name, zone: zone, serverAddr: serverAddr, serverName: serverName} + c.streamUpstreamServers[name+"_"+serverAddr+"_"+zone] = v + } + v.updated, v.notSeenTimes = true, 0 +} + +func (c *cache) putResolver(zone string) { + v, ok := c.resolvers[zone] + if !ok { + v = &cacheResolverEntry{zone: zone} + c.resolvers[zone] = v + } + v.updated, v.notSeenTimes = true, 0 +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/charts.go b/src/go/collectors/go.d.plugin/modules/nginxplus/charts.go new file mode 100644 index 000000000..c50390984 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/charts.go @@ -0,0 +1,981 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxplus + +import ( + "fmt" + "strings" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" +) + +const ( + prioClientConnectionsRate = module.Priority + iota + prioClientConnectionsCount + + prioSSLHandshakesRate + prioSSLHandshakesFailuresRate + prioSSLVerificationErrorsRate + prioSSLSessionReusesRate + + prioHTTPRequestsRate + prioHTTPRequestsCount + prioHTTPServerZoneRequestsRate + prioHTTPLocationZoneRequestsRate + prioHTTPServerZoneRequestsProcessingCount + prioHTTPServerZoneRequestsDiscardedRate + prioHTTPLocationZoneRequestsDiscardedRate + + prioHTTPServerZoneResponsesPerCodeClassRate + prioHTTPLocationZoneResponsesPerCodeClassRate + + prioHTTPServerZoneTrafficRate + prioHTTPLocationZoneTrafficRate + + prioHTTPUpstreamPeersCount + prioHTTPUpstreamZombiesCount + prioHTTPUpstreamKeepaliveCount + + prioHTTPUpstreamServerState + prioHTTPUpstreamServerDowntime + + prioHTTPUpstreamServerConnectionsCount + + prioHTTPUpstreamServerRequestsRate + + prioHTTPUpstreamServerResponsesPerCodeClassRate + + prioHTTPUpstreamServerResponseTime + prioHTTPUpstreamServerResponseHeaderTime + + prioHTTPUpstreamServerTrafficRate + + prioHTTPCacheState + prioHTTPCacheIOPS + prioHTTPCacheIO + prioHTTPCacheSize + + prioStreamServerZoneConnectionsRate + prioStreamServerZoneConnectionsProcessingCount + prioStreamServerZoneConnectionsDiscardedRate + + prioStreamServerZoneSessionsPerCodeClassRate + + prioStreamServerZoneTrafficRate + + prioStreamUpstreamPeersCount + prioStreamUpstreamZombiesCount + + prioStreamUpstreamServerState + prioStreamUpstreamServerDowntime + + prioStreamUpstreamServerConnectionsRate + prioStreamUpstreamServerConnectionsCount + + prioStreamUpstreamServerTrafficRate + + prioResolverZoneRequestsRate + prioResolverZoneResponsesRate + + prioUptime +) + +var ( + baseCharts = module.Charts{ + clientConnectionsRateChart.Copy(), + clientConnectionsCountChart.Copy(), + sslHandshakesRateChart.Copy(), + sslHandshakesFailuresRateChart.Copy(), + sslVerificationErrorsRateChart.Copy(), + sslSessionReusesRateChart.Copy(), + httpRequestsRateChart.Copy(), + httpRequestsCountChart.Copy(), + uptimeChart.Copy(), + } + + clientConnectionsRateChart = module.Chart{ + ID: "client_connections_rate", + Title: "Client connections rate", + Units: "connections/s", + Fam: "connections", + Ctx: "nginxplus.client_connections_rate", + Priority: prioClientConnectionsRate, + Dims: module.Dims{ + {ID: "connections_accepted", Name: "accepted", Algo: module.Incremental}, + {ID: "connections_dropped", Name: "dropped", Algo: module.Incremental}, + }, + } + clientConnectionsCountChart = module.Chart{ + ID: "client_connections_count", + Title: "Client connections", + Units: "connections", + Fam: "connections", + Ctx: "nginxplus.client_connections_count", + Priority: prioClientConnectionsCount, + Dims: module.Dims{ + {ID: "connections_active", Name: "active"}, + {ID: "connections_idle", Name: "idle"}, + }, + } + sslHandshakesRateChart = module.Chart{ + ID: "ssl_handshakes_rate", + Title: "SSL handshakes rate", + Units: "handshakes/s", + Fam: "ssl", + Ctx: "nginxplus.ssl_handshakes_rate", + Priority: prioSSLHandshakesRate, + Dims: module.Dims{ + {ID: "ssl_handshakes", Name: "successful", Algo: module.Incremental}, + {ID: "ssl_handshakes_failed", Name: "failed", Algo: module.Incremental}, + }, + } + sslHandshakesFailuresRateChart = module.Chart{ + ID: "ssl_handshakes_failures_rate", + Title: "SSL handshakes failures rate", + Units: "failures/s", + Fam: "ssl", + Ctx: "nginxplus.ssl_handshakes_failures_rate", + Priority: prioSSLHandshakesFailuresRate, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "ssl_no_common_protocol", Name: "no_common_protocol", Algo: module.Incremental}, + {ID: "ssl_no_common_cipher", Name: "no_common_cipher", Algo: module.Incremental}, + {ID: "ssl_handshake_timeout", Name: "timeout", Algo: module.Incremental}, + {ID: "ssl_peer_rejected_cert", Name: "peer_rejected_cert", Algo: module.Incremental}, + }, + } + sslVerificationErrorsRateChart = module.Chart{ + ID: "ssl_verification_errors_rate", + Title: "SSL verification errors rate", + Units: "errors/s", + Fam: "ssl", + Ctx: "nginxplus.ssl_verification_errors_rate", + Priority: prioSSLVerificationErrorsRate, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "ssl_verify_failures_no_cert", Name: "no_cert", Algo: module.Incremental}, + {ID: "ssl_verify_failures_expired_cert", Name: "expired_cert", Algo: module.Incremental}, + {ID: "ssl_verify_failures_revoked_cert", Name: "revoked_cert", Algo: module.Incremental}, + {ID: "ssl_verify_failures_hostname_mismatch", Name: "hostname_mismatch", Algo: module.Incremental}, + {ID: "ssl_verify_failures_other", Name: "other", Algo: module.Incremental}, + }, + } + sslSessionReusesRateChart = module.Chart{ + ID: "ssl_session_reuses_rate", + Title: "Session reuses during SSL handshake", + Units: "reuses/s", + Fam: "ssl", + Ctx: "nginxplus.ssl_session_reuses_rate", + Priority: prioSSLSessionReusesRate, + Dims: module.Dims{ + {ID: "ssl_session_reuses", Name: "ssl_session", Algo: module.Incremental}, + }, + } + httpRequestsRateChart = module.Chart{ + ID: "http_requests_rate", + Title: "HTTP requests rate", + Units: "requests/s", + Fam: "http requests", + Ctx: "nginxplus.http_requests_rate", + Priority: prioHTTPRequestsRate, + Dims: module.Dims{ + {ID: "http_requests_total", Name: "requests", Algo: module.Incremental}, + }, + } + httpRequestsCountChart = module.Chart{ + ID: "http_requests_count", + Title: "HTTP requests", + Units: "requests", + Fam: "http requests", + Ctx: "nginxplus.http_requests_count", + Priority: prioHTTPRequestsCount, + Dims: module.Dims{ + {ID: "http_requests_current", Name: "requests"}, + }, + } + uptimeChart = module.Chart{ + ID: "uptime", + Title: "Uptime", + Units: "seconds", + Fam: "uptime", + Ctx: "nginxplus.uptime", + Priority: prioUptime, + Dims: module.Dims{ + {ID: "uptime", Name: "uptime"}, + }, + } +) + +var ( + httpServerZoneChartsTmpl = module.Charts{ + httpServerZoneRequestsRateChartTmpl.Copy(), + httpServerZoneResponsesPerCodeClassRateChartTmpl.Copy(), + httpServerZoneTrafficRateChartTmpl.Copy(), + httpServerZoneRequestsProcessingCountChartTmpl.Copy(), + httpServerZoneRequestsDiscardedRateChartTmpl.Copy(), + } + httpServerZoneRequestsRateChartTmpl = module.Chart{ + ID: "http_server_zone_%s_requests_rate", + Title: "HTTP Server Zone requests rate", + Units: "requests/s", + Fam: "http requests", + Ctx: "nginxplus.http_server_zone_requests_rate", + Priority: prioHTTPServerZoneRequestsRate, + Dims: module.Dims{ + {ID: "http_server_zone_%s_requests", Name: "requests", Algo: module.Incremental}, + }, + } + httpServerZoneResponsesPerCodeClassRateChartTmpl = module.Chart{ + ID: "http_server_zone_%s_responses_per_code_class_rate", + Title: "HTTP Server Zone responses rate", + Units: "responses/s", + Fam: "http responses", + Ctx: "nginxplus.http_server_zone_responses_per_code_class_rate", + Priority: prioHTTPServerZoneResponsesPerCodeClassRate, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "http_server_zone_%s_responses_1xx", Name: "1xx", Algo: module.Incremental}, + {ID: "http_server_zone_%s_responses_2xx", Name: "2xx", Algo: module.Incremental}, + {ID: "http_server_zone_%s_responses_3xx", Name: "3xx", Algo: module.Incremental}, + {ID: "http_server_zone_%s_responses_4xx", Name: "4xx", Algo: module.Incremental}, + {ID: "http_server_zone_%s_responses_5xx", Name: "5xx", Algo: module.Incremental}, + }, + } + httpServerZoneTrafficRateChartTmpl = module.Chart{ + ID: "http_server_zone_%s_traffic_rate", + Title: "HTTP Server Zone traffic", + Units: "bytes/s", + Fam: "http traffic", + Ctx: "nginxplus.http_server_zone_traffic_rate", + Priority: prioHTTPServerZoneTrafficRate, + Type: module.Area, + Dims: module.Dims{ + {ID: "http_server_zone_%s_bytes_received", Name: "received", Algo: module.Incremental}, + {ID: "http_server_zone_%s_bytes_sent", Name: "sent", Algo: module.Incremental, Mul: -1}, + }, + } + httpServerZoneRequestsProcessingCountChartTmpl = module.Chart{ + ID: "http_server_zone_%s_requests_processing_count", + Title: "HTTP Server Zone currently processed requests", + Units: "requests", + Fam: "http requests", + Ctx: "nginxplus.http_server_zone_requests_processing_count", + Priority: prioHTTPServerZoneRequestsProcessingCount, + Dims: module.Dims{ + {ID: "http_server_zone_%s_requests_processing", Name: "processing"}, + }, + } + httpServerZoneRequestsDiscardedRateChartTmpl = module.Chart{ + ID: "http_server_zone_%s_requests_discarded_rate", + Title: "HTTP Server Zone requests discarded rate", + Units: "requests/s", + Fam: "http requests", + Ctx: "nginxplus.http_server_zone_requests_discarded_rate", + Priority: prioHTTPServerZoneRequestsDiscardedRate, + Dims: module.Dims{ + {ID: "http_server_zone_%s_requests_discarded", Name: "discarded", Algo: module.Incremental}, + }, + } +) + +var ( + httpLocationZoneChartsTmpl = module.Charts{ + httpLocationZoneRequestsRateChartTmpl.Copy(), + httpLocationZoneRequestsDiscardedRateChartTmpl.Copy(), + httpLocationZoneTrafficRateChartTmpl.Copy(), + httpLocationZoneResponsesPerCodeClassRateChartTmpl.Copy(), + } + httpLocationZoneRequestsRateChartTmpl = module.Chart{ + ID: "http_location_zone_%s_requests_rate", + Title: "HTTP Location Zone requests rate", + Units: "requests/s", + Fam: "http requests", + Ctx: "nginxplus.http_location_zone_requests_rate", + Priority: prioHTTPLocationZoneRequestsRate, + Dims: module.Dims{ + {ID: "http_location_zone_%s_requests", Name: "requests", Algo: module.Incremental}, + }, + } + httpLocationZoneResponsesPerCodeClassRateChartTmpl = module.Chart{ + ID: "http_location_zone_%s_responses_per_code_class_rate", + Title: "HTTP Location Zone responses rate", + Units: "responses/s", + Fam: "http responses", + Ctx: "nginxplus.http_location_zone_responses_per_code_class_rate", + Priority: prioHTTPLocationZoneResponsesPerCodeClassRate, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "http_location_zone_%s_responses_1xx", Name: "1xx", Algo: module.Incremental}, + {ID: "http_location_zone_%s_responses_2xx", Name: "2xx", Algo: module.Incremental}, + {ID: "http_location_zone_%s_responses_3xx", Name: "3xx", Algo: module.Incremental}, + {ID: "http_location_zone_%s_responses_4xx", Name: "4xx", Algo: module.Incremental}, + {ID: "http_location_zone_%s_responses_5xx", Name: "5xx", Algo: module.Incremental}, + }, + } + httpLocationZoneTrafficRateChartTmpl = module.Chart{ + ID: "http_location_zone_%s_traffic_rate", + Title: "HTTP Location Zone traffic rate", + Units: "bytes/s", + Fam: "http traffic", + Ctx: "nginxplus.http_location_zone_traffic_rate", + Priority: prioHTTPLocationZoneTrafficRate, + Type: module.Area, + Dims: module.Dims{ + {ID: "http_location_zone_%s_bytes_received", Name: "received", Algo: module.Incremental}, + {ID: "http_location_zone_%s_bytes_sent", Name: "sent", Algo: module.Incremental, Mul: -1}, + }, + } + httpLocationZoneRequestsDiscardedRateChartTmpl = module.Chart{ + ID: "http_location_zone_%s_requests_discarded_rate", + Title: "HTTP Location Zone requests discarded rate", + Units: "requests/s", + Fam: "http requests", + Ctx: "nginxplus.http_location_zone_requests_discarded_rate", + Priority: prioHTTPLocationZoneRequestsDiscardedRate, + Dims: module.Dims{ + {ID: "http_location_zone_%s_requests_discarded", Name: "discarded", Algo: module.Incremental}, + }, + } +) + +var ( + httpUpstreamChartsTmpl = module.Charts{ + httpUpstreamPeersCountChartTmpl.Copy(), + httpUpstreamZombiesCountChartTmpl.Copy(), + httpUpstreamKeepaliveCountChartTmpl.Copy(), + } + httpUpstreamPeersCountChartTmpl = module.Chart{ + ID: "http_upstream_%s_zone_%s_peers_count", + Title: "HTTP Upstream peers", + Units: "peers", + Fam: "http upstream", + Ctx: "nginxplus.http_upstream_peers_count", + Priority: prioHTTPUpstreamPeersCount, + Dims: module.Dims{ + {ID: "http_upstream_%s_zone_%s_peers", Name: "peers"}, + }, + } + httpUpstreamZombiesCountChartTmpl = module.Chart{ + ID: "http_upstream_%s_zone_%s_zombies_count", + Title: "HTTP Upstream zombies", + Units: "servers", + Fam: "http upstream", + Ctx: "nginxplus.http_upstream_zombies_count", + Priority: prioHTTPUpstreamZombiesCount, + Dims: module.Dims{ + {ID: "http_upstream_%s_zone_%s_zombies", Name: "zombie"}, + }, + } + httpUpstreamKeepaliveCountChartTmpl = module.Chart{ + ID: "http_upstream_%s_zone_%s_keepalive_count", + Title: "HTTP Upstream keepalive", + Units: "connections", + Fam: "http upstream", + Ctx: "nginxplus.http_upstream_keepalive_count", + Priority: prioHTTPUpstreamKeepaliveCount, + Dims: module.Dims{ + {ID: "http_upstream_%s_zone_%s_keepalive", Name: "keepalive"}, + }, + } + + httpUpstreamServerChartsTmpl = module.Charts{ + httpUpstreamServerRequestsRateChartTmpl.Copy(), + httpUpstreamServerResponsesPerCodeClassRateChartTmpl.Copy(), + httpUpstreamServerResponseTimeChartTmpl.Copy(), + httpUpstreamServerResponseHeaderTimeChartTmpl.Copy(), + httpUpstreamServerTrafficRateChartTmpl.Copy(), + httpUpstreamServerStateChartTmpl.Copy(), + httpUpstreamServerDowntimeChartTmpl.Copy(), + httpUpstreamServerConnectionsCountChartTmpl.Copy(), + } + httpUpstreamServerRequestsRateChartTmpl = module.Chart{ + ID: "http_upstream_%s_server_%s_zone_%s_requests_rate", + Title: "HTTP Upstream Server requests", + Units: "requests/s", + Fam: "http upstream requests", + Ctx: "nginxplus.http_upstream_server_requests_rate", + Priority: prioHTTPUpstreamServerRequestsRate, + Dims: module.Dims{ + {ID: "http_upstream_%s_server_%s_zone_%s_requests", Name: "requests", Algo: module.Incremental}, + }, + } + httpUpstreamServerResponsesPerCodeClassRateChartTmpl = module.Chart{ + ID: "http_upstream_%s_server_%s_zone_%s_responses_per_code_class_rate", + Title: "HTTP Upstream Server responses", + Units: "responses/s", + Fam: "http upstream responses", + Ctx: "nginxplus.http_upstream_server_responses_per_code_class_rate", + Priority: prioHTTPUpstreamServerResponsesPerCodeClassRate, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "http_upstream_%s_server_%s_zone_%s_responses_1xx", Name: "1xx", Algo: module.Incremental}, + {ID: "http_upstream_%s_server_%s_zone_%s_responses_2xx", Name: "2xx", Algo: module.Incremental}, + {ID: "http_upstream_%s_server_%s_zone_%s_responses_3xx", Name: "3xx", Algo: module.Incremental}, + {ID: "http_upstream_%s_server_%s_zone_%s_responses_4xx", Name: "4xx", Algo: module.Incremental}, + {ID: "http_upstream_%s_server_%s_zone_%s_responses_5xx", Name: "5xx", Algo: module.Incremental}, + }, + } + httpUpstreamServerResponseTimeChartTmpl = module.Chart{ + ID: "http_upstream_%s_server_%s_zone_%s_response_time", + Title: "HTTP Upstream Server average response time", + Units: "milliseconds", + Fam: "http upstream response time", + Ctx: "nginxplus.http_upstream_server_response_time", + Priority: prioHTTPUpstreamServerResponseTime, + Dims: module.Dims{ + {ID: "http_upstream_%s_server_%s_zone_%s_response_time", Name: "response"}, + }, + } + httpUpstreamServerResponseHeaderTimeChartTmpl = module.Chart{ + ID: "http_upstream_%s_server_%s_zone_%s_response_header_time", + Title: "HTTP Upstream Server average response header time", + Units: "milliseconds", + Fam: "http upstream response time", + Ctx: "nginxplus.http_upstream_server_response_header_time", + Priority: prioHTTPUpstreamServerResponseHeaderTime, + Dims: module.Dims{ + {ID: "http_upstream_%s_server_%s_zone_%s_header_time", Name: "header"}, + }, + } + httpUpstreamServerTrafficRateChartTmpl = module.Chart{ + ID: "http_upstream_%s_server_%s_zone_%s_traffic_rate", + Title: "HTTP Upstream Server traffic rate", + Units: "bytes/s", + Fam: "http upstream traffic", + Ctx: "nginxplus.http_upstream_server_traffic_rate", + Priority: prioHTTPUpstreamServerTrafficRate, + Type: module.Area, + Dims: module.Dims{ + {ID: "http_upstream_%s_server_%s_zone_%s_bytes_received", Name: "received", Algo: module.Incremental}, + {ID: "http_upstream_%s_server_%s_zone_%s_bytes_sent", Name: "sent", Algo: module.Incremental, Mul: -1}, + }, + } + httpUpstreamServerStateChartTmpl = module.Chart{ + ID: "http_upstream_%s_server_%s_zone_%s_state", + Title: "HTTP Upstream Server state", + Units: "state", + Fam: "http upstream state", + Ctx: "nginxplus.http_upstream_server_state", + Priority: prioHTTPUpstreamServerState, + Dims: module.Dims{ + {ID: "http_upstream_%s_server_%s_zone_%s_state_up", Name: "up"}, + {ID: "http_upstream_%s_server_%s_zone_%s_state_down", Name: "down"}, + {ID: "http_upstream_%s_server_%s_zone_%s_state_draining", Name: "draining"}, + {ID: "http_upstream_%s_server_%s_zone_%s_state_unavail", Name: "unavail"}, + {ID: "http_upstream_%s_server_%s_zone_%s_state_checking", Name: "checking"}, + {ID: "http_upstream_%s_server_%s_zone_%s_state_unhealthy", Name: "unhealthy"}, + }, + } + httpUpstreamServerConnectionsCountChartTmpl = module.Chart{ + ID: "http_upstream_%s_server_%s_zone_%s_connection_count", + Title: "HTTP Upstream Server connections", + Units: "connections", + Fam: "http upstream connections", + Ctx: "nginxplus.http_upstream_server_connections_count", + Priority: prioHTTPUpstreamServerConnectionsCount, + Dims: module.Dims{ + {ID: "http_upstream_%s_server_%s_zone_%s_active", Name: "active"}, + }, + } + httpUpstreamServerDowntimeChartTmpl = module.Chart{ + ID: "http_upstream_%s_server_%s_zone_%s_downtime", + Title: "HTTP Upstream Server downtime", + Units: "seconds", + Fam: "http upstream state", + Ctx: "nginxplus.http_upstream_server_downtime", + Priority: prioHTTPUpstreamServerDowntime, + Dims: module.Dims{ + {ID: "http_upstream_%s_server_%s_zone_%s_downtime", Name: "downtime"}, + }, + } +) + +var ( + httpCacheChartsTmpl = module.Charts{ + httpCacheStateChartTmpl.Copy(), + httpCacheIOPSChartTmpl.Copy(), + httpCacheIOChartTmpl.Copy(), + httpCacheSizeChartTmpl.Copy(), + } + httpCacheStateChartTmpl = module.Chart{ + ID: "http_cache_%s_state", + Title: "HTTP Cache state", + Units: "state", + Fam: "http cache", + Ctx: "nginxplus.http_cache_state", + Priority: prioHTTPCacheState, + Dims: module.Dims{ + {ID: "http_cache_%s_state_warm", Name: "warm"}, + {ID: "http_cache_%s_state_cold", Name: "cold"}, + }, + } + httpCacheSizeChartTmpl = module.Chart{ + ID: "http_cache_%s_size", + Title: "HTTP Cache size", + Units: "bytes", + Fam: "http cache", + Ctx: "nginxplus.http_cache_size", + Priority: prioHTTPCacheSize, + Dims: module.Dims{ + {ID: "http_cache_%s_size", Name: "size"}, + }, + } + httpCacheIOPSChartTmpl = module.Chart{ + ID: "http_cache_%s_iops", + Title: "HTTP Cache IOPS", + Units: "responses/s", + Fam: "http cache", + Ctx: "nginxplus.http_cache_iops", + Priority: prioHTTPCacheIOPS, + Dims: module.Dims{ + {ID: "http_cache_%s_served_responses", Name: "served", Algo: module.Incremental}, + {ID: "http_cache_%s_written_responses", Name: "written", Algo: module.Incremental}, + {ID: "http_cache_%s_bypassed_responses", Name: "bypassed", Algo: module.Incremental}, + }, + } + httpCacheIOChartTmpl = module.Chart{ + ID: "http_cache_%s_io", + Title: "HTTP Cache IO", + Units: "bytes/s", + Fam: "http cache", + Ctx: "nginxplus.http_cache_io", + Priority: prioHTTPCacheIO, + Dims: module.Dims{ + {ID: "http_cache_%s_served_bytes", Name: "served", Algo: module.Incremental}, + {ID: "http_cache_%s_written_bytes", Name: "written", Algo: module.Incremental}, + {ID: "http_cache_%s_bypassed_bytes", Name: "bypassed", Algo: module.Incremental}, + }, + } +) + +var ( + streamServerZoneChartsTmpl = module.Charts{ + streamServerZoneConnectionsRateChartTmpl.Copy(), + streamServerZoneTrafficRateChartTmpl.Copy(), + streamServerZoneSessionsPerCodeClassRateChartTmpl.Copy(), + streamServerZoneConnectionsProcessingCountRateChartTmpl.Copy(), + streamServerZoneConnectionsDiscardedRateChartTmpl.Copy(), + } + streamServerZoneConnectionsRateChartTmpl = module.Chart{ + ID: "stream_server_zone_%s_connections_rate", + Title: "Stream Server Zone connections rate", + Units: "connections/s", + Fam: "stream connections", + Ctx: "nginxplus.stream_server_zone_connections_rate", + Priority: prioStreamServerZoneConnectionsRate, + Dims: module.Dims{ + {ID: "stream_server_zone_%s_connections", Name: "accepted", Algo: module.Incremental}, + }, + } + streamServerZoneSessionsPerCodeClassRateChartTmpl = module.Chart{ + ID: "stream_server_zone_%s_sessions_per_code_class_rate", + Title: "Stream Server Zone sessions rate", + Units: "sessions/s", + Fam: "stream sessions", + Ctx: "nginxplus.stream_server_zone_sessions_per_code_class_rate", + Priority: prioStreamServerZoneSessionsPerCodeClassRate, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "stream_server_zone_%s_sessions_2xx", Name: "2xx", Algo: module.Incremental}, + {ID: "stream_server_zone_%s_sessions_4xx", Name: "4xx", Algo: module.Incremental}, + {ID: "stream_server_zone_%s_sessions_5xx", Name: "5xx", Algo: module.Incremental}, + }, + } + streamServerZoneTrafficRateChartTmpl = module.Chart{ + ID: "stream_server_zone_%s_traffic_rate", + Title: "Stream Server Zone traffic rate", + Units: "bytes/s", + Fam: "stream traffic", + Ctx: "nginxplus.stream_server_zone_traffic_rate", + Priority: prioStreamServerZoneTrafficRate, + Type: module.Area, + Dims: module.Dims{ + {ID: "stream_server_zone_%s_bytes_received", Name: "received", Algo: module.Incremental}, + {ID: "stream_server_zone_%s_bytes_sent", Name: "sent", Algo: module.Incremental, Mul: -1}, + }, + } + streamServerZoneConnectionsProcessingCountRateChartTmpl = module.Chart{ + ID: "stream_server_zone_%s_connections_processing_count", + Title: "Stream Server Zone connections processed", + Units: "connections", + Fam: "stream connections", + Ctx: "nginxplus.stream_server_zone_connections_processing_count", + Priority: prioStreamServerZoneConnectionsProcessingCount, + Dims: module.Dims{ + {ID: "stream_server_zone_%s_connections_processing", Name: "processing"}, + }, + } + streamServerZoneConnectionsDiscardedRateChartTmpl = module.Chart{ + ID: "stream_server_zone_%s_connections_discarded_rate", + Title: "Stream Server Zone connections discarded", + Units: "connections/s", + Fam: "stream connections", + Ctx: "nginxplus.stream_server_zone_connections_discarded_rate", + Priority: prioStreamServerZoneConnectionsDiscardedRate, + Dims: module.Dims{ + {ID: "stream_server_zone_%s_connections_discarded", Name: "discarded", Algo: module.Incremental}, + }, + } +) + +var ( + streamUpstreamChartsTmpl = module.Charts{ + streamUpstreamPeersCountChartTmpl.Copy(), + streamUpstreamZombiesCountChartTmpl.Copy(), + } + streamUpstreamPeersCountChartTmpl = module.Chart{ + ID: "stream_upstream_%s_zone_%s_peers_count", + Title: "Stream Upstream peers", + Units: "peers", + Fam: "stream upstream", + Ctx: "nginxplus.stream_upstream_peers_count", + Priority: prioStreamUpstreamPeersCount, + Dims: module.Dims{ + {ID: "stream_upstream_%s_zone_%s_peers", Name: "peers"}, + }, + } + streamUpstreamZombiesCountChartTmpl = module.Chart{ + ID: "stream_upstream_%s_zone_%s_zombies_count", + Title: "Stream Upstream zombies", + Units: "servers", + Fam: "stream upstream", + Ctx: "nginxplus.stream_upstream_zombies_count", + Priority: prioStreamUpstreamZombiesCount, + Dims: module.Dims{ + {ID: "stream_upstream_%s_zone_%s_zombies", Name: "zombie"}, + }, + } + + streamUpstreamServerChartsTmpl = module.Charts{ + streamUpstreamServerConnectionsRateChartTmpl.Copy(), + streamUpstreamServerTrafficRateChartTmpl.Copy(), + streamUpstreamServerConnectionsCountChartTmpl.Copy(), + streamUpstreamServerStateChartTmpl.Copy(), + streamUpstreamServerDowntimeChartTmpl.Copy(), + } + streamUpstreamServerConnectionsRateChartTmpl = module.Chart{ + ID: "stream_upstream_%s_server_%s_zone_%s_connection_rate", + Title: "Stream Upstream Server connections", + Units: "connections/s", + Fam: "stream upstream connections", + Ctx: "nginxplus.stream_upstream_server_connections_rate", + Priority: prioStreamUpstreamServerConnectionsRate, + Dims: module.Dims{ + {ID: "stream_upstream_%s_server_%s_zone_%s_connections", Name: "forwarded", Algo: module.Incremental}, + }, + } + streamUpstreamServerTrafficRateChartTmpl = module.Chart{ + ID: "stream_upstream_%s_server_%s_zone_%s_traffic_rate", + Title: "Stream Upstream Server traffic rate", + Units: "bytes/s", + Fam: "stream upstream traffic", + Ctx: "nginxplus.stream_upstream_server_traffic_rate", + Priority: prioStreamUpstreamServerTrafficRate, + Type: module.Area, + Dims: module.Dims{ + {ID: "stream_upstream_%s_server_%s_zone_%s_bytes_received", Name: "received", Algo: module.Incremental}, + {ID: "stream_upstream_%s_server_%s_zone_%s_bytes_sent", Name: "sent", Algo: module.Incremental, Mul: -1}, + }, + } + streamUpstreamServerStateChartTmpl = module.Chart{ + ID: "stream_upstream_%s_server_%s_zone_%s_state", + Title: "Stream Upstream Server state", + Units: "state", + Fam: "stream upstream state", + Ctx: "nginxplus.stream_upstream_server_state", + Priority: prioStreamUpstreamServerState, + Dims: module.Dims{ + {ID: "stream_upstream_%s_server_%s_zone_%s_state_up", Name: "up"}, + {ID: "stream_upstream_%s_server_%s_zone_%s_state_down", Name: "down"}, + {ID: "stream_upstream_%s_server_%s_zone_%s_state_unavail", Name: "unavail"}, + {ID: "stream_upstream_%s_server_%s_zone_%s_state_checking", Name: "checking"}, + {ID: "stream_upstream_%s_server_%s_zone_%s_state_unhealthy", Name: "unhealthy"}, + }, + } + streamUpstreamServerDowntimeChartTmpl = module.Chart{ + ID: "stream_upstream_%s_server_%s_zone_%s_downtime", + Title: "Stream Upstream Server downtime", + Units: "seconds", + Fam: "stream upstream state", + Ctx: "nginxplus.stream_upstream_server_downtime", + Priority: prioStreamUpstreamServerDowntime, + Dims: module.Dims{ + {ID: "stream_upstream_%s_server_%s_zone_%s_downtime", Name: "downtime"}, + }, + } + streamUpstreamServerConnectionsCountChartTmpl = module.Chart{ + ID: "stream_upstream_%s_server_%s_zone_%s_connection_count", + Title: "Stream Upstream Server connections", + Units: "connections", + Fam: "stream upstream connections", + Ctx: "nginxplus.stream_upstream_server_connections_count", + Priority: prioStreamUpstreamServerConnectionsCount, + Dims: module.Dims{ + {ID: "stream_upstream_%s_server_%s_zone_%s_active", Name: "active"}, + }, + } +) + +var ( + resolverZoneChartsTmpl = module.Charts{ + resolverZoneRequestsRateChartTmpl.Copy(), + resolverZoneResponsesRateChartTmpl.Copy(), + } + resolverZoneRequestsRateChartTmpl = module.Chart{ + ID: "resolver_zone_%s_requests_rate", + Title: "Resolver requests rate", + Units: "requests/s", + Fam: "resolver requests", + Ctx: "nginxplus.resolver_zone_requests_rate", + Priority: prioResolverZoneRequestsRate, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "resolver_zone_%s_requests_name", Name: "name", Algo: module.Incremental}, + {ID: "resolver_zone_%s_requests_srv", Name: "srv", Algo: module.Incremental}, + {ID: "resolver_zone_%s_requests_addr", Name: "addr", Algo: module.Incremental}, + }, + } + resolverZoneResponsesRateChartTmpl = module.Chart{ + ID: "resolver_zone_%s_responses_rate", + Title: "Resolver responses rate", + Units: "responses/s", + Fam: "resolver responses", + Ctx: "nginxplus.resolver_zone_responses_rate", + Priority: prioResolverZoneResponsesRate, + Type: module.Stacked, + Dims: module.Dims{ + {ID: "resolver_zone_%s_responses_noerror", Name: "noerror", Algo: module.Incremental}, + {ID: "resolver_zone_%s_responses_formerr", Name: "formerr", Algo: module.Incremental}, + {ID: "resolver_zone_%s_responses_servfail", Name: "servfail", Algo: module.Incremental}, + {ID: "resolver_zone_%s_responses_nxdomain", Name: "nxdomain", Algo: module.Incremental}, + {ID: "resolver_zone_%s_responses_notimp", Name: "notimp", Algo: module.Incremental}, + {ID: "resolver_zone_%s_responses_refused", Name: "refused", Algo: module.Incremental}, + {ID: "resolver_zone_%s_responses_timedout", Name: "timedout", Algo: module.Incremental}, + {ID: "resolver_zone_%s_responses_unknown", Name: "unknown", Algo: module.Incremental}, + }, + } +) + +func (n *NginxPlus) addHTTPCacheCharts(name string) { + charts := httpCacheChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, name) + chart.Labels = []module.Label{ + {Key: "http_cache", Value: name}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, name) + } + } + + if err := n.Charts().Add(*charts...); err != nil { + n.Warning(err) + } +} + +func (n *NginxPlus) removeHTTPCacheCharts(name string) { + px := fmt.Sprintf("http_cache_%s_", name) + n.removeCharts(px) +} + +func (n *NginxPlus) addHTTPServerZoneCharts(zone string) { + charts := httpServerZoneChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, zone) + chart.Labels = []module.Label{ + {Key: "http_server_zone", Value: zone}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, zone) + } + } + + if err := n.Charts().Add(*charts...); err != nil { + n.Warning(err) + } +} + +func (n *NginxPlus) removeHTTPServerZoneCharts(zone string) { + px := fmt.Sprintf("http_server_zone_%s_", zone) + n.removeCharts(px) +} + +func (n *NginxPlus) addHTTPLocationZoneCharts(zone string) { + charts := httpLocationZoneChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, zone) + chart.Labels = []module.Label{ + {Key: "http_location_zone", Value: zone}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, zone) + } + } + + if err := n.Charts().Add(*charts...); err != nil { + n.Warning(err) + } +} + +func (n *NginxPlus) removeHTTPLocationZoneCharts(zone string) { + px := fmt.Sprintf("http_location_zone_%s_", zone) + n.removeCharts(px) +} + +func (n *NginxPlus) addHTTPUpstreamCharts(name, zone string) { + charts := httpUpstreamChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, name, zone) + chart.Labels = []module.Label{ + {Key: "http_upstream_name", Value: name}, + {Key: "http_upstream_zone", Value: zone}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, name, zone) + } + } + + if err := n.Charts().Add(*charts...); err != nil { + n.Warning(err) + } +} + +func (n *NginxPlus) removeHTTPUpstreamCharts(name, zone string) { + px := fmt.Sprintf("http_upstream_%s_zone_%s", name, zone) + n.removeCharts(px) +} + +func (n *NginxPlus) addHTTPUpstreamServerCharts(name, serverAddr, serverName, zone string) { + charts := httpUpstreamServerChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, name, serverAddr, zone) + chart.Labels = []module.Label{ + {Key: "http_upstream_name", Value: name}, + {Key: "http_upstream_zone", Value: zone}, + {Key: "http_upstream_server_address", Value: serverAddr}, + {Key: "http_upstream_server_name", Value: serverName}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, name, serverAddr, zone) + } + } + + if err := n.Charts().Add(*charts...); err != nil { + n.Warning(err) + } +} + +func (n *NginxPlus) removeHTTPUpstreamServerCharts(name, serverAddr, zone string) { + px := fmt.Sprintf("http_upstream_%s_server_%s_zone_%s_", name, zone, serverAddr) + n.removeCharts(px) +} + +func (n *NginxPlus) addStreamServerZoneCharts(zone string) { + charts := streamServerZoneChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, zone) + chart.Labels = []module.Label{ + {Key: "stream_server_zone", Value: zone}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, zone) + } + } + + if err := n.Charts().Add(*charts...); err != nil { + n.Warning(err) + } +} + +func (n *NginxPlus) removeStreamServerZoneCharts(zone string) { + px := fmt.Sprintf("stream_server_zone_%s_", zone) + n.removeCharts(px) +} + +func (n *NginxPlus) addStreamUpstreamCharts(zone, name string) { + charts := streamUpstreamChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, zone, name) + chart.Labels = []module.Label{ + {Key: "stream_upstream_zone", Value: name}, + {Key: "stream_upstream_zone", Value: zone}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, zone, name) + } + } + + if err := n.Charts().Add(*charts...); err != nil { + n.Warning(err) + } +} + +func (n *NginxPlus) removeStreamUpstreamCharts(name, zone string) { + px := fmt.Sprintf("stream_upstream_%s_zone_%s_", name, zone) + n.removeCharts(px) +} + +func (n *NginxPlus) addStreamUpstreamServerCharts(name, serverAddr, serverName, zone string) { + charts := streamUpstreamServerChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, name, serverAddr, zone) + chart.Labels = []module.Label{ + {Key: "stream_upstream_name", Value: name}, + {Key: "stream_upstream_zone", Value: zone}, + {Key: "stream_upstream_server_address", Value: serverAddr}, + {Key: "stream_upstream_server_name", Value: serverName}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, name, serverAddr, zone) + } + } + + if err := n.Charts().Add(*charts...); err != nil { + n.Warning(err) + } +} + +func (n *NginxPlus) removeStreamUpstreamServerCharts(name, serverAddr, zone string) { + px := fmt.Sprintf("stream_upstream_%s_server_%s_zone_%s", name, serverAddr, zone) + n.removeCharts(px) +} + +func (n *NginxPlus) addResolverZoneCharts(zone string) { + charts := resolverZoneChartsTmpl.Copy() + + for _, chart := range *charts { + chart.ID = fmt.Sprintf(chart.ID, zone) + chart.Labels = []module.Label{ + {Key: "resolver_zone", Value: zone}, + } + for _, dim := range chart.Dims { + dim.ID = fmt.Sprintf(dim.ID, zone) + } + } + + if err := n.Charts().Add(*charts...); err != nil { + n.Warning(err) + } +} + +func (n *NginxPlus) removeResolverZoneCharts(zone string) { + px := fmt.Sprintf("resolver_zone_%s_", zone) + n.removeCharts(px) +} + +func (n *NginxPlus) removeCharts(prefix string) { + for _, chart := range *n.Charts() { + if strings.HasPrefix(chart.ID, prefix) { + chart.MarkRemove() + chart.MarkNotCreated() + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/collect.go b/src/go/collectors/go.d.plugin/modules/nginxplus/collect.go new file mode 100644 index 000000000..f986778ba --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/collect.go @@ -0,0 +1,393 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxplus + +import ( + "errors" + "fmt" + "time" +) + +func (n *NginxPlus) collect() (map[string]int64, error) { + if n.apiVersion == 0 { + v, err := n.queryAPIVersion() + if err != nil { + return nil, err + } + n.apiVersion = v + } + + now := time.Now() + if now.Sub(n.queryEndpointsTime) > n.queryEndpointsEvery { + n.queryEndpointsTime = now + if err := n.queryAvailableEndpoints(); err != nil { + return nil, err + } + } + + ms := n.queryMetrics() + if ms.empty() { + return nil, errors.New("no metrics collected") + } + + mx := make(map[string]int64) + n.cache.resetUpdated() + n.collectInfo(mx, ms) + n.collectConnections(mx, ms) + n.collectSSL(mx, ms) + n.collectHTTPRequests(mx, ms) + n.collectHTTPCache(mx, ms) + n.collectHTTPServerZones(mx, ms) + n.collectHTTPLocationZones(mx, ms) + n.collectHTTPUpstreams(mx, ms) + n.collectStreamServerZones(mx, ms) + n.collectStreamUpstreams(mx, ms) + n.collectResolvers(mx, ms) + n.updateCharts() + + return mx, nil +} + +func (n *NginxPlus) collectInfo(mx map[string]int64, ms *nginxMetrics) { + if ms.info == nil { + return + } + mx["uptime"] = int64(ms.info.Timestamp.Sub(ms.info.LoadTimestamp).Seconds()) +} + +func (n *NginxPlus) collectConnections(mx map[string]int64, ms *nginxMetrics) { + if ms.connections == nil { + return + } + mx["connections_accepted"] = ms.connections.Accepted + mx["connections_dropped"] = ms.connections.Dropped + mx["connections_active"] = ms.connections.Active + mx["connections_idle"] = ms.connections.Idle +} + +func (n *NginxPlus) collectSSL(mx map[string]int64, ms *nginxMetrics) { + if ms.ssl == nil { + return + } + mx["ssl_handshakes"] = ms.ssl.Handshakes + mx["ssl_handshakes_failed"] = ms.ssl.HandshakesFailed + mx["ssl_session_reuses"] = ms.ssl.SessionReuses + mx["ssl_no_common_protocol"] = ms.ssl.NoCommonProtocol + mx["ssl_no_common_cipher"] = ms.ssl.NoCommonCipher + mx["ssl_handshake_timeout"] = ms.ssl.HandshakeTimeout + mx["ssl_peer_rejected_cert"] = ms.ssl.PeerRejectedCert + mx["ssl_verify_failures_no_cert"] = ms.ssl.VerifyFailures.NoCert + mx["ssl_verify_failures_expired_cert"] = ms.ssl.VerifyFailures.ExpiredCert + mx["ssl_verify_failures_revoked_cert"] = ms.ssl.VerifyFailures.RevokedCert + mx["ssl_verify_failures_hostname_mismatch"] = ms.ssl.VerifyFailures.HostnameMismatch + mx["ssl_verify_failures_other"] = ms.ssl.VerifyFailures.Other +} + +func (n *NginxPlus) collectHTTPRequests(mx map[string]int64, ms *nginxMetrics) { + if ms.httpRequests == nil { + return + } + mx["http_requests_total"] = ms.httpRequests.Total + mx["http_requests_current"] = ms.httpRequests.Current +} + +func (n *NginxPlus) collectHTTPCache(mx map[string]int64, ms *nginxMetrics) { + if ms.httpCaches == nil { + return + } + for name, cache := range *ms.httpCaches { + n.cache.putHTTPCache(name) + px := fmt.Sprintf("http_cache_%s_", name) + mx[px+"state_cold"] = boolToInt(cache.Cold) + mx[px+"state_warm"] = boolToInt(!cache.Cold) + mx[px+"size"] = cache.Size + mx[px+"served_responses"] = cache.Hit.Responses + cache.Stale.Responses + cache.Updating.Responses + cache.Revalidated.Responses + mx[px+"written_responses"] = cache.Miss.ResponsesWritten + cache.Expired.ResponsesWritten + cache.Bypass.ResponsesWritten + mx[px+"bypassed_responses"] = cache.Miss.Responses + cache.Expired.Responses + cache.Bypass.Responses + mx[px+"served_bytes"] = cache.Hit.Bytes + cache.Stale.Bytes + cache.Updating.Bytes + cache.Revalidated.Bytes + mx[px+"written_bytes"] = cache.Miss.BytesWritten + cache.Expired.BytesWritten + cache.Bypass.BytesWritten + mx[px+"bypassed_bytes"] = cache.Miss.Bytes + cache.Expired.Bytes + cache.Bypass.Bytes + } +} + +func (n *NginxPlus) collectHTTPServerZones(mx map[string]int64, ms *nginxMetrics) { + if ms.httpServerZones == nil { + return + } + for name, zone := range *ms.httpServerZones { + n.cache.putHTTPServerZone(name) + + px := fmt.Sprintf("http_server_zone_%s_", name) + mx[px+"requests_processing"] = zone.Processing + mx[px+"requests"] = zone.Requests + mx[px+"requests_discarded"] = zone.Discarded + mx[px+"bytes_received"] = zone.Received + mx[px+"bytes_sent"] = zone.Sent + mx[px+"responses"] = zone.Responses.Total + mx[px+"responses_1xx"] = zone.Responses.Class1xx + mx[px+"responses_2xx"] = zone.Responses.Class2xx + mx[px+"responses_3xx"] = zone.Responses.Class3xx + mx[px+"responses_4xx"] = zone.Responses.Class4xx + mx[px+"responses_5xx"] = zone.Responses.Class5xx + } +} + +func (n *NginxPlus) collectHTTPLocationZones(mx map[string]int64, ms *nginxMetrics) { + if ms.httpLocationZones == nil { + return + } + for name, zone := range *ms.httpLocationZones { + n.cache.putHTTPLocationZone(name) + + px := fmt.Sprintf("http_location_zone_%s_", name) + mx[px+"requests"] = zone.Requests + mx[px+"requests_discarded"] = zone.Discarded + mx[px+"bytes_received"] = zone.Received + mx[px+"bytes_sent"] = zone.Sent + mx[px+"responses"] = zone.Responses.Total + mx[px+"responses_1xx"] = zone.Responses.Class1xx + mx[px+"responses_2xx"] = zone.Responses.Class2xx + mx[px+"responses_3xx"] = zone.Responses.Class3xx + mx[px+"responses_4xx"] = zone.Responses.Class4xx + mx[px+"responses_5xx"] = zone.Responses.Class5xx + } +} + +func (n *NginxPlus) collectHTTPUpstreams(mx map[string]int64, ms *nginxMetrics) { + if ms.httpUpstreams == nil { + return + } + for name, upstream := range *ms.httpUpstreams { + n.cache.putHTTPUpstream(name, upstream.Zone) + + px := fmt.Sprintf("http_upstream_%s_zone_%s_", name, upstream.Zone) + mx[px+"zombies"] = upstream.Zombies + mx[px+"keepalive"] = upstream.Keepalive + mx[px+"peers"] = int64(len(upstream.Peers)) + + for _, peer := range upstream.Peers { + n.cache.putHTTPUpstreamServer(name, peer.Server, peer.Name, upstream.Zone) + + px = fmt.Sprintf("http_upstream_%s_server_%s_zone_%s_", name, peer.Server, upstream.Zone) + mx[px+"active"] = peer.Active + mx[px+"state_up"] = boolToInt(peer.State == "up") + mx[px+"state_down"] = boolToInt(peer.State == "down") + mx[px+"state_draining"] = boolToInt(peer.State == "draining") + mx[px+"state_unavail"] = boolToInt(peer.State == "unavail") + mx[px+"state_checking"] = boolToInt(peer.State == "checking") + mx[px+"state_unhealthy"] = boolToInt(peer.State == "unhealthy") + mx[px+"bytes_received"] = peer.Received + mx[px+"bytes_sent"] = peer.Sent + mx[px+"requests"] = peer.Requests + mx[px+"responses"] = peer.Responses.Total + mx[px+"responses_1xx"] = peer.Responses.Class1xx + mx[px+"responses_2xx"] = peer.Responses.Class2xx + mx[px+"responses_3xx"] = peer.Responses.Class3xx + mx[px+"responses_4xx"] = peer.Responses.Class4xx + mx[px+"responses_5xx"] = peer.Responses.Class5xx + mx[px+"response_time"] = peer.ResponseTime + mx[px+"header_time"] = peer.HeaderTime + mx[px+"downtime"] = peer.Downtime / 1000 + } + } +} + +func (n *NginxPlus) collectStreamServerZones(mx map[string]int64, ms *nginxMetrics) { + if ms.streamServerZones == nil { + return + } + for name, zone := range *ms.streamServerZones { + n.cache.putStreamServerZone(name) + + px := fmt.Sprintf("stream_server_zone_%s_", name) + mx[px+"connections"] = zone.Connections + mx[px+"connections_processing"] = zone.Processing + mx[px+"connections_discarded"] = zone.Discarded + mx[px+"bytes_received"] = zone.Received + mx[px+"bytes_sent"] = zone.Sent + mx[px+"sessions"] = zone.Sessions.Total + mx[px+"sessions_2xx"] = zone.Sessions.Class2xx + mx[px+"sessions_4xx"] = zone.Sessions.Class4xx + mx[px+"sessions_5xx"] = zone.Sessions.Class5xx + } +} + +func (n *NginxPlus) collectStreamUpstreams(mx map[string]int64, ms *nginxMetrics) { + if ms.streamUpstreams == nil { + return + } + for name, upstream := range *ms.streamUpstreams { + n.cache.putStreamUpstream(name, upstream.Zone) + + px := fmt.Sprintf("stream_upstream_%s_zone_%s_", name, upstream.Zone) + mx[px+"zombies"] = upstream.Zombies + mx[px+"peers"] = int64(len(upstream.Peers)) + + for _, peer := range upstream.Peers { + n.cache.putStreamUpstreamServer(name, peer.Server, peer.Name, upstream.Zone) + + px = fmt.Sprintf("stream_upstream_%s_server_%s_zone_%s_", name, peer.Server, upstream.Zone) + mx[px+"active"] = peer.Active + mx[px+"connections"] = peer.Connections + mx[px+"state_up"] = boolToInt(peer.State == "up") + mx[px+"state_down"] = boolToInt(peer.State == "down") + mx[px+"state_unavail"] = boolToInt(peer.State == "unavail") + mx[px+"state_checking"] = boolToInt(peer.State == "checking") + mx[px+"state_unhealthy"] = boolToInt(peer.State == "unhealthy") + mx[px+"bytes_received"] = peer.Received + mx[px+"bytes_sent"] = peer.Sent + mx[px+"downtime"] = peer.Downtime / 1000 + } + } +} + +func (n *NginxPlus) collectResolvers(mx map[string]int64, ms *nginxMetrics) { + if ms.resolvers == nil { + return + } + for name, zone := range *ms.resolvers { + n.cache.putResolver(name) + + px := fmt.Sprintf("resolver_zone_%s_", name) + mx[px+"requests_name"] = zone.Requests.Name + mx[px+"requests_srv"] = zone.Requests.Srv + mx[px+"requests_addr"] = zone.Requests.Addr + mx[px+"responses_noerror"] = zone.Responses.NoError + mx[px+"responses_formerr"] = zone.Responses.Formerr + mx[px+"responses_servfail"] = zone.Responses.Servfail + mx[px+"responses_nxdomain"] = zone.Responses.Nxdomain + mx[px+"responses_notimp"] = zone.Responses.Notimp + mx[px+"responses_refused"] = zone.Responses.Refused + mx[px+"responses_timedout"] = zone.Responses.TimedOut + mx[px+"responses_unknown"] = zone.Responses.Unknown + } +} + +func (n *NginxPlus) updateCharts() { + const notSeenLimit = 3 + + for key, v := range n.cache.httpCaches { + if v.updated && !v.hasCharts { + v.hasCharts = true + n.addHTTPCacheCharts(v.name) + continue + } + if !v.updated { + if v.notSeenTimes++; v.notSeenTimes >= notSeenLimit { + delete(n.cache.httpCaches, key) + n.removeHTTPCacheCharts(v.name) + } + } + } + for key, v := range n.cache.httpServerZones { + if v.updated && !v.hasCharts { + v.hasCharts = true + n.addHTTPServerZoneCharts(v.zone) + continue + } + if !v.updated { + if v.notSeenTimes++; v.notSeenTimes >= notSeenLimit { + delete(n.cache.httpServerZones, key) + n.removeHTTPServerZoneCharts(v.zone) + } + } + } + for key, v := range n.cache.httpLocationZones { + if v.updated && !v.hasCharts { + v.hasCharts = true + n.addHTTPLocationZoneCharts(v.zone) + continue + } + if !v.updated { + if v.notSeenTimes++; v.notSeenTimes >= notSeenLimit { + delete(n.cache.httpLocationZones, key) + n.removeHTTPLocationZoneCharts(v.zone) + } + } + } + for key, v := range n.cache.httpUpstreams { + if v.updated && !v.hasCharts { + v.hasCharts = true + n.addHTTPUpstreamCharts(v.name, v.zone) + continue + } + if !v.updated { + if v.notSeenTimes++; v.notSeenTimes >= notSeenLimit { + delete(n.cache.httpUpstreams, key) + n.removeHTTPUpstreamCharts(v.name, v.zone) + } + } + } + for key, v := range n.cache.httpUpstreamServers { + if v.updated && !v.hasCharts { + v.hasCharts = true + n.addHTTPUpstreamServerCharts(v.name, v.serverAddr, v.serverName, v.zone) + continue + } + if !v.updated { + if v.notSeenTimes++; v.notSeenTimes >= notSeenLimit { + delete(n.cache.httpUpstreamServers, key) + n.removeHTTPUpstreamServerCharts(v.name, v.serverAddr, v.zone) + } + } + } + for key, v := range n.cache.streamServerZones { + if v.updated && !v.hasCharts { + v.hasCharts = true + n.addStreamServerZoneCharts(v.zone) + continue + } + if !v.updated { + if v.notSeenTimes++; v.notSeenTimes >= notSeenLimit { + delete(n.cache.streamServerZones, key) + n.removeStreamServerZoneCharts(v.zone) + } + } + } + for key, v := range n.cache.streamUpstreams { + if v.updated && !v.hasCharts { + v.hasCharts = true + n.addStreamUpstreamCharts(v.name, v.zone) + continue + } + if !v.updated { + if v.notSeenTimes++; v.notSeenTimes >= notSeenLimit { + delete(n.cache.streamUpstreams, key) + n.removeStreamUpstreamCharts(v.name, v.zone) + } + } + } + for key, v := range n.cache.streamUpstreamServers { + if v.updated && !v.hasCharts { + v.hasCharts = true + n.addStreamUpstreamServerCharts(v.name, v.serverAddr, v.serverName, v.zone) + continue + } + if !v.updated { + if v.notSeenTimes++; v.notSeenTimes >= notSeenLimit { + delete(n.cache.streamUpstreamServers, key) + n.removeStreamUpstreamServerCharts(v.name, v.serverAddr, v.zone) + } + } + } + for key, v := range n.cache.resolvers { + if v.updated && !v.hasCharts { + v.hasCharts = true + n.addResolverZoneCharts(v.zone) + continue + } + if !v.updated { + if v.notSeenTimes++; v.notSeenTimes >= notSeenLimit { + delete(n.cache.resolvers, key) + n.removeResolverZoneCharts(v.zone) + } + } + } +} + +func boolToInt(v bool) int64 { + if v { + return 1 + } + return 0 +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/config_schema.json b/src/go/collectors/go.d.plugin/modules/nginxplus/config_schema.json new file mode 100644 index 000000000..937c528d1 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/config_schema.json @@ -0,0 +1,177 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NGINX Plus collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "url": { + "title": "URL", + "description": "The base URL of the NGINX Plus webserver.", + "type": "string", + "default": "http://127.0.0.1:80", + "format": "uri" + }, + "timeout": { + "title": "Timeout", + "description": "The timeout in seconds for the HTTP request.", + "type": "number", + "minimum": 0.5, + "default": 1 + }, + "not_follow_redirects": { + "title": "Not follow redirects", + "description": "If set, the client will not follow HTTP redirects automatically.", + "type": "boolean" + }, + "username": { + "title": "Username", + "description": "The username for basic authentication.", + "type": "string", + "sensitive": true + }, + "password": { + "title": "Password", + "description": "The password for basic authentication.", + "type": "string", + "sensitive": true + }, + "proxy_url": { + "title": "Proxy URL", + "description": "The URL of the proxy server.", + "type": "string" + }, + "proxy_username": { + "title": "Proxy username", + "description": "The username for proxy authentication.", + "type": "string", + "sensitive": true + }, + "proxy_password": { + "title": "Proxy password", + "description": "The password for proxy authentication.", + "type": "string", + "sensitive": true + }, + "headers": { + "title": "Headers", + "description": "Additional HTTP headers to include in the request.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "tls_skip_verify": { + "title": "Skip TLS verification", + "description": "If set, TLS certificate verification will be skipped.", + "type": "boolean" + }, + "tls_ca": { + "title": "TLS CA", + "description": "The path to the CA certificate file for TLS verification.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_cert": { + "title": "TLS certificate", + "description": "The path to the client certificate file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_key": { + "title": "TLS key", + "description": "The path to the client key file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + }, + "body": { + "title": "Body", + "type": "string" + }, + "method": { + "title": "Method", + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "ui:flavour": "tabs", + "ui:options": { + "tabs": [ + { + "title": "Base", + "fields": [ + "update_every", + "url", + "timeout", + "not_follow_redirects" + ] + }, + { + "title": "Auth", + "fields": [ + "username", + "password" + ] + }, + { + "title": "TLS", + "fields": [ + "tls_skip_verify", + "tls_ca", + "tls_cert", + "tls_key" + ] + }, + { + "title": "Proxy", + "fields": [ + "proxy_url", + "proxy_username", + "proxy_password" + ] + }, + { + "title": "Headers", + "fields": [ + "headers" + ] + } + ] + }, + "uiOptions": { + "fullPage": true + }, + "body": { + "ui:widget": "hidden" + }, + "method": { + "ui:widget": "hidden" + }, + "timeout": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + }, + "password": { + "ui:widget": "password" + }, + "proxy_password": { + "ui:widget": "password" + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/integrations/nginx_plus.md b/src/go/collectors/go.d.plugin/modules/nginxplus/integrations/nginx_plus.md new file mode 100644 index 000000000..f0593c212 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/integrations/nginx_plus.md @@ -0,0 +1,413 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/nginxplus/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/nginxplus/metadata.yaml" +sidebar_label: "NGINX Plus" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/Web Servers and Web Proxies" +most_popular: False +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# NGINX Plus + + +<img src="https://netdata.cloud/img/nginxplus.svg" width="150"/> + + +Plugin: go.d.plugin +Module: nginxplus + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors NGINX Plus servers. + + + + +This collector is supported on all platforms. + +This collector supports collecting metrics from multiple instances of this integration, including remote instances. + + +### Default Behavior + +#### Auto-Detection + +This integration doesn't support auto-detection. + +#### Limits + +The default configuration for this integration does not impose any limits on data collection. + +#### Performance Impact + +The default configuration for this integration is not expected to impose a significant performance impact on the system. + + +## Metrics + +Metrics grouped by *scope*. + +The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels. + + + +### Per NGINX Plus instance + +These metrics refer to the entire monitored application. + +This scope has no labels. + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.client_connections_rate | accepted, dropped | connections/s | +| nginxplus.client_connections_count | active, idle | connections | +| nginxplus.ssl_handshakes_rate | successful, failed | handshakes/s | +| nginxplus.ssl_handshakes_failures_rate | no_common_protocol, no_common_cipher, timeout, peer_rejected_cert | failures/s | +| nginxplus.ssl_verification_errors_rate | no_cert, expired_cert, revoked_cert, hostname_mismatch, other | errors/s | +| nginxplus.ssl_session_reuses_rate | ssl_session | reuses/s | +| nginxplus.http_requests_rate | requests | requests/s | +| nginxplus.http_requests_count | requests | requests | +| nginxplus.uptime | uptime | seconds | + +### Per http server zone + +These metrics refer to the HTTP server zone. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| http_server_zone | HTTP server zone name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.http_server_zone_requests_rate | requests | requests/s | +| nginxplus.http_server_zone_responses_per_code_class_rate | 1xx, 2xx, 3xx, 4xx, 5xx | responses/s | +| nginxplus.http_server_zone_traffic_rate | received, sent | bytes/s | +| nginxplus.http_server_zone_requests_processing_count | processing | requests | +| nginxplus.http_server_zone_requests_discarded_rate | discarded | requests/s | + +### Per http location zone + +These metrics refer to the HTTP location zone. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| http_location_zone | HTTP location zone name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.http_location_zone_requests_rate | requests | requests/s | +| nginxplus.http_location_zone_responses_per_code_class_rate | 1xx, 2xx, 3xx, 4xx, 5xx | responses/s | +| nginxplus.http_location_zone_traffic_rate | received, sent | bytes/s | +| nginxplus.http_location_zone_requests_discarded_rate | discarded | requests/s | + +### Per http upstream + +These metrics refer to the HTTP upstream. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| http_upstream_name | HTTP upstream name | +| http_upstream_zone | HTTP upstream zone name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.http_upstream_peers_count | peers | peers | +| nginxplus.http_upstream_zombies_count | zombie | servers | +| nginxplus.http_upstream_keepalive_count | keepalive | connections | + +### Per http upstream server + +These metrics refer to the HTTP upstream server. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| http_upstream_name | HTTP upstream name | +| http_upstream_zone | HTTP upstream zone name | +| http_upstream_server_address | HTTP upstream server address (e.g. 127.0.0.1:81) | +| http_upstream_server_name | HTTP upstream server name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.http_upstream_server_requests_rate | requests | requests/s | +| nginxplus.http_upstream_server_responses_per_code_class_rate | 1xx, 2xx, 3xx, 4xx, 5xx | responses/s | +| nginxplus.http_upstream_server_response_time | response | milliseconds | +| nginxplus.http_upstream_server_response_header_time | header | milliseconds | +| nginxplus.http_upstream_server_traffic_rate | received, sent | bytes/s | +| nginxplus.http_upstream_server_state | up, down, draining, unavail, checking, unhealthy | state | +| nginxplus.http_upstream_server_connections_count | active | connections | +| nginxplus.http_upstream_server_downtime | downtime | seconds | + +### Per http cache + +These metrics refer to the HTTP cache. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| http_cache | HTTP cache name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.http_cache_state | warm, cold | state | +| nginxplus.http_cache_iops | served, written, bypass | responses/s | +| nginxplus.http_cache_io | served, written, bypass | bytes/s | +| nginxplus.http_cache_size | size | bytes | + +### Per stream server zone + +These metrics refer to the Stream server zone. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| stream_server_zone | Stream server zone name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.stream_server_zone_connections_rate | accepted | connections/s | +| nginxplus.stream_server_zone_sessions_per_code_class_rate | 2xx, 4xx, 5xx | sessions/s | +| nginxplus.stream_server_zone_traffic_rate | received, sent | bytes/s | +| nginxplus.stream_server_zone_connections_processing_count | processing | connections | +| nginxplus.stream_server_zone_connections_discarded_rate | discarded | connections/s | + +### Per stream upstream + +These metrics refer to the Stream upstream. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| stream_upstream_name | Stream upstream name | +| stream_upstream_zone | Stream upstream zone name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.stream_upstream_peers_count | peers | peers | +| nginxplus.stream_upstream_zombies_count | zombie | servers | + +### Per stream upstream server + +These metrics refer to the Stream upstream server. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| stream_upstream_name | Stream upstream name | +| stream_upstream_zone | Stream upstream zone name | +| stream_upstream_server_address | Stream upstream server address (e.g. 127.0.0.1:12346) | +| stream_upstream_server_name | Stream upstream server name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.stream_upstream_server_connections_rate | forwarded | connections/s | +| nginxplus.stream_upstream_server_traffic_rate | received, sent | bytes/s | +| nginxplus.stream_upstream_server_state | up, down, unavail, checking, unhealthy | state | +| nginxplus.stream_upstream_server_downtime | downtime | seconds | +| nginxplus.stream_upstream_server_connections_count | active | connections | + +### Per resolver zone + +These metrics refer to the resolver zone. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| resolver_zone | resolver zone name | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxplus.resolver_zone_requests_rate | name, srv, addr | requests/s | +| nginxplus.resolver_zone_responses_rate | noerror, formerr, servfail, nxdomain, notimp, refused, timedout, unknown | responses/s | + + + +## Alerts + +There are no alerts configured by default for this integration. + + +## Setup + +### Prerequisites + +#### Config API + +To configure API, see the [official documentation](https://docs.nginx.com/nginx/admin-guide/monitoring/live-activity-monitoring/#configuring-the-api). + + + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/nginxplus.conf`. + + +You can edit the configuration file using the `edit-config` script from the +Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory). + +```bash +cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata +sudo ./edit-config go.d/nginxplus.conf +``` +#### Options + +The following options can be defined globally: update_every, autodetection_retry. + + +<details open><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 1 | no | +| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no | +| url | Server URL. | http://127.0.0.1 | yes | +| timeout | HTTP request timeout. | 1 | no | +| username | Username for basic HTTP authentication. | | no | +| password | Password for basic HTTP authentication. | | no | +| proxy_url | Proxy URL. | | no | +| proxy_username | Username for proxy basic HTTP authentication. | | no | +| proxy_password | Password for proxy basic HTTP authentication. | | no | +| method | HTTP request method. | GET | no | +| body | HTTP request body. | | no | +| headers | HTTP request headers. | | no | +| not_follow_redirects | Redirect handling policy. Controls whether the client follows redirects. | no | no | +| tls_skip_verify | Server certificate chain and hostname validation policy. Controls whether the client performs this check. | no | no | +| tls_ca | Certification authority that the client uses when verifying the server's certificates. | | no | +| tls_cert | Client TLS certificate. | | no | +| tls_key | Client TLS key. | | no | + +</details> + +#### Examples + +##### Basic + +A basic example configuration. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1 + +``` +</details> + +##### HTTP authentication + +Basic HTTP authentication. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1 + username: username + password: password + +``` +</details> + +##### HTTPS with self-signed certificate + +NGINX Plus with enabled HTTPS and self-signed certificate. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: https://127.0.0.1 + tls_skip_verify: yes + +``` +</details> + +##### Multi-instance + +> **Note**: When you define multiple jobs, their names must be unique. + +Collecting metrics from local and remote instances. + + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1 + + - name: remote + url: http://192.0.2.1 + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +To troubleshoot issues with the `nginxplus` collector, run the `go.d.plugin` with the debug option enabled. The output +should give you clues as to why the collector isn't working. + +- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on + your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`. + + ```bash + cd /usr/libexec/netdata/plugins.d/ + ``` + +- Switch to the `netdata` user. + + ```bash + sudo -u netdata -s + ``` + +- Run the `go.d.plugin` to debug the collector: + + ```bash + ./go.d.plugin -d -m nginxplus + ``` + + diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/metadata.yaml b/src/go/collectors/go.d.plugin/modules/nginxplus/metadata.yaml new file mode 100644 index 000000000..6bc3a29bd --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/metadata.yaml @@ -0,0 +1,584 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-nginxplus + plugin_name: go.d.plugin + module_name: nginxplus + monitored_instance: + name: NGINX Plus + link: https://www.nginx.com/products/nginx/ + icon_filename: nginxplus.svg + categories: + - data-collection.web-servers-and-web-proxies + keywords: + - nginxplus + - nginx + - web + - webserver + - http + - proxy + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + most_popular: false + overview: + data_collection: + metrics_description: | + This collector monitors NGINX Plus servers. + method_description: "" + supported_platforms: + include: [] + exclude: [] + multi_instance: true + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: "" + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: + - title: Config API + description: | + To configure API, see the [official documentation](https://docs.nginx.com/nginx/admin-guide/monitoring/live-activity-monitoring/#configuring-the-api). + configuration: + file: + name: go.d/nginxplus.conf + options: + description: | + The following options can be defined globally: update_every, autodetection_retry. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 1 + required: false + - name: autodetection_retry + description: Recheck interval in seconds. Zero means no recheck will be scheduled. + default_value: 0 + required: false + - name: url + description: Server URL. + default_value: http://127.0.0.1 + required: true + - name: timeout + description: HTTP request timeout. + default_value: 1 + required: false + - name: username + description: Username for basic HTTP authentication. + default_value: "" + required: false + - name: password + description: Password for basic HTTP authentication. + default_value: "" + required: false + - name: proxy_url + description: Proxy URL. + default_value: "" + required: false + - name: proxy_username + description: Username for proxy basic HTTP authentication. + default_value: "" + required: false + - name: proxy_password + description: Password for proxy basic HTTP authentication. + default_value: "" + required: false + - name: method + description: HTTP request method. + default_value: GET + required: false + - name: body + description: HTTP request body. + default_value: "" + required: false + - name: headers + description: HTTP request headers. + default_value: "" + required: false + - name: not_follow_redirects + description: Redirect handling policy. Controls whether the client follows redirects. + default_value: false + required: false + - name: tls_skip_verify + description: Server certificate chain and hostname validation policy. Controls whether the client performs this check. + default_value: false + required: false + - name: tls_ca + description: Certification authority that the client uses when verifying the server's certificates. + default_value: "" + required: false + - name: tls_cert + description: Client TLS certificate. + default_value: "" + required: false + - name: tls_key + description: Client TLS key. + default_value: "" + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Basic + description: A basic example configuration. + config: | + jobs: + - name: local + url: http://127.0.0.1 + - name: HTTP authentication + description: Basic HTTP authentication. + config: | + jobs: + - name: local + url: http://127.0.0.1 + username: username + password: password + - name: HTTPS with self-signed certificate + description: NGINX Plus with enabled HTTPS and self-signed certificate. + config: | + jobs: + - name: local + url: https://127.0.0.1 + tls_skip_verify: yes + - name: Multi-instance + description: | + > **Note**: When you define multiple jobs, their names must be unique. + + Collecting metrics from local and remote instances. + config: | + jobs: + - name: local + url: http://127.0.0.1 + + - name: remote + url: http://192.0.2.1 + troubleshooting: + problems: + list: [] + alerts: [] + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: global + description: These metrics refer to the entire monitored application. + labels: [] + metrics: + - name: nginxplus.client_connections_rate + description: Client connections rate + unit: connections/s + chart_type: line + dimensions: + - name: accepted + - name: dropped + - name: nginxplus.client_connections_count + description: Client connections + unit: connections + chart_type: line + dimensions: + - name: active + - name: idle + - name: nginxplus.ssl_handshakes_rate + description: SSL handshakes rate + unit: handshakes/s + chart_type: line + dimensions: + - name: successful + - name: failed + - name: nginxplus.ssl_handshakes_failures_rate + description: SSL handshakes failures rate + unit: failures/s + chart_type: stacked + dimensions: + - name: no_common_protocol + - name: no_common_cipher + - name: timeout + - name: peer_rejected_cert + - name: nginxplus.ssl_verification_errors_rate + description: SSL verification errors rate + unit: errors/s + chart_type: stacked + dimensions: + - name: no_cert + - name: expired_cert + - name: revoked_cert + - name: hostname_mismatch + - name: other + - name: nginxplus.ssl_session_reuses_rate + description: Session reuses during SSL handshak + unit: reuses/s + chart_type: line + dimensions: + - name: ssl_session + - name: nginxplus.http_requests_rate + description: HTTP requests rate + unit: requests/s + chart_type: line + dimensions: + - name: requests + - name: nginxplus.http_requests_count + description: HTTP requests + unit: requests + chart_type: line + dimensions: + - name: requests + - name: nginxplus.uptime + description: Uptime + unit: seconds + chart_type: line + dimensions: + - name: uptime + - name: http server zone + description: These metrics refer to the HTTP server zone. + labels: + - name: http_server_zone + description: HTTP server zone name + metrics: + - name: nginxplus.http_server_zone_requests_rate + description: HTTP Server Zone requests rate + unit: requests/s + chart_type: line + dimensions: + - name: requests + - name: nginxplus.http_server_zone_responses_per_code_class_rate + description: HTTP Server Zone responses rate + unit: responses/s + chart_type: stacked + dimensions: + - name: 1xx + - name: 2xx + - name: 3xx + - name: 4xx + - name: 5xx + - name: nginxplus.http_server_zone_traffic_rate + description: HTTP Server Zone traffic + unit: bytes/s + chart_type: area + dimensions: + - name: received + - name: sent + - name: nginxplus.http_server_zone_requests_processing_count + description: HTTP Server Zone currently processed requests + unit: requests + chart_type: line + dimensions: + - name: processing + - name: nginxplus.http_server_zone_requests_discarded_rate + description: HTTP Server Zone requests discarded rate + unit: requests/s + chart_type: line + dimensions: + - name: discarded + - name: http location zone + description: These metrics refer to the HTTP location zone. + labels: + - name: http_location_zone + description: HTTP location zone name + metrics: + - name: nginxplus.http_location_zone_requests_rate + description: HTTP Location Zone requests rate + unit: requests/s + chart_type: line + dimensions: + - name: requests + - name: nginxplus.http_location_zone_responses_per_code_class_rate + description: HTTP Location Zone responses rate + unit: responses/s + chart_type: stacked + dimensions: + - name: 1xx + - name: 2xx + - name: 3xx + - name: 4xx + - name: 5xx + - name: nginxplus.http_location_zone_traffic_rate + description: HTTP Location Zone traffic rate + unit: bytes/s + chart_type: area + dimensions: + - name: received + - name: sent + - name: nginxplus.http_location_zone_requests_discarded_rate + description: HTTP Location Zone requests discarded rate + unit: requests/s + chart_type: line + dimensions: + - name: discarded + - name: http upstream + description: These metrics refer to the HTTP upstream. + labels: + - name: http_upstream_name + description: HTTP upstream name + - name: http_upstream_zone + description: HTTP upstream zone name + metrics: + - name: nginxplus.http_upstream_peers_count + description: HTTP Upstream peers + unit: peers + chart_type: line + dimensions: + - name: peers + - name: nginxplus.http_upstream_zombies_count + description: HTTP Upstream zombies + unit: servers + chart_type: line + dimensions: + - name: zombie + - name: nginxplus.http_upstream_keepalive_count + description: HTTP Upstream keepalive + unit: connections + chart_type: line + dimensions: + - name: keepalive + - name: http upstream server + description: These metrics refer to the HTTP upstream server. + labels: + - name: http_upstream_name + description: HTTP upstream name + - name: http_upstream_zone + description: HTTP upstream zone name + - name: http_upstream_server_address + description: HTTP upstream server address (e.g. 127.0.0.1:81) + - name: http_upstream_server_name + description: HTTP upstream server name + metrics: + - name: nginxplus.http_upstream_server_requests_rate + description: HTTP Upstream Server requests + unit: requests/s + chart_type: line + dimensions: + - name: requests + - name: nginxplus.http_upstream_server_responses_per_code_class_rate + description: HTTP Upstream Server responses + unit: responses/s + chart_type: stacked + dimensions: + - name: 1xx + - name: 2xx + - name: 3xx + - name: 4xx + - name: 5xx + - name: nginxplus.http_upstream_server_response_time + description: HTTP Upstream Server average response time + unit: milliseconds + chart_type: line + dimensions: + - name: response + - name: nginxplus.http_upstream_server_response_header_time + description: HTTP Upstream Server average response header time + unit: milliseconds + chart_type: line + dimensions: + - name: header + - name: nginxplus.http_upstream_server_traffic_rate + description: HTTP Upstream Server traffic rate + unit: bytes/s + chart_type: area + dimensions: + - name: received + - name: sent + - name: nginxplus.http_upstream_server_state + description: HTTP Upstream Server state + unit: state + chart_type: line + dimensions: + - name: up + - name: down + - name: draining + - name: unavail + - name: checking + - name: unhealthy + - name: nginxplus.http_upstream_server_connections_count + description: HTTP Upstream Server connections + unit: connections + chart_type: line + dimensions: + - name: active + - name: nginxplus.http_upstream_server_downtime + description: HTTP Upstream Server downtime + unit: seconds + chart_type: line + dimensions: + - name: downtime + - name: http cache + description: These metrics refer to the HTTP cache. + labels: + - name: http_cache + description: HTTP cache name + metrics: + - name: nginxplus.http_cache_state + description: HTTP Cache state + unit: state + chart_type: line + dimensions: + - name: warm + - name: cold + - name: nginxplus.http_cache_iops + description: HTTP Cache size + unit: responses/s + chart_type: line + dimensions: + - name: served + - name: written + - name: bypass + - name: nginxplus.http_cache_io + description: HTTP Cache IOPS + unit: bytes/s + chart_type: line + dimensions: + - name: served + - name: written + - name: bypass + - name: nginxplus.http_cache_size + description: HTTP Cache IO + unit: bytes + chart_type: line + dimensions: + - name: size + - name: stream server zone + description: These metrics refer to the Stream server zone. + labels: + - name: stream_server_zone + description: Stream server zone name + metrics: + - name: nginxplus.stream_server_zone_connections_rate + description: Stream Server Zone connections rate + unit: connections/s + chart_type: line + dimensions: + - name: accepted + - name: nginxplus.stream_server_zone_sessions_per_code_class_rate + description: Stream Server Zone sessions rate + unit: sessions/s + chart_type: stacked + dimensions: + - name: 2xx + - name: 4xx + - name: 5xx + - name: nginxplus.stream_server_zone_traffic_rate + description: Stream Server Zone traffic rate + unit: bytes/s + chart_type: area + dimensions: + - name: received + - name: sent + - name: nginxplus.stream_server_zone_connections_processing_count + description: Stream Server Zone connections processed + unit: connections + chart_type: line + dimensions: + - name: processing + - name: nginxplus.stream_server_zone_connections_discarded_rate + description: Stream Server Zone connections discarded + unit: connections/s + chart_type: line + dimensions: + - name: discarded + - name: stream upstream + description: These metrics refer to the Stream upstream. + labels: + - name: stream_upstream_name + description: Stream upstream name + - name: stream_upstream_zone + description: Stream upstream zone name + metrics: + - name: nginxplus.stream_upstream_peers_count + description: Stream Upstream peers + unit: peers + chart_type: line + dimensions: + - name: peers + - name: nginxplus.stream_upstream_zombies_count + description: Stream Upstream zombies + unit: servers + chart_type: line + dimensions: + - name: zombie + - name: stream upstream server + description: These metrics refer to the Stream upstream server. + labels: + - name: stream_upstream_name + description: Stream upstream name + - name: stream_upstream_zone + description: Stream upstream zone name + - name: stream_upstream_server_address + description: Stream upstream server address (e.g. 127.0.0.1:12346) + - name: stream_upstream_server_name + description: Stream upstream server name + metrics: + - name: nginxplus.stream_upstream_server_connections_rate + description: Stream Upstream Server connections + unit: connections/s + chart_type: line + dimensions: + - name: forwarded + - name: nginxplus.stream_upstream_server_traffic_rate + description: Stream Upstream Server traffic rate + unit: bytes/s + chart_type: area + dimensions: + - name: received + - name: sent + - name: nginxplus.stream_upstream_server_state + description: Stream Upstream Server state + unit: state + chart_type: line + dimensions: + - name: up + - name: down + - name: unavail + - name: checking + - name: unhealthy + - name: nginxplus.stream_upstream_server_downtime + description: Stream Upstream Server downtime + unit: seconds + chart_type: line + dimensions: + - name: downtime + - name: nginxplus.stream_upstream_server_connections_count + description: Stream Upstream Server connections + unit: connections + chart_type: line + dimensions: + - name: active + - name: resolver zone + description: These metrics refer to the resolver zone. + labels: + - name: resolver_zone + description: resolver zone name + metrics: + - name: nginxplus.resolver_zone_requests_rate + description: Resolver requests rate + unit: requests/s + chart_type: stacked + dimensions: + - name: name + - name: srv + - name: addr + - name: nginxplus.resolver_zone_responses_rate + description: Resolver responses rate + unit: responses/s + chart_type: stacked + dimensions: + - name: noerror + - name: formerr + - name: servfail + - name: nxdomain + - name: notimp + - name: refused + - name: timedout + - name: unknown diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/nginx_http_api.go b/src/go/collectors/go.d.plugin/modules/nginxplus/nginx_http_api.go new file mode 100644 index 000000000..0f7999ac5 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/nginx_http_api.go @@ -0,0 +1,212 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxplus + +import "time" + +// https://demo.nginx.com/dashboard.html +// https://demo.nginx.com/swagger-ui/ +// http://nginx.org/en/docs/http/ngx_http_api_module.html + +type nginxAPIVersions []int64 + +type ( + nginxInfo struct { + Version string `json:"version"` + Build string `json:"build"` + Address string `json:"address"` + Generation int `json:"generation"` + LoadTimestamp time.Time `json:"load_timestamp"` + Timestamp time.Time `json:"timestamp"` + } + nginxConnections struct { + Accepted int64 `json:"accepted"` + Dropped int64 `json:"dropped"` + Active int64 `json:"active"` + Idle int64 `json:"idle"` + } + nginxSSL struct { + Handshakes int64 `json:"handshakes"` + HandshakesFailed int64 `json:"handshakes_failed"` + SessionReuses int64 `json:"session_reuses"` + NoCommonProtocol int64 `json:"no_common_protocol"` + NoCommonCipher int64 `json:"no_common_cipher"` + HandshakeTimeout int64 `json:"handshake_timeout"` + PeerRejectedCert int64 `json:"peer_rejected_cert"` + VerifyFailures struct { + NoCert int64 `json:"no_cert"` + ExpiredCert int64 `json:"expired_cert"` + RevokedCert int64 `json:"revoked_cert"` + HostnameMismatch int64 `json:"hostname_mismatch"` + Other int64 `json:"other"` + } `json:"verify_failures"` + } +) + +type ( + nginxHTTPRequests struct { + Total int64 `json:"total"` + Current int64 `json:"current"` + } + nginxHTTPServerZones map[string]struct { + Processing int64 `json:"processing"` + Requests int64 `json:"requests"` + Responses struct { + Class1xx int64 `json:"1xx"` + Class2xx int64 `json:"2xx"` + Class3xx int64 `json:"3xx"` + Class4xx int64 `json:"4xx"` + Class5xx int64 `json:"5xx"` + Total int64 + } `json:"responses"` + Discarded int64 `json:"discarded"` + Received int64 `json:"received"` + Sent int64 `json:"sent"` + } + nginxHTTPLocationZones map[string]struct { + Requests int64 `json:"requests"` + Responses struct { + Class1xx int64 `json:"1xx"` + Class2xx int64 `json:"2xx"` + Class3xx int64 `json:"3xx"` + Class4xx int64 `json:"4xx"` + Class5xx int64 `json:"5xx"` + Total int64 + } `json:"responses"` + Discarded int64 `json:"discarded"` + Received int64 `json:"received"` + Sent int64 `json:"sent"` + } + nginxHTTPUpstreams map[string]struct { + Peers []struct { + Id int64 `json:"id"` + Server string `json:"server"` + Name string `json:"name"` + Backup bool `json:"backup"` + Weight int64 `json:"weight"` + State string `json:"state"` + Active int64 `json:"active"` + Requests int64 `json:"requests"` + HeaderTime int64 `json:"header_time"` + ResponseTime int64 `json:"response_time"` + Responses struct { + Class1xx int64 `json:"1xx"` + Class2xx int64 `json:"2xx"` + Class3xx int64 `json:"3xx"` + Class4xx int64 `json:"4xx"` + Class5xx int64 `json:"5xx"` + Total int64 + } `json:"responses"` + Sent int64 `json:"sent"` + Received int64 `json:"received"` + Fails int64 `json:"fails"` + Unavail int64 `json:"unavail"` + HealthChecks struct { + Checks int64 `json:"checks"` + Fails int64 `json:"fails"` + Unhealthy int64 `json:"unhealthy"` + } `json:"health_checks"` + Downtime int64 `json:"downtime"` + Selected time.Time `json:"selected"` + } `json:"peers"` + Keepalive int64 `json:"keepalive"` + Zombies int64 `json:"zombies"` + Zone string `json:"zone"` + } + nginxHTTPCaches map[string]struct { + Size int64 `json:"size"` + Cold bool `json:"cold"` + Hit struct { + Responses int64 `json:"responses"` + Bytes int64 `json:"bytes"` + } `json:"hit"` + Stale struct { + Responses int64 `json:"responses"` + Bytes int64 `json:"bytes"` + } `json:"stale"` + Updating struct { + Responses int64 `json:"responses"` + Bytes int64 `json:"bytes"` + } `json:"updating"` + Revalidated struct { + Responses int64 `json:"responses"` + Bytes int64 `json:"bytes"` + } `json:"revalidated"` + Miss struct { + Responses int64 `json:"responses"` + Bytes int64 `json:"bytes"` + ResponsesWritten int64 `json:"responses_written"` + BytesWritten int64 `json:"bytes_written"` + } `json:"miss"` + Expired struct { + Responses int64 `json:"responses"` + Bytes int64 `json:"bytes"` + ResponsesWritten int64 `json:"responses_written"` + BytesWritten int64 `json:"bytes_written"` + } `json:"expired"` + Bypass struct { + Responses int64 `json:"responses"` + Bytes int64 `json:"bytes"` + ResponsesWritten int64 `json:"responses_written"` + BytesWritten int64 `json:"bytes_written"` + } `json:"bypass"` + } +) + +type ( + nginxStreamServerZones map[string]struct { + Processing int64 `json:"processing"` + Connections int64 `json:"connections"` + Sessions struct { + Class2xx int64 `json:"2xx"` + Class4xx int64 `json:"4xx"` + Class5xx int64 `json:"5xx"` + Total int64 `json:"total"` + } `json:"sessions"` + Discarded int64 `json:"discarded"` + Received int64 `json:"received"` + Sent int64 `json:"sent"` + } + nginxStreamUpstreams map[string]struct { + Peers []struct { + Id int64 `json:"id"` + Server string `json:"server"` + Name string `json:"name"` + Backup bool `json:"backup"` + Weight int64 `json:"weight"` + State string `json:"state"` + Active int64 `json:"active"` + Connections int64 `json:"connections"` + Sent int64 `json:"sent"` + Received int64 `json:"received"` + Fails int64 `json:"fails"` + Unavail int64 `json:"unavail"` + HealthChecks struct { + Checks int64 `json:"checks"` + Fails int64 `json:"fails"` + Unhealthy int64 `json:"unhealthy"` + } `json:"health_checks"` + Downtime int64 `json:"downtime"` + } `json:"peers"` + Zombies int64 `json:"zombies"` + Zone string `json:"zone"` + } +) + +type nginxResolvers map[string]struct { + Requests struct { + Name int64 `json:"name"` + Srv int64 `json:"srv"` + Addr int64 `json:"addr"` + } `json:"requests"` + Responses struct { + NoError int64 `json:"noerror"` + Formerr int64 `json:"formerr"` + Servfail int64 `json:"servfail"` + Nxdomain int64 `json:"nxdomain"` + Notimp int64 `json:"notimp"` + Refused int64 `json:"refused"` + TimedOut int64 `json:"timedout"` + Unknown int64 `json:"unknown"` + } `json:"responses"` +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/nginx_http_api_query.go b/src/go/collectors/go.d.plugin/modules/nginxplus/nginx_http_api_query.go new file mode 100644 index 000000000..b05ce1d7b --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/nginx_http_api_query.go @@ -0,0 +1,388 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxplus + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "sync" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +const ( + urlPathAPIVersions = "/api/" + urlPathAPIEndpointsRoot = "/api/%d" + urlPathAPINginx = "/api/%d/nginx" + urlPathAPIEndpointsHTTP = "/api/%d/http" + urlPathAPIEndpointsStream = "/api/%d/stream" + urlPathAPIConnections = "/api/%d/connections" + urlPathAPISSL = "/api/%d/ssl" + urlPathAPIResolvers = "/api/%d/resolvers" + urlPathAPIHTTPRequests = "/api/%d/http/requests" + urlPathAPIHTTPServerZones = "/api/%d/http/server_zones" + urlPathAPIHTTPLocationZones = "/api/%d/http/location_zones" + urlPathAPIHTTPUpstreams = "/api/%d/http/upstreams" + urlPathAPIHTTPCaches = "/api/%d/http/caches" + urlPathAPIStreamServerZones = "/api/%d/stream/server_zones" + urlPathAPIStreamUpstreams = "/api/%d/stream/upstreams" +) + +type nginxMetrics struct { + info *nginxInfo + connections *nginxConnections + ssl *nginxSSL + httpRequests *nginxHTTPRequests + httpServerZones *nginxHTTPServerZones + httpLocationZones *nginxHTTPLocationZones + httpUpstreams *nginxHTTPUpstreams + httpCaches *nginxHTTPCaches + streamServerZones *nginxStreamServerZones + streamUpstreams *nginxStreamUpstreams + resolvers *nginxResolvers +} + +func (n *NginxPlus) queryAPIVersion() (int64, error) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = urlPathAPIVersions + + var versions nginxAPIVersions + if err := n.doWithDecode(&versions, req); err != nil { + return 0, err + } + + if len(versions) == 0 { + return 0, fmt.Errorf("'%s' returned no data", req.URL) + } + + return versions[len(versions)-1], nil +} + +func (n *NginxPlus) queryAvailableEndpoints() error { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIEndpointsRoot, n.apiVersion) + + var endpoints []string + if err := n.doWithDecode(&endpoints, req); err != nil { + return err + } + + n.Debugf("discovered root endpoints: %v", endpoints) + var hasHTTP, hasStream bool + for _, v := range endpoints { + switch v { + case "nginx": + n.endpoints.nginx = true + case "connections": + n.endpoints.connections = true + case "ssl": + n.endpoints.ssl = true + case "resolvers": + n.endpoints.resolvers = true + case "http": + hasHTTP = true + case "stream": + hasStream = true + } + } + + if hasHTTP { + endpoints = endpoints[:0] + req, _ = web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIEndpointsHTTP, n.apiVersion) + + if err := n.doWithDecode(&endpoints, req); err != nil { + return err + } + + n.Debugf("discovered http endpoints: %v", endpoints) + for _, v := range endpoints { + switch v { + case "requests": + n.endpoints.httpRequest = true + case "server_zones": + n.endpoints.httpServerZones = true + case "location_zones": + n.endpoints.httpLocationZones = true + case "caches": + n.endpoints.httpCaches = true + case "upstreams": + n.endpoints.httpUpstreams = true + } + } + } + + if hasStream { + endpoints = endpoints[:0] + req, _ = web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIEndpointsStream, n.apiVersion) + + if err := n.doWithDecode(&endpoints, req); err != nil { + return err + } + + n.Debugf("discovered stream endpoints: %v", endpoints) + for _, v := range endpoints { + switch v { + case "server_zones": + n.endpoints.streamServerZones = true + case "upstreams": + n.endpoints.streamUpstreams = true + } + } + } + + return nil +} + +func (n *NginxPlus) queryMetrics() *nginxMetrics { + ms := &nginxMetrics{} + wg := &sync.WaitGroup{} + + for _, task := range []struct { + do bool + fn func(*nginxMetrics) + }{ + {do: n.endpoints.nginx, fn: n.queryNginxInfo}, + {do: n.endpoints.connections, fn: n.queryConnections}, + {do: n.endpoints.ssl, fn: n.querySSL}, + {do: n.endpoints.httpRequest, fn: n.queryHTTPRequests}, + {do: n.endpoints.httpServerZones, fn: n.queryHTTPServerZones}, + {do: n.endpoints.httpLocationZones, fn: n.queryHTTPLocationZones}, + {do: n.endpoints.httpUpstreams, fn: n.queryHTTPUpstreams}, + {do: n.endpoints.httpCaches, fn: n.queryHTTPCaches}, + {do: n.endpoints.streamServerZones, fn: n.queryStreamServerZones}, + {do: n.endpoints.streamUpstreams, fn: n.queryStreamUpstreams}, + {do: n.endpoints.resolvers, fn: n.queryResolvers}, + } { + task := task + if task.do { + wg.Add(1) + go func() { task.fn(ms); wg.Done() }() + } + } + + wg.Wait() + + return ms +} + +func (n *NginxPlus) queryNginxInfo(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPINginx, n.apiVersion) + + var v nginxInfo + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.nginx = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.info = &v +} + +func (n *NginxPlus) queryConnections(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIConnections, n.apiVersion) + + var v nginxConnections + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.connections = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.connections = &v +} + +func (n *NginxPlus) querySSL(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPISSL, n.apiVersion) + + var v nginxSSL + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.ssl = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.ssl = &v +} + +func (n *NginxPlus) queryHTTPRequests(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIHTTPRequests, n.apiVersion) + + var v nginxHTTPRequests + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.httpRequest = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.httpRequests = &v +} + +func (n *NginxPlus) queryHTTPServerZones(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIHTTPServerZones, n.apiVersion) + + var v nginxHTTPServerZones + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.httpServerZones = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.httpServerZones = &v +} + +func (n *NginxPlus) queryHTTPLocationZones(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIHTTPLocationZones, n.apiVersion) + + var v nginxHTTPLocationZones + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.httpLocationZones = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.httpLocationZones = &v +} + +func (n *NginxPlus) queryHTTPUpstreams(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIHTTPUpstreams, n.apiVersion) + + var v nginxHTTPUpstreams + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.httpUpstreams = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.httpUpstreams = &v +} + +func (n *NginxPlus) queryHTTPCaches(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIHTTPCaches, n.apiVersion) + + var v nginxHTTPCaches + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.httpCaches = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.httpCaches = &v +} + +func (n *NginxPlus) queryStreamServerZones(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIStreamServerZones, n.apiVersion) + + var v nginxStreamServerZones + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.streamServerZones = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.streamServerZones = &v +} + +func (n *NginxPlus) queryStreamUpstreams(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIStreamUpstreams, n.apiVersion) + + var v nginxStreamUpstreams + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.streamUpstreams = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.streamUpstreams = &v +} + +func (n *NginxPlus) queryResolvers(ms *nginxMetrics) { + req, _ := web.NewHTTPRequest(n.Request.Copy()) + req.URL.Path = fmt.Sprintf(urlPathAPIResolvers, n.apiVersion) + + var v nginxResolvers + + if err := n.doWithDecode(&v, req); err != nil { + n.endpoints.resolvers = !errors.Is(err, errPathNotFound) + n.Warning(err) + return + } + + ms.resolvers = &v +} + +var ( + errPathNotFound = errors.New("path not found") +) + +func (n *NginxPlus) doWithDecode(dst interface{}, req *http.Request) error { + n.Debugf("executing %s '%s'", req.Method, req.URL) + resp, err := n.httpClient.Do(req) + if err != nil { + return err + } + defer closeBody(resp) + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("%s returned %d status code (%w)", req.URL, resp.StatusCode, errPathNotFound) + } + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("%s returned %d status code (%s)", req.URL, resp.StatusCode, resp.Status) + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error on reading response from %s : %v", req.URL, err) + } + + if err := json.Unmarshal(content, dst); err != nil { + return fmt.Errorf("error on parsing response from %s : %v", req.URL, err) + } + + return nil +} + +func closeBody(resp *http.Response) { + if resp != nil && resp.Body != nil { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + } +} + +func (n *nginxMetrics) empty() bool { + return n.info != nil && + n.connections == nil && + n.ssl == nil && + n.httpRequests == nil && + n.httpServerZones == nil && + n.httpLocationZones == nil && + n.httpUpstreams == nil && + n.httpCaches == nil && + n.streamServerZones == nil && + n.streamUpstreams == nil && + n.resolvers != nil +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/nginxplus.go b/src/go/collectors/go.d.plugin/modules/nginxplus/nginxplus.go new file mode 100644 index 000000000..3a0c2f97c --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/nginxplus.go @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxplus + +import ( + _ "embed" + "errors" + "net/http" + "time" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("nginxplus", module.Creator{ + JobConfigSchema: configSchema, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *NginxPlus { + return &NginxPlus{ + Config: Config{ + HTTP: web.HTTP{ + Request: web.Request{ + URL: "http://127.0.0.1", + }, + Client: web.Client{ + Timeout: web.Duration(time.Second * 1), + }, + }, + }, + charts: baseCharts.Copy(), + queryEndpointsEvery: time.Minute, + cache: newCache(), + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + web.HTTP `yaml:",inline" json:""` +} + +type NginxPlus struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + httpClient *http.Client + + apiVersion int64 + endpoints struct { + nginx bool + connections bool + ssl bool + httpCaches bool + httpRequest bool + httpServerZones bool + httpLocationZones bool + httpUpstreams bool + streamServerZones bool + streamUpstreams bool + resolvers bool + } + queryEndpointsTime time.Time + queryEndpointsEvery time.Duration + cache *cache +} + +func (n *NginxPlus) Configuration() any { + return n.Config +} + +func (n *NginxPlus) Init() error { + if n.URL == "" { + n.Error("config validation: 'url' can not be empty'") + return errors.New("url not set") + } + + client, err := web.NewHTTPClient(n.Client) + if err != nil { + n.Errorf("init HTTP client: %v", err) + return err + } + n.httpClient = client + + return nil +} + +func (n *NginxPlus) Check() error { + mx, err := n.collect() + if err != nil { + n.Error(err) + return err + } + if len(mx) == 0 { + return errors.New("no metrics collected") + } + return nil +} + +func (n *NginxPlus) Charts() *module.Charts { + return n.charts +} + +func (n *NginxPlus) Collect() map[string]int64 { + mx, err := n.collect() + + if err != nil { + n.Error(err) + return nil + } + + return mx +} + +func (n *NginxPlus) Cleanup() { + if n.httpClient != nil { + n.httpClient.CloseIdleConnections() + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/nginxplus_test.go b/src/go/collectors/go.d.plugin/modules/nginxplus/nginxplus_test.go new file mode 100644 index 000000000..7c6f4fc76 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/nginxplus_test.go @@ -0,0 +1,596 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxplus + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") + + dataAPI8APIVersions, _ = os.ReadFile("testdata/api-8/api_versions.json") + dataAPI8Connections, _ = os.ReadFile("testdata/api-8/connections.json") + dataAPI8EndpointsHTTP, _ = os.ReadFile("testdata/api-8/endpoints_http.json") + dataAPI8EndpointsRoot, _ = os.ReadFile("testdata/api-8/endpoints_root.json") + dataAPI8EndpointsStream, _ = os.ReadFile("testdata/api-8/endpoints_stream.json") + dataAPI8HTTPCaches, _ = os.ReadFile("testdata/api-8/http_caches.json") + dataAPI8HTTPLocationZones, _ = os.ReadFile("testdata/api-8/http_location_zones.json") + dataAPI8HTTPRequests, _ = os.ReadFile("testdata/api-8/http_requests.json") + dataAPI8HTTPServerZones, _ = os.ReadFile("testdata/api-8/http_server_zones.json") + dataAPI8HTTPUpstreams, _ = os.ReadFile("testdata/api-8/http_upstreams.json") + dataAPI8SSL, _ = os.ReadFile("testdata/api-8/ssl.json") + dataAPI8StreamServerZones, _ = os.ReadFile("testdata/api-8/stream_server_zones.json") + dataAPI8StreamUpstreams, _ = os.ReadFile("testdata/api-8/stream_upstreams.json") + dataAPI8Resolvers, _ = os.ReadFile("testdata/api-8/resolvers.json") + data404, _ = os.ReadFile("testdata/404.json") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + "dataAPI8APIVersions": dataAPI8APIVersions, + "dataAPI8Connections": dataAPI8Connections, + "dataAPI8EndpointsHTTP": dataAPI8EndpointsHTTP, + "dataAPI8EndpointsRoot": dataAPI8EndpointsRoot, + "dataAPI8EndpointsStream": dataAPI8EndpointsStream, + "dataAPI8HTTPCaches": dataAPI8HTTPCaches, + "dataAPI8HTTPLocationZones": dataAPI8HTTPLocationZones, + "dataAPI8HTTPRequests": dataAPI8HTTPRequests, + "dataAPI8HTTPServerZones": dataAPI8HTTPServerZones, + "dataAPI8HTTPUpstreams": dataAPI8HTTPUpstreams, + "dataAPI8SSL": dataAPI8SSL, + "dataAPI8StreamServerZones": dataAPI8StreamServerZones, + "dataAPI8StreamUpstreams": dataAPI8StreamUpstreams, + "dataAPI8Resolvers": dataAPI8Resolvers, + "data404": data404, + } { + require.NotNil(t, data, name) + } +} + +func TestNginxPlus_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &NginxPlus{}, dataConfigJSON, dataConfigYAML) +} + +func TestNginxPlus_Init(t *testing.T) { + tests := map[string]struct { + wantFail bool + config Config + }{ + "success with default": { + wantFail: false, + config: New().Config, + }, + "fail when URL not set": { + wantFail: true, + config: Config{ + HTTP: web.HTTP{ + Request: web.Request{URL: ""}, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + nginx := New() + nginx.Config = test.config + + if test.wantFail { + assert.Error(t, nginx.Init()) + } else { + assert.NoError(t, nginx.Init()) + } + }) + } +} + +func TestNginxPlus_Check(t *testing.T) { + tests := map[string]struct { + wantFail bool + prepare func(t *testing.T) (nginx *NginxPlus, cleanup func()) + }{ + "success when all requests OK": { + wantFail: false, + prepare: caseAPI8AllRequestsOK, + }, + "success when all requests except stream OK": { + wantFail: false, + prepare: caseAPI8AllRequestsExceptStreamOK, + }, + "fail on invalid data response": { + wantFail: true, + prepare: caseInvalidDataResponse, + }, + "fail on connection refused": { + wantFail: true, + prepare: caseConnectionRefused, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + nginx, cleanup := test.prepare(t) + defer cleanup() + + if test.wantFail { + assert.Error(t, nginx.Check()) + } else { + assert.NoError(t, nginx.Check()) + } + }) + } +} + +func TestNginxPlus_Collect(t *testing.T) { + tests := map[string]struct { + prepare func(t *testing.T) (nginx *NginxPlus, cleanup func()) + wantNumOfCharts int + wantMetrics map[string]int64 + }{ + "success when all requests OK": { + prepare: caseAPI8AllRequestsOK, + wantNumOfCharts: len(baseCharts) + + len(httpCacheChartsTmpl) + + len(httpServerZoneChartsTmpl) + + len(httpLocationZoneChartsTmpl)*2 + + len(httpUpstreamChartsTmpl) + + len(httpUpstreamServerChartsTmpl)*2 + + len(streamServerZoneChartsTmpl) + + len(streamUpstreamChartsTmpl) + + len(streamUpstreamServerChartsTmpl)*2 + + len(resolverZoneChartsTmpl)*2, + wantMetrics: map[string]int64{ + "connections_accepted": 6079, + "connections_active": 1, + "connections_dropped": 0, + "connections_idle": 8, + "http_cache_cache_backend_bypassed_bytes": 67035, + "http_cache_cache_backend_bypassed_responses": 109, + "http_cache_cache_backend_served_bytes": 0, + "http_cache_cache_backend_served_responses": 0, + "http_cache_cache_backend_size": 0, + "http_cache_cache_backend_state_cold": 0, + "http_cache_cache_backend_state_warm": 1, + "http_cache_cache_backend_written_bytes": 0, + "http_cache_cache_backend_written_responses": 0, + "http_location_zone_server_api_bytes_received": 1854427, + "http_location_zone_server_api_bytes_sent": 4668778, + "http_location_zone_server_api_requests": 9188, + "http_location_zone_server_api_requests_discarded": 0, + "http_location_zone_server_api_responses": 9188, + "http_location_zone_server_api_responses_1xx": 0, + "http_location_zone_server_api_responses_2xx": 9187, + "http_location_zone_server_api_responses_3xx": 0, + "http_location_zone_server_api_responses_4xx": 1, + "http_location_zone_server_api_responses_5xx": 0, + "http_location_zone_server_dashboard_bytes_received": 0, + "http_location_zone_server_dashboard_bytes_sent": 0, + "http_location_zone_server_dashboard_requests": 0, + "http_location_zone_server_dashboard_requests_discarded": 0, + "http_location_zone_server_dashboard_responses": 0, + "http_location_zone_server_dashboard_responses_1xx": 0, + "http_location_zone_server_dashboard_responses_2xx": 0, + "http_location_zone_server_dashboard_responses_3xx": 0, + "http_location_zone_server_dashboard_responses_4xx": 0, + "http_location_zone_server_dashboard_responses_5xx": 0, + "http_requests_current": 1, + "http_requests_total": 8363, + "http_server_zone_server_backend_bytes_received": 1773834, + "http_server_zone_server_backend_bytes_sent": 4585734, + "http_server_zone_server_backend_requests": 8962, + "http_server_zone_server_backend_requests_discarded": 0, + "http_server_zone_server_backend_requests_processing": 1, + "http_server_zone_server_backend_responses": 8961, + "http_server_zone_server_backend_responses_1xx": 0, + "http_server_zone_server_backend_responses_2xx": 8960, + "http_server_zone_server_backend_responses_3xx": 0, + "http_server_zone_server_backend_responses_4xx": 1, + "http_server_zone_server_backend_responses_5xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_active": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_bytes_received": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_bytes_sent": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_downtime": 1020, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_header_time": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_requests": 26, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_response_time": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_1xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_2xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_3xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_4xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_5xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_checking": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_down": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_draining": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_unavail": 1, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_unhealthy": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_up": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_active": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_bytes_received": 86496, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_bytes_sent": 9180, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_downtime": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_header_time": 1, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_requests": 102, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_response_time": 1, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses": 102, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_1xx": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_2xx": 102, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_3xx": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_4xx": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_5xx": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_checking": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_down": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_draining": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_unavail": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_unhealthy": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_up": 1, + "http_upstream_backend_zone_http_backend_keepalive": 0, + "http_upstream_backend_zone_http_backend_peers": 2, + "http_upstream_backend_zone_http_backend_zombies": 0, + "resolver_zone_resolver-http_requests_addr": 0, + "resolver_zone_resolver-http_requests_name": 0, + "resolver_zone_resolver-http_requests_srv": 2939408, + "resolver_zone_resolver-http_responses_formerr": 0, + "resolver_zone_resolver-http_responses_noerror": 0, + "resolver_zone_resolver-http_responses_notimp": 0, + "resolver_zone_resolver-http_responses_nxdomain": 2939404, + "resolver_zone_resolver-http_responses_refused": 0, + "resolver_zone_resolver-http_responses_servfail": 0, + "resolver_zone_resolver-http_responses_timedout": 4, + "resolver_zone_resolver-http_responses_unknown": 0, + "resolver_zone_resolver-stream_requests_addr": 0, + "resolver_zone_resolver-stream_requests_name": 638797, + "resolver_zone_resolver-stream_requests_srv": 0, + "resolver_zone_resolver-stream_responses_formerr": 0, + "resolver_zone_resolver-stream_responses_noerror": 433136, + "resolver_zone_resolver-stream_responses_notimp": 0, + "resolver_zone_resolver-stream_responses_nxdomain": 40022, + "resolver_zone_resolver-stream_responses_refused": 165639, + "resolver_zone_resolver-stream_responses_servfail": 0, + "resolver_zone_resolver-stream_responses_timedout": 0, + "resolver_zone_resolver-stream_responses_unknown": 0, + "ssl_handshake_timeout": 4, + "ssl_handshakes": 15804607, + "ssl_handshakes_failed": 37862, + "ssl_no_common_cipher": 24, + "ssl_no_common_protocol": 16648, + "ssl_peer_rejected_cert": 0, + "ssl_session_reuses": 13096060, + "ssl_verify_failures_expired_cert": 0, + "ssl_verify_failures_hostname_mismatch": 0, + "ssl_verify_failures_other": 0, + "ssl_verify_failures_no_cert": 0, + "ssl_verify_failures_revoked_cert": 0, + "stream_server_zone_tcp_server_bytes_received": 0, + "stream_server_zone_tcp_server_bytes_sent": 0, + "stream_server_zone_tcp_server_connections": 0, + "stream_server_zone_tcp_server_connections_discarded": 0, + "stream_server_zone_tcp_server_connections_processing": 0, + "stream_server_zone_tcp_server_sessions": 0, + "stream_server_zone_tcp_server_sessions_2xx": 0, + "stream_server_zone_tcp_server_sessions_4xx": 0, + "stream_server_zone_tcp_server_sessions_5xx": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_active": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_bytes_received": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_bytes_sent": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_connections": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_downtime": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_state_checking": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_state_down": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_state_unavail": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_state_unhealthy": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12346_zone_tcp_servers_state_up": 1, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_active": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_bytes_received": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_bytes_sent": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_connections": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_downtime": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_state_checking": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_state_down": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_state_unavail": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_state_unhealthy": 0, + "stream_upstream_stream_backend_server_127.0.0.1:12347_zone_tcp_servers_state_up": 1, + "stream_upstream_stream_backend_zone_tcp_servers_peers": 2, + "stream_upstream_stream_backend_zone_tcp_servers_zombies": 0, + }, + }, + "success when all requests except stream OK": { + prepare: caseAPI8AllRequestsExceptStreamOK, + wantNumOfCharts: len(baseCharts) + + len(httpCacheChartsTmpl) + + len(httpServerZoneChartsTmpl) + + len(httpLocationZoneChartsTmpl)*2 + + len(httpUpstreamChartsTmpl) + + len(httpUpstreamServerChartsTmpl)*2 + + len(resolverZoneChartsTmpl)*2, + wantMetrics: map[string]int64{ + "connections_accepted": 6079, + "connections_active": 1, + "connections_dropped": 0, + "connections_idle": 8, + "http_cache_cache_backend_bypassed_bytes": 67035, + "http_cache_cache_backend_bypassed_responses": 109, + "http_cache_cache_backend_served_bytes": 0, + "http_cache_cache_backend_served_responses": 0, + "http_cache_cache_backend_size": 0, + "http_cache_cache_backend_state_cold": 0, + "http_cache_cache_backend_state_warm": 1, + "http_cache_cache_backend_written_bytes": 0, + "http_cache_cache_backend_written_responses": 0, + "http_location_zone_server_api_bytes_received": 1854427, + "http_location_zone_server_api_bytes_sent": 4668778, + "http_location_zone_server_api_requests": 9188, + "http_location_zone_server_api_requests_discarded": 0, + "http_location_zone_server_api_responses": 9188, + "http_location_zone_server_api_responses_1xx": 0, + "http_location_zone_server_api_responses_2xx": 9187, + "http_location_zone_server_api_responses_3xx": 0, + "http_location_zone_server_api_responses_4xx": 1, + "http_location_zone_server_api_responses_5xx": 0, + "http_location_zone_server_dashboard_bytes_received": 0, + "http_location_zone_server_dashboard_bytes_sent": 0, + "http_location_zone_server_dashboard_requests": 0, + "http_location_zone_server_dashboard_requests_discarded": 0, + "http_location_zone_server_dashboard_responses": 0, + "http_location_zone_server_dashboard_responses_1xx": 0, + "http_location_zone_server_dashboard_responses_2xx": 0, + "http_location_zone_server_dashboard_responses_3xx": 0, + "http_location_zone_server_dashboard_responses_4xx": 0, + "http_location_zone_server_dashboard_responses_5xx": 0, + "http_requests_current": 1, + "http_requests_total": 8363, + "http_server_zone_server_backend_bytes_received": 1773834, + "http_server_zone_server_backend_bytes_sent": 4585734, + "http_server_zone_server_backend_requests": 8962, + "http_server_zone_server_backend_requests_discarded": 0, + "http_server_zone_server_backend_requests_processing": 1, + "http_server_zone_server_backend_responses": 8961, + "http_server_zone_server_backend_responses_1xx": 0, + "http_server_zone_server_backend_responses_2xx": 8960, + "http_server_zone_server_backend_responses_3xx": 0, + "http_server_zone_server_backend_responses_4xx": 1, + "http_server_zone_server_backend_responses_5xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_active": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_bytes_received": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_bytes_sent": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_downtime": 1020, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_header_time": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_requests": 26, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_response_time": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_1xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_2xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_3xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_4xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_responses_5xx": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_checking": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_down": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_draining": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_unavail": 1, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_unhealthy": 0, + "http_upstream_backend_server_127.0.0.1:81_zone_http_backend_state_up": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_active": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_bytes_received": 86496, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_bytes_sent": 9180, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_downtime": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_header_time": 1, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_requests": 102, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_response_time": 1, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses": 102, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_1xx": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_2xx": 102, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_3xx": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_4xx": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_responses_5xx": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_checking": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_down": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_draining": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_unavail": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_unhealthy": 0, + "http_upstream_backend_server_127.0.0.1:82_zone_http_backend_state_up": 1, + "http_upstream_backend_zone_http_backend_keepalive": 0, + "http_upstream_backend_zone_http_backend_peers": 2, + "http_upstream_backend_zone_http_backend_zombies": 0, + "resolver_zone_resolver-http_requests_addr": 0, + "resolver_zone_resolver-http_requests_name": 0, + "resolver_zone_resolver-http_requests_srv": 2939408, + "resolver_zone_resolver-http_responses_formerr": 0, + "resolver_zone_resolver-http_responses_noerror": 0, + "resolver_zone_resolver-http_responses_notimp": 0, + "resolver_zone_resolver-http_responses_nxdomain": 2939404, + "resolver_zone_resolver-http_responses_refused": 0, + "resolver_zone_resolver-http_responses_servfail": 0, + "resolver_zone_resolver-http_responses_timedout": 4, + "resolver_zone_resolver-http_responses_unknown": 0, + "resolver_zone_resolver-stream_requests_addr": 0, + "resolver_zone_resolver-stream_requests_name": 638797, + "resolver_zone_resolver-stream_requests_srv": 0, + "resolver_zone_resolver-stream_responses_formerr": 0, + "resolver_zone_resolver-stream_responses_noerror": 433136, + "resolver_zone_resolver-stream_responses_notimp": 0, + "resolver_zone_resolver-stream_responses_nxdomain": 40022, + "resolver_zone_resolver-stream_responses_refused": 165639, + "resolver_zone_resolver-stream_responses_servfail": 0, + "resolver_zone_resolver-stream_responses_timedout": 0, + "resolver_zone_resolver-stream_responses_unknown": 0, + "ssl_handshake_timeout": 4, + "ssl_handshakes": 15804607, + "ssl_handshakes_failed": 37862, + "ssl_no_common_cipher": 24, + "ssl_no_common_protocol": 16648, + "ssl_peer_rejected_cert": 0, + "ssl_session_reuses": 13096060, + "ssl_verify_failures_expired_cert": 0, + "ssl_verify_failures_hostname_mismatch": 0, + "ssl_verify_failures_other": 0, + "ssl_verify_failures_no_cert": 0, + "ssl_verify_failures_revoked_cert": 0, + }, + }, + "fail on invalid data response": { + prepare: caseInvalidDataResponse, + wantNumOfCharts: 0, + wantMetrics: nil, + }, + "fail on connection refused": { + prepare: caseConnectionRefused, + wantNumOfCharts: 0, + wantMetrics: nil, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + nginx, cleanup := test.prepare(t) + defer cleanup() + + mx := nginx.Collect() + + require.Equal(t, test.wantMetrics, mx) + if len(test.wantMetrics) > 0 { + assert.Equalf(t, test.wantNumOfCharts, len(*nginx.Charts()), "number of charts") + ensureCollectedHasAllChartsDimsVarsIDs(t, nginx, mx) + } + }) + } +} + +func caseAPI8AllRequestsOK(t *testing.T) (*NginxPlus, func()) { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case urlPathAPIVersions: + _, _ = w.Write(dataAPI8APIVersions) + case fmt.Sprintf(urlPathAPIEndpointsRoot, 8): + _, _ = w.Write(dataAPI8EndpointsRoot) + case fmt.Sprintf(urlPathAPIEndpointsHTTP, 8): + _, _ = w.Write(dataAPI8EndpointsHTTP) + case fmt.Sprintf(urlPathAPIEndpointsStream, 8): + _, _ = w.Write(dataAPI8EndpointsStream) + case fmt.Sprintf(urlPathAPIConnections, 8): + _, _ = w.Write(dataAPI8Connections) + case fmt.Sprintf(urlPathAPISSL, 8): + _, _ = w.Write(dataAPI8SSL) + case fmt.Sprintf(urlPathAPIHTTPRequests, 8): + _, _ = w.Write(dataAPI8HTTPRequests) + case fmt.Sprintf(urlPathAPIHTTPServerZones, 8): + _, _ = w.Write(dataAPI8HTTPServerZones) + case fmt.Sprintf(urlPathAPIHTTPLocationZones, 8): + _, _ = w.Write(dataAPI8HTTPLocationZones) + case fmt.Sprintf(urlPathAPIHTTPUpstreams, 8): + _, _ = w.Write(dataAPI8HTTPUpstreams) + case fmt.Sprintf(urlPathAPIHTTPCaches, 8): + _, _ = w.Write(dataAPI8HTTPCaches) + case fmt.Sprintf(urlPathAPIStreamServerZones, 8): + _, _ = w.Write(dataAPI8StreamServerZones) + case fmt.Sprintf(urlPathAPIStreamUpstreams, 8): + _, _ = w.Write(dataAPI8StreamUpstreams) + case fmt.Sprintf(urlPathAPIResolvers, 8): + _, _ = w.Write(dataAPI8Resolvers) + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write(data404) + + } + })) + nginx := New() + nginx.URL = srv.URL + require.NoError(t, nginx.Init()) + + return nginx, srv.Close +} + +func caseAPI8AllRequestsExceptStreamOK(t *testing.T) (*NginxPlus, func()) { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case urlPathAPIVersions: + _, _ = w.Write(dataAPI8APIVersions) + case fmt.Sprintf(urlPathAPIEndpointsRoot, 8): + _, _ = w.Write(dataAPI8EndpointsRoot) + case fmt.Sprintf(urlPathAPIEndpointsHTTP, 8): + _, _ = w.Write(dataAPI8EndpointsHTTP) + case fmt.Sprintf(urlPathAPIEndpointsStream, 8): + _, _ = w.Write(dataAPI8EndpointsStream) + case fmt.Sprintf(urlPathAPIConnections, 8): + _, _ = w.Write(dataAPI8Connections) + case fmt.Sprintf(urlPathAPISSL, 8): + _, _ = w.Write(dataAPI8SSL) + case fmt.Sprintf(urlPathAPIHTTPRequests, 8): + _, _ = w.Write(dataAPI8HTTPRequests) + case fmt.Sprintf(urlPathAPIHTTPServerZones, 8): + _, _ = w.Write(dataAPI8HTTPServerZones) + case fmt.Sprintf(urlPathAPIHTTPLocationZones, 8): + _, _ = w.Write(dataAPI8HTTPLocationZones) + case fmt.Sprintf(urlPathAPIHTTPUpstreams, 8): + _, _ = w.Write(dataAPI8HTTPUpstreams) + case fmt.Sprintf(urlPathAPIHTTPCaches, 8): + _, _ = w.Write(dataAPI8HTTPCaches) + case fmt.Sprintf(urlPathAPIResolvers, 8): + _, _ = w.Write(dataAPI8Resolvers) + default: + w.WriteHeader(http.StatusNotFound) + _, _ = w.Write(data404) + + } + })) + nginx := New() + nginx.URL = srv.URL + require.NoError(t, nginx.Init()) + + return nginx, srv.Close +} + +func caseInvalidDataResponse(t *testing.T) (*NginxPlus, func()) { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("hello and\n goodbye")) + })) + nginx := New() + nginx.URL = srv.URL + require.NoError(t, nginx.Init()) + + return nginx, srv.Close +} + +func caseConnectionRefused(t *testing.T) (*NginxPlus, func()) { + t.Helper() + nginx := New() + nginx.URL = "http://127.0.0.1:65001" + require.NoError(t, nginx.Init()) + + return nginx, func() {} +} + +func ensureCollectedHasAllChartsDimsVarsIDs(t *testing.T, n *NginxPlus, mx map[string]int64) { + for _, chart := range *n.Charts() { + if chart.ID == uptimeChart.ID { + continue + } + for _, dim := range chart.Dims { + _, ok := mx[dim.ID] + assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID) + } + for _, v := range chart.Vars { + _, ok := mx[v.ID] + assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID) + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/404.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/404.json new file mode 100644 index 000000000..d2ed8c9a8 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/404.json @@ -0,0 +1,9 @@ +{ + "error": { + "status": 404, + "text": "path not found", + "code": "PathNotFound" + }, + "request_id": "f0d20aca461d043e787ebaa52f018cb2", + "href": "https://nginx.org/en/docs/http/ngx_http_api_module.html" +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/api_versions.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/api_versions.json new file mode 100644 index 000000000..9ffc33973 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/api_versions.json @@ -0,0 +1,10 @@ +[ + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8 +] diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/connections.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/connections.json new file mode 100644 index 000000000..490ca13fc --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/connections.json @@ -0,0 +1,6 @@ +{ + "accepted": 6079, + "dropped": 0, + "active": 1, + "idle": 8 +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_http.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_http.json new file mode 100644 index 000000000..57c4e4aa2 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_http.json @@ -0,0 +1,10 @@ +[ + "requests", + "server_zones", + "location_zones", + "caches", + "limit_conns", + "limit_reqs", + "keyvals", + "upstreams" +] diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_root.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_root.json new file mode 100644 index 000000000..b185c55f2 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_root.json @@ -0,0 +1,10 @@ +[ + "nginx", + "processes", + "connections", + "slabs", + "http", + "stream", + "resolvers", + "ssl" +] diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_stream.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_stream.json new file mode 100644 index 000000000..0da092376 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_stream.json @@ -0,0 +1,6 @@ +[ + "server_zones", + "limit_conns", + "keyvals", + "upstreams" +] diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_caches.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_caches.json new file mode 100644 index 000000000..dd2d03adf --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_caches.json @@ -0,0 +1,40 @@ +{ + "cache_backend": { + "size": 0, + "cold": false, + "hit": { + "responses": 0, + "bytes": 0 + }, + "stale": { + "responses": 0, + "bytes": 0 + }, + "updating": { + "responses": 0, + "bytes": 0 + }, + "revalidated": { + "responses": 0, + "bytes": 0 + }, + "miss": { + "responses": 109, + "bytes": 67035, + "responses_written": 0, + "bytes_written": 0 + }, + "expired": { + "responses": 0, + "bytes": 0, + "responses_written": 0, + "bytes_written": 0 + }, + "bypass": { + "responses": 0, + "bytes": 0, + "responses_written": 0, + "bytes_written": 0 + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_location_zones.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_location_zones.json new file mode 100644 index 000000000..8812e6dff --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_location_zones.json @@ -0,0 +1,35 @@ +{ + "server_api": { + "requests": 9188, + "responses": { + "1xx": 0, + "2xx": 9187, + "3xx": 0, + "4xx": 1, + "5xx": 0, + "codes": { + "200": 9187, + "404": 1 + }, + "total": 9188 + }, + "discarded": 0, + "received": 1854427, + "sent": 4668778 + }, + "server_dashboard": { + "requests": 0, + "responses": { + "1xx": 0, + "2xx": 0, + "3xx": 0, + "4xx": 0, + "5xx": 0, + "codes": {}, + "total": 0 + }, + "discarded": 0, + "received": 0, + "sent": 0 + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_requests.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_requests.json new file mode 100644 index 000000000..0c2a17503 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_requests.json @@ -0,0 +1,4 @@ +{ + "total": 8363, + "current": 1 +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_server_zones.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_server_zones.json new file mode 100644 index 000000000..c25389210 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_server_zones.json @@ -0,0 +1,21 @@ +{ + "server_backend": { + "processing": 1, + "requests": 8962, + "responses": { + "1xx": 0, + "2xx": 8960, + "3xx": 0, + "4xx": 1, + "5xx": 0, + "codes": { + "200": 8960, + "404": 1 + }, + "total": 8961 + }, + "discarded": 0, + "received": 1773834, + "sent": 4585734 + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_upstreams.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_upstreams.json new file mode 100644 index 000000000..0f7ba7135 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_upstreams.json @@ -0,0 +1,76 @@ +{ + "backend": { + "peers": [ + { + "id": 0, + "server": "127.0.0.1:81", + "name": "127.0.0.1:81", + "backup": false, + "weight": 5, + "state": "unavail", + "active": 0, + "requests": 26, + "header_time": 0, + "response_time": 0, + "responses": { + "1xx": 0, + "2xx": 0, + "3xx": 0, + "4xx": 0, + "5xx": 0, + "codes": {}, + "total": 0 + }, + "sent": 0, + "received": 0, + "fails": 26, + "unavail": 1, + "health_checks": { + "checks": 0, + "fails": 0, + "unhealthy": 0 + }, + "downtime": 1020702, + "downstart": "2022-11-18T19:17:09.258Z", + "selected": "2022-11-18T19:33:50Z" + }, + { + "id": 1, + "server": "127.0.0.1:82", + "name": "127.0.0.1:82", + "backup": false, + "weight": 1, + "state": "up", + "active": 0, + "requests": 102, + "header_time": 1, + "response_time": 1, + "responses": { + "1xx": 0, + "2xx": 102, + "3xx": 0, + "4xx": 0, + "5xx": 0, + "codes": { + "200": 102 + }, + "total": 102 + }, + "sent": 9180, + "received": 86496, + "fails": 0, + "unavail": 0, + "health_checks": { + "checks": 0, + "fails": 0, + "unhealthy": 0 + }, + "downtime": 0, + "selected": "2022-11-18T19:34:00Z" + } + ], + "keepalive": 0, + "zombies": 0, + "zone": "http_backend" + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/nginx.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/nginx.json new file mode 100644 index 000000000..4480c2bcc --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/nginx.json @@ -0,0 +1,10 @@ +{ + "version": "1.21.6", + "build": "nginx-plus-r27-p1", + "address": "127.0.0.1", + "generation": 1, + "load_timestamp": "2022-11-19T14:38:38.676Z", + "timestamp": "2022-11-19T14:38:57.031Z", + "pid": 2254633, + "ppid": 2254629 +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/resolvers.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/resolvers.json new file mode 100644 index 000000000..ad66f5584 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/resolvers.json @@ -0,0 +1,36 @@ +{ + "resolver-http": { + "requests": { + "name": 0, + "srv": 2939408, + "addr": 0 + }, + "responses": { + "noerror": 0, + "formerr": 0, + "servfail": 0, + "nxdomain": 2939404, + "notimp": 0, + "refused": 0, + "timedout": 4, + "unknown": 0 + } + }, + "resolver-stream": { + "requests": { + "name": 638797, + "srv": 0, + "addr": 0 + }, + "responses": { + "noerror": 433136, + "formerr": 0, + "servfail": 0, + "nxdomain": 40022, + "notimp": 0, + "refused": 165639, + "timedout": 0, + "unknown": 0 + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/ssl.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/ssl.json new file mode 100644 index 000000000..2ca8a6a3e --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/ssl.json @@ -0,0 +1,16 @@ +{ + "handshakes": 15804607, + "session_reuses": 13096060, + "handshakes_failed": 37862, + "no_common_protocol": 16648, + "no_common_cipher": 24, + "handshake_timeout": 4, + "peer_rejected_cert": 0, + "verify_failures": { + "no_cert": 0, + "expired_cert": 0, + "revoked_cert": 0, + "hostname_mismatch": 0, + "other": 0 + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/stream_server_zones.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/stream_server_zones.json new file mode 100644 index 000000000..0c7df7873 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/stream_server_zones.json @@ -0,0 +1,15 @@ +{ + "tcp_server": { + "processing": 0, + "connections": 0, + "sessions": { + "2xx": 0, + "4xx": 0, + "5xx": 0, + "total": 0 + }, + "discarded": 0, + "received": 0, + "sent": 0 + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/stream_upstreams.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/stream_upstreams.json new file mode 100644 index 000000000..707ad4db7 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/stream_upstreams.json @@ -0,0 +1,48 @@ +{ + "stream_backend": { + "peers": [ + { + "id": 0, + "server": "127.0.0.1:12346", + "name": "127.0.0.1:12346", + "backup": false, + "weight": 1, + "state": "up", + "active": 0, + "connections": 0, + "sent": 0, + "received": 0, + "fails": 0, + "unavail": 0, + "health_checks": { + "checks": 0, + "fails": 0, + "unhealthy": 0 + }, + "downtime": 0 + }, + { + "id": 1, + "server": "127.0.0.1:12347", + "name": "127.0.0.1:12347", + "backup": false, + "weight": 1, + "state": "up", + "active": 0, + "connections": 0, + "sent": 0, + "received": 0, + "fails": 0, + "unavail": 0, + "health_checks": { + "checks": 0, + "fails": 0, + "unhealthy": 0 + }, + "downtime": 0 + } + ], + "zombies": 0, + "zone": "tcp_servers" + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/config.json b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/config.json new file mode 100644 index 000000000..984c3ed6e --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/config.json @@ -0,0 +1,20 @@ +{ + "update_every": 123, + "url": "ok", + "body": "ok", + "method": "ok", + "headers": { + "ok": "ok" + }, + "username": "ok", + "password": "ok", + "proxy_url": "ok", + "proxy_username": "ok", + "proxy_password": "ok", + "timeout": 123.123, + "not_follow_redirects": true, + "tls_ca": "ok", + "tls_cert": "ok", + "tls_key": "ok", + "tls_skip_verify": true +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/config.yaml new file mode 100644 index 000000000..8558b61cc --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxplus/testdata/config.yaml @@ -0,0 +1,17 @@ +update_every: 123 +url: "ok" +body: "ok" +method: "ok" +headers: + ok: "ok" +username: "ok" +password: "ok" +proxy_url: "ok" +proxy_username: "ok" +proxy_password: "ok" +timeout: 123.123 +not_follow_redirects: yes +tls_ca: "ok" +tls_cert: "ok" +tls_key: "ok" +tls_skip_verify: yes diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/README.md b/src/go/collectors/go.d.plugin/modules/nginxvts/README.md new file mode 120000 index 000000000..e185fa81b --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/README.md @@ -0,0 +1 @@ +integrations/nginx_vts.md
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/charts.go b/src/go/collectors/go.d.plugin/modules/nginxvts/charts.go new file mode 100644 index 000000000..6fc859ed5 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/charts.go @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxvts + +import "github.com/netdata/netdata/go/go.d.plugin/agent/module" + +var mainCharts = module.Charts{ + { + ID: "requests", + Title: "Total requests", + Units: "requests/s", + Fam: "requests", + Ctx: "nginxvts.requests_total", + Dims: module.Dims{ + {ID: "connections_requests", Name: "requests", Algo: module.Incremental}, + }, + }, + { + ID: "active_connections", + Title: "Active connections", + Units: "connections", + Fam: "connections", + Ctx: "nginxvts.active_connections", + Dims: module.Dims{ + {ID: "connections_active", Name: "active"}, + }, + }, + { + ID: "connections", + Title: "Total connections", + Units: "connections/s", + Fam: "connections", + Ctx: "nginxvts.connections_total", + Dims: module.Dims{ + {ID: "connections_reading", Name: "reading", Algo: module.Incremental}, + {ID: "connections_writing", Name: "writing", Algo: module.Incremental}, + {ID: "connections_waiting", Name: "waiting", Algo: module.Incremental}, + {ID: "connections_accepted", Name: "accepted", Algo: module.Incremental}, + {ID: "connections_handled", Name: "handled", Algo: module.Incremental}, + }, + }, + { + ID: "uptime", + Title: "Uptime", + Units: "seconds", + Fam: "uptime", + Ctx: "nginxvts.uptime", + Dims: module.Dims{ + {ID: "uptime", Name: "uptime"}, + }, + }, +} +var sharedZonesCharts = module.Charts{ + { + ID: "shared_memory_size", + Title: "Shared memory size", + Units: "bytes", + Fam: "shared memory", + Ctx: "nginxvts.shm_usage", + Dims: module.Dims{ + {ID: "sharedzones_maxsize", Name: "max"}, + {ID: "sharedzones_usedsize", Name: "used"}, + }, + }, + { + ID: "shared_memory_used_node", + Title: "Number of node using shared memory", + Units: "nodes", + Fam: "shared memory", + Ctx: "nginxvts.shm_used_node", + Dims: module.Dims{ + {ID: "sharedzones_usednode", Name: "used"}, + }, + }, +} + +var serverZonesCharts = module.Charts{ + { + ID: "server_requests_total", + Title: "Total number of client requests", + Units: "requests/s", + Fam: "serverzones", + Ctx: "nginxvts.server_requests_total", + Dims: module.Dims{ + {ID: "total_requestcounter", Name: "requests", Algo: module.Incremental}, + }, + }, + { + ID: "server_responses_total", + Title: "Total number of responses by code class", + Units: "responses/s", + Fam: "serverzones", + Ctx: "nginxvts.server_responses_total", + Dims: module.Dims{ + {ID: "total_responses_1xx", Name: "1xx", Algo: module.Incremental}, + {ID: "total_responses_2xx", Name: "2xx", Algo: module.Incremental}, + {ID: "total_responses_3xx", Name: "3xx", Algo: module.Incremental}, + {ID: "total_responses_4xx", Name: "4xx", Algo: module.Incremental}, + {ID: "total_responses_5xx", Name: "5xx", Algo: module.Incremental}, + }, + }, + { + ID: "server_traffic_total", + Title: "Total amount of data transferred to and from the server", + Units: "bytes/s", + Fam: "serverzones", + Ctx: "nginxvts.server_traffic_total", + Dims: module.Dims{ + {ID: "total_inbytes", Name: "in", Algo: module.Incremental}, + {ID: "total_outbytes", Name: "out", Algo: module.Incremental}, + }, + }, + { + ID: "server_cache_total", + Title: "Total server cache", + Units: "events/s", + Fam: "serverzones", + Ctx: "nginxvts.server_cache_total", + Dims: module.Dims{ + {ID: "total_cache_miss", Name: "miss", Algo: module.Incremental}, + {ID: "total_cache_bypass", Name: "bypass", Algo: module.Incremental}, + {ID: "total_cache_expired", Name: "expired", Algo: module.Incremental}, + {ID: "total_cache_stale", Name: "stale", Algo: module.Incremental}, + {ID: "total_cache_updating", Name: "updating", Algo: module.Incremental}, + {ID: "total_cache_revalidated", Name: "revalidated", Algo: module.Incremental}, + {ID: "total_cache_hit", Name: "hit", Algo: module.Incremental}, + {ID: "total_cache_scarce", Name: "scarce", Algo: module.Incremental}, + }, + }, +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/collect.go b/src/go/collectors/go.d.plugin/modules/nginxvts/collect.go new file mode 100644 index 000000000..c4c389682 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/collect.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxvts + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/stm" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +func (vts *NginxVTS) collect() (map[string]int64, error) { + ms, err := vts.scapeVTS() + if err != nil { + return nil, nil + } + + collected := make(map[string]interface{}) + vts.collectMain(collected, ms) + vts.collectSharedZones(collected, ms) + vts.collectServerZones(collected, ms) + + return stm.ToMap(collected), nil +} + +func (vts *NginxVTS) collectMain(collected map[string]interface{}, ms *vtsMetrics) { + collected["uptime"] = (ms.NowMsec - ms.LoadMsec) / 1000 + collected["connections"] = ms.Connections +} + +func (vts *NginxVTS) collectSharedZones(collected map[string]interface{}, ms *vtsMetrics) { + collected["sharedzones"] = ms.SharedZones +} + +func (vts *NginxVTS) collectServerZones(collected map[string]interface{}, ms *vtsMetrics) { + if !ms.hasServerZones() { + return + } + + // "*" means all servers + collected["total"] = ms.ServerZones["*"] +} + +func (vts *NginxVTS) scapeVTS() (*vtsMetrics, error) { + req, _ := web.NewHTTPRequest(vts.Request) + + var total vtsMetrics + + if err := vts.doOKDecode(req, &total); err != nil { + vts.Warning(err) + return nil, err + } + return &total, nil +} + +func (vts *NginxVTS) doOKDecode(req *http.Request, in interface{}) error { + resp, err := vts.httpClient.Do(req) + if err != nil { + return fmt.Errorf("error on HTTP request '%s': %v", req.URL, err) + } + defer closeBody(resp) + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("'%s' returned HTTP status code: %d", req.URL, resp.StatusCode) + } + + if err := json.NewDecoder(resp.Body).Decode(in); err != nil { + return fmt.Errorf("error on decoding response from '%s': %v", req.URL, err) + } + return nil +} + +func closeBody(resp *http.Response) { + if resp != nil && resp.Body != nil { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/config_schema.json b/src/go/collectors/go.d.plugin/modules/nginxvts/config_schema.json new file mode 100644 index 000000000..1abcdb658 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/config_schema.json @@ -0,0 +1,176 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NGINX VTS module collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "url": { + "title": "URL", + "description": "The URL of the NGINX VTS [module status page](https://github.com/vozlt/nginx-module-vts#readme).", + "type": "string", + "default": "http://localhost/status/format/json" + }, + "timeout": { + "title": "Timeout", + "description": "The timeout in seconds for the HTTP request.", + "type": "number", + "minimum": 0.5, + "default": 1 + }, + "not_follow_redirects": { + "title": "Not follow redirects", + "description": "If set, the client will not follow HTTP redirects automatically.", + "type": "boolean" + }, + "username": { + "title": "Username", + "description": "The username for basic authentication.", + "type": "string", + "sensitive": true + }, + "password": { + "title": "Password", + "description": "The password for basic authentication.", + "type": "string", + "sensitive": true + }, + "proxy_url": { + "title": "Proxy URL", + "description": "The URL of the proxy server.", + "type": "string" + }, + "proxy_username": { + "title": "Proxy username", + "description": "The username for proxy authentication.", + "type": "string", + "sensitive": true + }, + "proxy_password": { + "title": "Proxy password", + "description": "The password for proxy authentication.", + "type": "string", + "sensitive": true + }, + "headers": { + "title": "Headers", + "description": "Additional HTTP headers to include in the request.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "tls_skip_verify": { + "title": "Skip TLS verification", + "description": "If set, TLS certificate verification will be skipped.", + "type": "boolean" + }, + "tls_ca": { + "title": "TLS CA", + "description": "The path to the CA certificate file for TLS verification.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_cert": { + "title": "TLS certificate", + "description": "The path to the client certificate file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_key": { + "title": "TLS key", + "description": "The path to the client key file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + }, + "body": { + "title": "Body", + "type": "string" + }, + "method": { + "title": "Method", + "type": "string" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "ui:flavour": "tabs", + "ui:options": { + "tabs": [ + { + "title": "Base", + "fields": [ + "update_every", + "url", + "timeout", + "not_follow_redirects" + ] + }, + { + "title": "Auth", + "fields": [ + "username", + "password" + ] + }, + { + "title": "TLS", + "fields": [ + "tls_skip_verify", + "tls_ca", + "tls_cert", + "tls_key" + ] + }, + { + "title": "Proxy", + "fields": [ + "proxy_url", + "proxy_username", + "proxy_password" + ] + }, + { + "title": "Headers", + "fields": [ + "headers" + ] + } + ] + }, + "uiOptions": { + "fullPage": true + }, + "body": { + "ui:widget": "hidden" + }, + "method": { + "ui:widget": "hidden" + }, + "timeout": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + }, + "password": { + "ui:widget": "password" + }, + "proxy_password": { + "ui:widget": "password" + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/init.go b/src/go/collectors/go.d.plugin/modules/nginxvts/init.go new file mode 100644 index 000000000..17ff63020 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/init.go @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxvts + +import ( + "errors" + "net/http" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +func (vts *NginxVTS) validateConfig() error { + if vts.URL == "" { + return errors.New("URL not set") + } + + if _, err := web.NewHTTPRequest(vts.Request); err != nil { + return err + } + return nil +} + +func (vts *NginxVTS) initHTTPClient() (*http.Client, error) { + return web.NewHTTPClient(vts.Client) +} + +func (vts *NginxVTS) initCharts() (*module.Charts, error) { + charts := module.Charts{} + + if err := charts.Add(*mainCharts.Copy()...); err != nil { + return nil, err + } + + if err := charts.Add(*sharedZonesCharts.Copy()...); err != nil { + return nil, err + } + + if err := charts.Add(*serverZonesCharts.Copy()...); err != nil { + return nil, err + } + + if len(charts) == 0 { + return nil, errors.New("zero charts") + } + return &charts, nil +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/integrations/nginx_vts.md b/src/go/collectors/go.d.plugin/modules/nginxvts/integrations/nginx_vts.md new file mode 100644 index 000000000..cc1f30475 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/integrations/nginx_vts.md @@ -0,0 +1,233 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/nginxvts/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/nginxvts/metadata.yaml" +sidebar_label: "NGINX VTS" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/Web Servers and Web Proxies" +most_popular: True +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# NGINX VTS + + +<img src="https://netdata.cloud/img/nginx.svg" width="150"/> + + +Plugin: go.d.plugin +Module: nginxvts + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors NGINX servers with [virtual host traffic status module](https://github.com/vozlt/nginx-module-vts). + + +It sends HTTP requests to the NGINX VTS location [status](https://github.com/vozlt/nginx-module-vts#synopsis), +which is a built-in location that provides metrics about the NGINX VTS server. + + +This collector is supported on all platforms. + +This collector supports collecting metrics from multiple instances of this integration, including remote instances. + + +### Default Behavior + +#### Auto-Detection + +By default, it detects NGINX instances running on localhost. + + +#### Limits + +The default configuration for this integration does not impose any limits on data collection. + +#### Performance Impact + +The default configuration for this integration is not expected to impose a significant performance impact on the system. + + +## Metrics + +Metrics grouped by *scope*. + +The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels. + + + +### Per NGINX VTS instance + +These metrics refer to the entire monitored application. + +This scope has no labels. + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| nginxvts.requests_total | requests | requests/s | +| nginxvts.active_connections | active | connections | +| nginxvts.connections_total | reading, writing, waiting, accepted, handled | connections/s | +| nginxvts.uptime | uptime | seconds | +| nginxvts.shm_usage | max, used | bytes | +| nginxvts.shm_used_node | used | nodes | +| nginxvts.server_requests_total | requests | requests/s | +| nginxvts.server_responses_total | 1xx, 2xx, 3xx, 4xx, 5xx | responses/s | +| nginxvts.server_traffic_total | in, out | bytes/s | +| nginxvts.server_cache_total | miss, bypass, expired, stale, updating, revalidated, hit, scarce | events/s | + + + +## Alerts + +There are no alerts configured by default for this integration. + + +## Setup + +### Prerequisites + +#### Configure nginx-vts module + +To configure nginx-vts, see the [https://github.com/vozlt/nginx-module-vts#installation). + + + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/nginxvts.conf`. + + +You can edit the configuration file using the `edit-config` script from the +Netdata [config directory](/docs/netdata-agent/configuration/README.md#the-netdata-config-directory). + +```bash +cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata +sudo ./edit-config go.d/nginxvts.conf +``` +#### Options + +The following options can be defined globally: update_every, autodetection_retry. + + +<details open><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 1 | no | +| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no | +| url | Server URL. | http://127.0.0.1/status/format/json | yes | +| timeout | HTTP request timeout. | 1 | no | +| username | Username for basic HTTP authentication. | | no | +| password | Password for basic HTTP authentication. | | no | +| proxy_url | Proxy URL. | | no | +| proxy_username | Username for proxy basic HTTP authentication. | | no | +| proxy_password | Password for proxy basic HTTP authentication. | | no | +| method | HTTP request method. | GET | no | +| body | HTTP request body. | | no | +| headers | HTTP request headers. | | no | +| not_follow_redirects | Redirect handling policy. Controls whether the client follows redirects. | no | no | +| tls_skip_verify | Server certificate chain and hostname validation policy. Controls whether the client performs this check. | no | no | +| tls_ca | Certification authority that the client uses when verifying the server's certificates. | | no | +| tls_cert | Client TLS certificate. | | no | +| tls_key | Client TLS key. | | no | + +</details> + +#### Examples + +##### Basic + +A basic example configuration. + +```yaml +jobs: + - name: local + url: http://127.0.0.1/status/format/json + +``` +##### HTTP authentication + +Basic HTTP authentication. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1/server-status?auto + username: username + password: password + +``` +</details> + +##### HTTPS with self-signed certificate + +Do not validate server certificate chain and hostname. + + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: https://127.0.0.1/status/format/json + tls_skip_verify: yes + +``` +</details> + +##### Multi-instance + +> **Note**: When you define multiple jobs, their names must be unique. + +Collecting metrics from local and remote instances. + + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1/status/format/json + + - name: remote + url: http://192.0.2.1/status/format/json + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +To troubleshoot issues with the `nginxvts` collector, run the `go.d.plugin` with the debug option enabled. The output +should give you clues as to why the collector isn't working. + +- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on + your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`. + + ```bash + cd /usr/libexec/netdata/plugins.d/ + ``` + +- Switch to the `netdata` user. + + ```bash + sudo -u netdata -s + ``` + +- Run the `go.d.plugin` to debug the collector: + + ```bash + ./go.d.plugin -d -m nginxvts + ``` + + diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/metadata.yaml b/src/go/collectors/go.d.plugin/modules/nginxvts/metadata.yaml new file mode 100644 index 000000000..bb602863b --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/metadata.yaml @@ -0,0 +1,264 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-nginxvts + plugin_name: go.d.plugin + module_name: nginxvts + monitored_instance: + name: NGINX VTS + link: https://www.nginx.com/ + icon_filename: nginx.svg + categories: + - data-collection.web-servers-and-web-proxies + keywords: + - webserver + related_resources: + integrations: + list: + - plugin_name: go.d.plugin + module_name: weblog + - plugin_name: go.d.plugin + module_name: httpcheck + - plugin_name: apps.plugin + module_name: apps + info_provided_to_referring_integrations: + description: "" + most_popular: true + overview: + data_collection: + metrics_description: | + This collector monitors NGINX servers with [virtual host traffic status module](https://github.com/vozlt/nginx-module-vts). + method_description: | + It sends HTTP requests to the NGINX VTS location [status](https://github.com/vozlt/nginx-module-vts#synopsis), + which is a built-in location that provides metrics about the NGINX VTS server. + supported_platforms: + include: [] + exclude: [] + multi_instance: true + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: | + By default, it detects NGINX instances running on localhost. + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: + - title: Configure nginx-vts module + description: | + To configure nginx-vts, see the [https://github.com/vozlt/nginx-module-vts#installation). + configuration: + file: + name: go.d/nginxvts.conf + options: + description: | + The following options can be defined globally: update_every, autodetection_retry. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 1 + required: false + - name: autodetection_retry + description: Recheck interval in seconds. Zero means no recheck will be scheduled. + default_value: 0 + required: false + - name: url + description: Server URL. + default_value: http://127.0.0.1/status/format/json + required: true + - name: timeout + description: HTTP request timeout. + default_value: 1 + required: false + - name: username + description: Username for basic HTTP authentication. + default_value: "" + required: false + - name: password + description: Password for basic HTTP authentication. + default_value: "" + required: false + - name: proxy_url + description: Proxy URL. + default_value: "" + required: false + - name: proxy_username + description: Username for proxy basic HTTP authentication. + default_value: "" + required: false + - name: proxy_password + description: Password for proxy basic HTTP authentication. + default_value: "" + required: false + - name: method + description: HTTP request method. + default_value: GET + required: false + - name: body + description: HTTP request body. + default_value: "" + required: false + - name: headers + description: HTTP request headers. + default_value: "" + required: false + - name: not_follow_redirects + description: Redirect handling policy. Controls whether the client follows redirects. + default_value: no + required: false + - name: tls_skip_verify + description: Server certificate chain and hostname validation policy. Controls whether the client performs this check. + default_value: no + required: false + - name: tls_ca + description: Certification authority that the client uses when verifying the server's certificates. + default_value: "" + required: false + - name: tls_cert + description: Client TLS certificate. + default_value: "" + required: false + - name: tls_key + description: Client TLS key. + default_value: "" + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Basic + folding: + enabled: false + description: A basic example configuration. + config: | + jobs: + - name: local + url: http://127.0.0.1/status/format/json + - name: HTTP authentication + description: Basic HTTP authentication. + config: | + jobs: + - name: local + url: http://127.0.0.1/server-status?auto + username: username + password: password + - name: HTTPS with self-signed certificate + description: | + Do not validate server certificate chain and hostname. + config: | + jobs: + - name: local + url: https://127.0.0.1/status/format/json + tls_skip_verify: yes + - name: Multi-instance + description: | + > **Note**: When you define multiple jobs, their names must be unique. + + Collecting metrics from local and remote instances. + config: | + jobs: + - name: local + url: http://127.0.0.1/status/format/json + + - name: remote + url: http://192.0.2.1/status/format/json + troubleshooting: + problems: + list: [] + alerts: [] + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: global + description: These metrics refer to the entire monitored application. + labels: [] + metrics: + - name: nginxvts.requests_total + description: Total requests + unit: requests/s + chart_type: line + dimensions: + - name: requests + - name: nginxvts.active_connections + description: Active connections + unit: connections + chart_type: line + dimensions: + - name: active + - name: nginxvts.connections_total + description: Total connections + unit: connections/s + chart_type: line + dimensions: + - name: reading + - name: writing + - name: waiting + - name: accepted + - name: handled + - name: nginxvts.uptime + description: Uptime + unit: seconds + chart_type: line + dimensions: + - name: uptime + - name: nginxvts.shm_usage + description: Shared memory size + unit: bytes + chart_type: line + dimensions: + - name: max + - name: used + - name: nginxvts.shm_used_node + description: Number of node using shared memory + unit: nodes + chart_type: line + dimensions: + - name: used + - name: nginxvts.server_requests_total + description: Total number of client requests + unit: requests/s + chart_type: line + dimensions: + - name: requests + - name: nginxvts.server_responses_total + description: Total number of responses by code class + unit: responses/s + chart_type: line + dimensions: + - name: 1xx + - name: 2xx + - name: 3xx + - name: 4xx + - name: 5xx + - name: nginxvts.server_traffic_total + description: Total amount of data transferred to and from the server + unit: bytes/s + chart_type: line + dimensions: + - name: in + - name: out + - name: nginxvts.server_cache_total + description: Total server cache + unit: events/s + chart_type: line + dimensions: + - name: miss + - name: bypass + - name: expired + - name: stale + - name: updating + - name: revalidated + - name: hit + - name: scarce diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/metrics.go b/src/go/collectors/go.d.plugin/modules/nginxvts/metrics.go new file mode 100644 index 000000000..2674d4bbe --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/metrics.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxvts + +// NginxVTS metrics: https://github.com/vozlt/nginx-module-vts#json + +type vtsMetrics struct { + // HostName string + // NginxVersion string + LoadMsec int64 + NowMsec int64 + Uptime int64 + Connections struct { + Active int64 `stm:"active"` + Reading int64 `stm:"reading"` + Writing int64 `stm:"writing"` + Waiting int64 `stm:"waiting"` + Accepted int64 `stm:"accepted"` + Handled int64 `stm:"handled"` + Requests int64 `stm:"requests"` + } `stm:"connections"` + SharedZones struct { + // Name string + MaxSize int64 `stm:"maxsize"` + UsedSize int64 `stm:"usedsize"` + UsedNode int64 `stm:"usednode"` + } + ServerZones map[string]Server +} + +func (m vtsMetrics) hasServerZones() bool { return m.ServerZones != nil } + +// Server is for total Nginx server +type Server struct { + RequestCounter int64 `stm:"requestcounter"` + InBytes int64 `stm:"inbytes"` + OutBytes int64 `stm:"outbytes"` + Responses struct { + Resp1xx int64 `stm:"responses_1xx" json:"1xx"` + Resp2xx int64 `stm:"responses_2xx" json:"2xx"` + Resp3xx int64 `stm:"responses_3xx" json:"3xx"` + Resp4xx int64 `stm:"responses_4xx" json:"4xx"` + Resp5xx int64 `stm:"responses_5xx" json:"5xx"` + Miss int64 `stm:"cache_miss"` + Bypass int64 `stm:"cache_bypass"` + Expired int64 `stm:"cache_expired"` + Stale int64 `stm:"cache_stale"` + Updating int64 `stm:"cache_updating"` + Revalidated int64 `stm:"cache_revalidated"` + Hit int64 `stm:"cache_hit"` + Scarce int64 `stm:"cache_scarce"` + } `stm:""` +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/nginxvts.go b/src/go/collectors/go.d.plugin/modules/nginxvts/nginxvts.go new file mode 100644 index 000000000..ad3aaf1e7 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/nginxvts.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxvts + +import ( + _ "embed" + "errors" + "net/http" + "time" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("nginxvts", module.Creator{ + JobConfigSchema: configSchema, + Defaults: module.Defaults{ + UpdateEvery: 1, + }, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *NginxVTS { + return &NginxVTS{ + Config: Config{ + HTTP: web.HTTP{ + Request: web.Request{ + URL: "http://localhost/status/format/json", + }, + Client: web.Client{ + Timeout: web.Duration(time.Second), + }, + }, + }, + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + web.HTTP `yaml:",inline" json:""` +} + +type NginxVTS struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + httpClient *http.Client +} + +func (vts *NginxVTS) Configuration() any { + return vts.Config +} + +func (vts *NginxVTS) Cleanup() { + if vts.httpClient == nil { + return + } + vts.httpClient.CloseIdleConnections() +} + +func (vts *NginxVTS) Init() error { + err := vts.validateConfig() + if err != nil { + vts.Errorf("check configuration: %v", err) + return err + } + + httpClient, err := vts.initHTTPClient() + if err != nil { + vts.Errorf("init HTTP client: %v", err) + } + vts.httpClient = httpClient + + charts, err := vts.initCharts() + if err != nil { + vts.Errorf("init charts: %v", err) + return err + } + vts.charts = charts + + return nil +} + +func (vts *NginxVTS) Check() error { + mx, err := vts.collect() + if err != nil { + vts.Error(err) + return err + } + if len(mx) == 0 { + return errors.New("no metrics collected") + } + return nil +} + +func (vts *NginxVTS) Charts() *module.Charts { + return vts.charts +} + +func (vts *NginxVTS) Collect() map[string]int64 { + mx, err := vts.collect() + if err != nil { + vts.Error(err) + return nil + } + if len(mx) == 0 { + return nil + } + return mx +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/nginxvts_test.go b/src/go/collectors/go.d.plugin/modules/nginxvts/nginxvts_test.go new file mode 100644 index 000000000..b9140c069 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/nginxvts_test.go @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package nginxvts + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/netdata/netdata/go/go.d.plugin/pkg/tlscfg" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") + + dataVer0118Response, _ = os.ReadFile("testdata/vts-v0.1.18.json") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + "dataVer0118Response": dataVer0118Response, + } { + require.NotNil(t, data, name) + } +} + +func TestNginxVTS_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &NginxVTS{}, dataConfigJSON, dataConfigYAML) +} + +func TestNginxVTS_Init(t *testing.T) { + tests := map[string]struct { + config Config + wantNumOfCharts int + wantFail bool + }{ + "default": { + wantNumOfCharts: numOfCharts( + mainCharts, + sharedZonesCharts, + serverZonesCharts, + ), + config: New().Config, + }, + "URL not set": { + wantFail: true, + config: Config{ + HTTP: web.HTTP{ + Request: web.Request{URL: ""}, + }}, + }, + "invalid TLSCA": { + wantFail: true, + config: Config{ + HTTP: web.HTTP{ + Client: web.Client{ + TLSConfig: tlscfg.TLSConfig{TLSCA: "testdata/tls"}, + }, + }}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + es := New() + es.Config = test.config + + if test.wantFail { + assert.Error(t, es.Init()) + } else { + assert.NoError(t, es.Init()) + assert.Equal(t, test.wantNumOfCharts, len(*es.Charts())) + } + }) + } +} + +func TestNginxVTS_Check(t *testing.T) { + tests := map[string]struct { + prepare func(*testing.T) (vts *NginxVTS, cleanup func()) + wantFail bool + }{ + "valid data": {prepare: prepareNginxVTSValidData}, + "invalid data": {prepare: prepareNginxVTSInvalidData, wantFail: true}, + "404": {prepare: prepareNginxVTS404, wantFail: true}, + "connection refused": {prepare: prepareNginxVTSConnectionRefused, wantFail: true}, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + vts, cleanup := test.prepare(t) + defer cleanup() + + if test.wantFail { + assert.Error(t, vts.Check()) + } else { + assert.NoError(t, vts.Check()) + } + }) + } +} + +func TestNginxVTS_Charts(t *testing.T) { + assert.Nil(t, New().Charts()) +} + +func TestNginxVTS_Cleanup(t *testing.T) { + assert.NotPanics(t, New().Cleanup) +} + +func TestNginxVTS_Collect(t *testing.T) { + tests := map[string]struct { + // prepare func() *NginxVTS + prepare func(t *testing.T) (vts *NginxVTS, cleanup func()) + wantCollected map[string]int64 + checkCharts bool + }{ + "right metrics": { + prepare: prepareNginxVTSValidData, + wantCollected: map[string]int64{ + // Nginx running time + "uptime": 319, + // Nginx connections + "connections_active": 2, + "connections_reading": 0, + "connections_writing": 1, + "connections_waiting": 1, + "connections_accepted": 12, + "connections_handled": 12, + "connections_requests": 17, + // Nginx shared memory + "sharedzones_maxsize": 1048575, + "sharedzones_usedsize": 45799, + "sharedzones_usednode": 13, + // Nginx traffic + "total_requestcounter": 2, + "total_inbytes": 156, + "total_outbytes": 692, + // Nginx response code + "total_responses_1xx": 1, + "total_responses_2xx": 2, + "total_responses_3xx": 3, + "total_responses_4xx": 4, + "total_responses_5xx": 5, + // Nginx cache + "total_cache_miss": 2, + "total_cache_bypass": 4, + "total_cache_expired": 6, + "total_cache_stale": 8, + "total_cache_updating": 10, + "total_cache_revalidated": 12, + "total_cache_hit": 14, + "total_cache_scarce": 16, + }, + checkCharts: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + vts, cleanup := test.prepare(t) + defer cleanup() + + collected := vts.Collect() + + assert.Equal(t, test.wantCollected, collected) + if test.checkCharts { + ensureCollectedHasAllChartsDimsVarsIDs(t, vts, collected) + } + }) + } +} + +func ensureCollectedHasAllChartsDimsVarsIDs(t *testing.T, vts *NginxVTS, collected map[string]int64) { + for _, chart := range *vts.Charts() { + if chart.Obsolete { + continue + } + for _, dim := range chart.Dims { + _, ok := collected[dim.ID] + assert.Truef(t, ok, "collected metrics has no data for dim '%s' chart '%s'", dim.ID, chart.ID) + } + for _, v := range chart.Vars { + _, ok := collected[v.ID] + assert.Truef(t, ok, "collected metrics has no data for var '%s' chart '%s'", v.ID, chart.ID) + } + } +} + +func prepareNginxVTS(t *testing.T, createNginxVTS func() *NginxVTS) (vts *NginxVTS, cleanup func()) { + t.Helper() + vts = createNginxVTS() + srv := prepareNginxVTSEndpoint() + vts.URL = srv.URL + + require.NoError(t, vts.Init()) + + return vts, srv.Close +} + +func prepareNginxVTSValidData(t *testing.T) (vts *NginxVTS, cleanup func()) { + return prepareNginxVTS(t, New) +} + +func prepareNginxVTSInvalidData(t *testing.T) (*NginxVTS, func()) { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("hello and\n goodbye")) + })) + vts := New() + vts.URL = srv.URL + require.NoError(t, vts.Init()) + + return vts, srv.Close +} + +func prepareNginxVTS404(t *testing.T) (*NginxVTS, func()) { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusNotFound) + })) + vts := New() + vts.URL = srv.URL + require.NoError(t, vts.Init()) + + return vts, srv.Close +} + +func prepareNginxVTSConnectionRefused(t *testing.T) (*NginxVTS, func()) { + t.Helper() + vts := New() + vts.URL = "http://127.0.0.1:18080" + require.NoError(t, vts.Init()) + + return vts, func() {} +} + +func prepareNginxVTSEndpoint() *httptest.Server { + return httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/": + _, _ = w.Write(dataVer0118Response) + default: + w.WriteHeader(http.StatusNotFound) + } + })) +} + +func numOfCharts(charts ...module.Charts) (num int) { + for _, v := range charts { + num += len(v) + } + return num +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/testdata/config.json b/src/go/collectors/go.d.plugin/modules/nginxvts/testdata/config.json new file mode 100644 index 000000000..984c3ed6e --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/testdata/config.json @@ -0,0 +1,20 @@ +{ + "update_every": 123, + "url": "ok", + "body": "ok", + "method": "ok", + "headers": { + "ok": "ok" + }, + "username": "ok", + "password": "ok", + "proxy_url": "ok", + "proxy_username": "ok", + "proxy_password": "ok", + "timeout": 123.123, + "not_follow_redirects": true, + "tls_ca": "ok", + "tls_cert": "ok", + "tls_key": "ok", + "tls_skip_verify": true +} diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/nginxvts/testdata/config.yaml new file mode 100644 index 000000000..8558b61cc --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/testdata/config.yaml @@ -0,0 +1,17 @@ +update_every: 123 +url: "ok" +body: "ok" +method: "ok" +headers: + ok: "ok" +username: "ok" +password: "ok" +proxy_url: "ok" +proxy_username: "ok" +proxy_password: "ok" +timeout: 123.123 +not_follow_redirects: yes +tls_ca: "ok" +tls_cert: "ok" +tls_key: "ok" +tls_skip_verify: yes diff --git a/src/go/collectors/go.d.plugin/modules/nginxvts/testdata/vts-v0.1.18.json b/src/go/collectors/go.d.plugin/modules/nginxvts/testdata/vts-v0.1.18.json new file mode 100644 index 000000000..cdc331d5f --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/testdata/vts-v0.1.18.json @@ -0,0 +1,44 @@ +{ + "hostName": "Web", + "nginxVersion": "1.18.0", + "loadMsec": 1606489796895, + "nowMsec": 1606490116734, + "connections": { + "active": 2, + "reading": 0, + "writing": 1, + "waiting": 1, + "accepted": 12, + "handled": 12, + "requests": 17 + }, + "sharedZones": { + "name": "ngx_http_vhost_traffic_status", + "maxSize": 1048575, + "usedSize": 45799, + "usedNode": 13 + }, + "serverZones": { + "*": { + "requestCounter": 2, + "inBytes": 156, + "outBytes": 692, + "responses": { + "1xx": 1, + "2xx": 2, + "3xx": 3, + "4xx": 4, + "5xx": 5, + "miss": 2, + "bypass": 4, + "expired": 6, + "stale": 8, + "updating": 10, + "revalidated": 12, + "hit": 14, + "scarce": 16 + } + } + } +} + |