diff options
Diffstat (limited to '')
13 files changed, 1435 insertions, 0 deletions
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..40a71c1de --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/config_schema.json @@ -0,0 +1,162 @@ +{ + "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": "^$|^/" + } + }, + "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 + }, + "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..475ed90a6 --- /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](https://github.com/netdata/netdata/blob/master/docs/netdata-agent/configuration.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><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><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><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><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..f3bbf646a --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/nginxvts/nginxvts.go @@ -0,0 +1,117 @@ +// 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() }, + }) +} + +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 { + web.HTTP `yaml:",inline" json:""` + UpdateEvery int `yaml:"update_every" json:"update_every"` +} + +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 + } + } + } +} + |