summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/nginx
diff options
context:
space:
mode:
Diffstat (limited to '')
l---------src/go/collectors/go.d.plugin/modules/nginx/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/apiclient.go168
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/charts.go58
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/collect.go17
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/config_schema.json177
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/integrations/nginx.md232
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/metadata.yaml226
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/metrics.go34
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/nginx.go106
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/nginx_test.go156
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/testdata/config.json20
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/testdata/config.yaml17
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/testdata/status.txt4
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginx/testdata/tengine-status.txt4
l---------src/go/collectors/go.d.plugin/modules/nginxplus/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/cache.go172
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/charts.go981
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/collect.go393
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/config_schema.json177
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/integrations/nginx_plus.md413
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/metadata.yaml584
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/nginx_http_api.go212
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/nginx_http_api_query.go388
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/nginxplus.go127
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/nginxplus_test.go596
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/404.json9
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/api_versions.json10
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/connections.json6
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_http.json10
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_root.json10
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/endpoints_stream.json6
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_caches.json40
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_location_zones.json35
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_requests.json4
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_server_zones.json21
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/http_upstreams.json76
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/nginx.json10
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/resolvers.json36
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/ssl.json16
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/stream_server_zones.json15
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/api-8/stream_upstreams.json48
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/config.json20
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxplus/testdata/config.yaml17
l---------src/go/collectors/go.d.plugin/modules/nginxvts/README.md1
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/charts.go130
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/collect.go81
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/config_schema.json176
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/init.go47
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/integrations/nginx_vts.md233
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/metadata.yaml264
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/metrics.go53
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/nginxvts.go118
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/nginxvts_test.go266
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/testdata/config.json20
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/testdata/config.yaml17
-rw-r--r--src/go/collectors/go.d.plugin/modules/nginxvts/testdata/vts-v0.1.18.json44
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
+ }
+ }
+ }
+}
+