diff options
Diffstat (limited to 'src/go/plugin/go.d/modules/puppet')
l--------- | src/go/plugin/go.d/modules/puppet/README.md | 1 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/charts.go | 93 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/collect.go | 75 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/config_schema.json | 177 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/integrations/puppet.md | 233 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/metadata.yaml | 184 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/puppet.go | 114 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/puppet_test.go | 252 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/response.go | 32 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/testdata/config.json | 20 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/testdata/config.yaml | 17 | ||||
-rw-r--r-- | src/go/plugin/go.d/modules/puppet/testdata/serviceStatusResponse.json | 497 |
12 files changed, 1695 insertions, 0 deletions
diff --git a/src/go/plugin/go.d/modules/puppet/README.md b/src/go/plugin/go.d/modules/puppet/README.md new file mode 120000 index 000000000..b6c4c83f9 --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/README.md @@ -0,0 +1 @@ +integrations/puppet.md
\ No newline at end of file diff --git a/src/go/plugin/go.d/modules/puppet/charts.go b/src/go/plugin/go.d/modules/puppet/charts.go new file mode 100644 index 000000000..c1da8d162 --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/charts.go @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package puppet + +import ( + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" +) + +const ( + prioJVMHeap = module.Priority + iota + prioJVMNonHeap + prioCPUUsage + prioFileDescriptors +) + +const ( + byteToMiB = 1 << 20 +) + +var charts = module.Charts{ + jvmHeapChart.Copy(), + jvmNonHeapChart.Copy(), + cpuUsageChart.Copy(), + fileDescriptorsChart.Copy(), +} + +var ( + jvmHeapChart = module.Chart{ + ID: "jvm_heap", + Title: "JVM Heap", + Units: "MiB", + Fam: "resources", + Ctx: "puppet.jvm_heap", + Type: module.Area, + Priority: prioJVMHeap, + Dims: module.Dims{ + {ID: "jvm_heap_committed", Name: "committed", Div: byteToMiB}, + {ID: "jvm_heap_used", Name: "used", Div: byteToMiB}, + }, + Vars: module.Vars{ + {ID: "jvm_heap_max"}, + {ID: "jvm_heap_init"}, + }, + } + + jvmNonHeapChart = module.Chart{ + ID: "jvm_nonheap", + Title: "JVM Non-Heap", + Units: "MiB", + Fam: "resources", + Ctx: "puppet.jvm_nonheap", + Type: module.Area, + Priority: prioJVMNonHeap, + Dims: module.Dims{ + {ID: "jvm_nonheap_committed", Name: "committed", Div: byteToMiB}, + {ID: "jvm_nonheap_used", Name: "used", Div: byteToMiB}, + }, + Vars: module.Vars{ + {ID: "jvm_nonheap_max"}, + {ID: "jvm_nonheap_init"}, + }, + } + + cpuUsageChart = module.Chart{ + ID: "cpu", + Title: "CPU usage", + Units: "percentage", + Fam: "resources", + Ctx: "puppet.cpu", + Type: module.Stacked, + Priority: prioCPUUsage, + Dims: module.Dims{ + {ID: "cpu_usage", Name: "execution", Div: 1000}, + {ID: "gc_cpu_usage", Name: "GC", Div: 1000}, + }, + } + + fileDescriptorsChart = module.Chart{ + ID: "fd_open", + Title: "File Descriptors", + Units: "descriptors", + Fam: "resources", + Ctx: "puppet.fdopen", + Type: module.Line, + Priority: prioFileDescriptors, + Dims: module.Dims{ + {ID: "fd_used", Name: "used"}, + }, + Vars: module.Vars{ + {ID: "fd_max"}, + }, + } +) diff --git a/src/go/plugin/go.d/modules/puppet/collect.go b/src/go/plugin/go.d/modules/puppet/collect.go new file mode 100644 index 000000000..a1b95e09c --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/collect.go @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package puppet + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/stm" + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web" +) + +var ( + //https://puppet.com/docs/puppet/8/server/status-api/v1/services + urlPathStatusService = "/status/v1/services" + urlQueryStatusService = url.Values{"level": {"debug"}}.Encode() +) + +func (p *Puppet) collect() (map[string]int64, error) { + stats, err := p.queryStatsService() + if err != nil { + return nil, err + } + + mx := stm.ToMap(stats) + + return mx, nil +} + +func (p *Puppet) queryStatsService() (*statusServiceResponse, error) { + req, err := web.NewHTTPRequestWithPath(p.Request, urlPathStatusService) + if err != nil { + return nil, err + } + + req.URL.RawQuery = urlQueryStatusService + + var stats statusServiceResponse + if err := p.doOKDecode(req, &stats); err != nil { + return nil, err + } + + if stats.StatusService == nil { + return nil, fmt.Errorf("unexpected response: not puppet service status data") + } + + return &stats, nil +} + +func (p *Puppet) doOKDecode(req *http.Request, in interface{}) error { + resp, err := p.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/plugin/go.d/modules/puppet/config_schema.json b/src/go/plugin/go.d/modules/puppet/config_schema.json new file mode 100644 index 000000000..92cbcb87f --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/config_schema.json @@ -0,0 +1,177 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Puppet 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 where the Puppet instance can be accessed.", + "type": "string", + "default": "https://127.0.0.1:8140", + "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": { + "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" + }, + "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" + ] + } + ] + } + } +} diff --git a/src/go/plugin/go.d/modules/puppet/integrations/puppet.md b/src/go/plugin/go.d/modules/puppet/integrations/puppet.md new file mode 100644 index 000000000..23e85dc4d --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/integrations/puppet.md @@ -0,0 +1,233 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/puppet/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/plugin/go.d/modules/puppet/metadata.yaml" +sidebar_label: "Puppet" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/CICD Platforms" +most_popular: False +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# Puppet + + +<img src="https://netdata.cloud/img/puppet.svg" width="150"/> + + +Plugin: go.d.plugin +Module: puppet + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors Puppet metrics, including JVM heap and non-heap memory, CPU usage, and file descriptors. + + +It uses Puppet's metrics API endpoint [/status/v1/services](https://www.puppet.com/docs/puppetserver/5.3/status-api/v1/services.html) to gather the metrics. + + +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 Puppet instances running on localhost that are listening on port 8140. +On startup, it tries to collect metrics from: + +- https://127.0.0.1:8140 + + +#### 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 Puppet instance + +These metrics refer to the entire monitored application. + +This scope has no labels. + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| puppet.jvm_heap | committed, used | MiB | +| puppet.jvm_nonheap | committed, used | MiB | +| puppet.cpu | execution, GC | percentage | +| puppet.fdopen | used | descriptors | + + + +## Alerts + +There are no alerts configured by default for this integration. + + +## Setup + +### Prerequisites + +No action required. + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/puppet.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/puppet.conf +``` +#### Options + +The following options can be defined globally: update_every, autodetection_retry. + + +<details open><summary></summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| url | The base URL where the Puppet instance can be accessed. | https://127.0.0.1:8140 | yes | +| timeout | HTTPS request timeout. | 1 | no | +| username | Username for basic HTTPS authentication. | | no | +| password | Password for basic HTTPS authentication. | | no | +| proxy_url | Proxy URL. | | no | +| proxy_username | Username for proxy basic HTTPS authentication. | | no | +| proxy_password | Password for proxy basic HTTPS authentication. | | no | +| method | HTTPS request method. | POST | no | +| body | HTTPS request body. | | no | +| headers | HTTPS 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 with self-signed certificate + +Puppet with self-signed TLS certificate. + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: local + url: https://127.0.0.1:8140 + 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: https://127.0.0.1:8140 + tls_skip_verify: yes + + - name: remote + url: https://192.0.2.1:8140 + tls_skip_verify: yes + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +**Important**: Debug mode is not supported for data collection jobs created via the UI using the Dyncfg feature. + +To troubleshoot issues with the `puppet` 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 puppet + ``` + +### Getting Logs + +If you're encountering problems with the `puppet` collector, follow these steps to retrieve logs and identify potential issues: + +- **Run the command** specific to your system (systemd, non-systemd, or Docker container). +- **Examine the output** for any warnings or error messages that might indicate issues. These messages should provide clues about the root cause of the problem. + +#### System with systemd + +Use the following command to view logs generated since the last Netdata service restart: + +```bash +journalctl _SYSTEMD_INVOCATION_ID="$(systemctl show --value --property=InvocationID netdata)" --namespace=netdata --grep puppet +``` + +#### System without systemd + +Locate the collector log file, typically at `/var/log/netdata/collector.log`, and use `grep` to filter for collector's name: + +```bash +grep puppet /var/log/netdata/collector.log +``` + +**Note**: This method shows logs from all restarts. Focus on the **latest entries** for troubleshooting current issues. + +#### Docker Container + +If your Netdata runs in a Docker container named "netdata" (replace if different), use this command: + +```bash +docker logs netdata 2>&1 | grep puppet +``` + + diff --git a/src/go/plugin/go.d/modules/puppet/metadata.yaml b/src/go/plugin/go.d/modules/puppet/metadata.yaml new file mode 100644 index 000000000..fa96ea8f2 --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/metadata.yaml @@ -0,0 +1,184 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-puppet + plugin_name: go.d.plugin + module_name: puppet + monitored_instance: + name: Puppet + link: "https://www.puppet.com/" + categories: + - data-collection.ci-cd-systems + icon_filename: "puppet.svg" + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + keywords: + - puppet + most_popular: false + overview: + data_collection: + metrics_description: | + This collector monitors Puppet metrics, including JVM heap and non-heap memory, CPU usage, and file descriptors. + method_description: | + It uses Puppet's metrics API endpoint [/status/v1/services](https://www.puppet.com/docs/puppetserver/5.3/status-api/v1/services.html) to gather the metrics. + supported_platforms: + include: [] + exclude: [] + multi_instance: true + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: | + By default, it detects Puppet instances running on localhost that are listening on port 8140. + On startup, it tries to collect metrics from: + + - https://127.0.0.1:8140 + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: [] + configuration: + file: + name: "go.d/puppet.conf" + options: + description: | + The following options can be defined globally: update_every, autodetection_retry. + folding: + title: "" + enabled: true + list: + - name: url + description: The base URL where the Puppet instance can be accessed. + default_value: https://127.0.0.1:8140 + required: true + - name: timeout + description: HTTPS request timeout. + default_value: 1 + required: false + - name: username + description: Username for basic HTTPS authentication. + default_value: "" + required: false + - name: password + description: Password for basic HTTPS authentication. + default_value: "" + required: false + - name: proxy_url + description: Proxy URL. + default_value: "" + required: false + - name: proxy_username + description: Username for proxy basic HTTPS authentication. + default_value: "" + required: false + - name: proxy_password + description: Password for proxy basic HTTPS authentication. + default_value: "" + required: false + - name: method + description: HTTPS request method. + default_value: POST + required: false + - name: body + description: HTTPS request body. + default_value: "" + required: false + - name: headers + description: HTTPS 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: + enabled: true + title: "Config" + list: + - name: Basic with self-signed certificate + description: Puppet with self-signed TLS certificate. + config: | + jobs: + - name: local + url: https://127.0.0.1:8140 + 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: https://127.0.0.1:8140 + tls_skip_verify: yes + + - name: remote + url: https://192.0.2.1:8140 + tls_skip_verify: yes + 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: puppet.jvm_heap + description: JVM Heap + unit: "MiB" + chart_type: area + dimensions: + - name: committed + - name: used + - name: puppet.jvm_nonheap + description: JVM Non-Heap + unit: "MiB" + chart_type: area + dimensions: + - name: committed + - name: used + - name: puppet.cpu + description: CPU usage + unit: "percentage" + chart_type: stacked + dimensions: + - name: execution + - name: GC + - name: puppet.fdopen + description: File Descriptors + unit: "descriptors" + chart_type: line + dimensions: + - name: used diff --git a/src/go/plugin/go.d/modules/puppet/puppet.go b/src/go/plugin/go.d/modules/puppet/puppet.go new file mode 100644 index 000000000..e6eb7b058 --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/puppet.go @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package puppet + +import ( + _ "embed" + "errors" + "net/http" + "time" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("puppet", module.Creator{ + JobConfigSchema: configSchema, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *Puppet { + return &Puppet{ + Config: Config{ + HTTP: web.HTTP{ + Request: web.Request{ + URL: "https://127.0.0.1:8140", + }, + Client: web.Client{ + Timeout: web.Duration(time.Second * 1), + }, + }, + }, + charts: charts.Copy(), + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + web.HTTP `yaml:",inline" json:""` +} + +type Puppet struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + httpClient *http.Client +} + +func (p *Puppet) Configuration() any { + return p.Config +} + +func (p *Puppet) Init() error { + if p.URL == "" { + p.Error("URL not set") + return errors.New("url not set") + } + + client, err := web.NewHTTPClient(p.Client) + if err != nil { + p.Error(err) + return err + } + p.httpClient = client + + p.Debugf("using URL %s", p.URL) + p.Debugf("using timeout: %s", p.Timeout) + + return nil +} + +func (p *Puppet) Check() error { + mx, err := p.collect() + if err != nil { + p.Error(err) + return err + } + + if len(mx) == 0 { + return errors.New("no metrics collected") + } + + return nil +} + +func (p *Puppet) Charts() *module.Charts { + return p.charts +} + +func (p *Puppet) Collect() map[string]int64 { + mx, err := p.collect() + if err != nil { + p.Error(err) + } + + if len(mx) == 0 { + return nil + } + + return mx +} + +func (p *Puppet) Cleanup() { + if p.httpClient != nil { + p.httpClient.CloseIdleConnections() + } +} diff --git a/src/go/plugin/go.d/modules/puppet/puppet_test.go b/src/go/plugin/go.d/modules/puppet/puppet_test.go new file mode 100644 index 000000000..7c80a638a --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/puppet_test.go @@ -0,0 +1,252 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package puppet + +import ( + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module" + "github.com/netdata/netdata/go/plugins/plugin/go.d/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") + + serviceStatusResponse, _ = os.ReadFile("testdata/serviceStatusResponse.json") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + "serviceStatusResponse": serviceStatusResponse, + } { + require.NotNil(t, data, name) + } +} + +func TestPuppet_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &Puppet{}, dataConfigJSON, dataConfigYAML) +} + +func TestPuppet_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) { + puppet := New() + puppet.Config = test.config + + if test.wantFail { + assert.Error(t, puppet.Init()) + } else { + assert.NoError(t, puppet.Init()) + } + }) + } +} + +func TestPuppet_Charts(t *testing.T) { + assert.NotNil(t, New().Charts()) +} + +func TestPuppet_Check(t *testing.T) { + tests := map[string]struct { + wantFail bool + prepare func(t *testing.T) (*Puppet, func()) + }{ + "success default config": { + wantFail: false, + prepare: prepareCaseOkDefault, + }, + "fails on unexpected json response": { + wantFail: true, + prepare: prepareCaseUnexpectedJsonResponse, + }, + "fails on invalid format response": { + wantFail: true, + prepare: prepareCaseInvalidFormatResponse, + }, + "fails on connection refused": { + wantFail: true, + prepare: prepareCaseConnectionRefused, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + puppet, cleanup := test.prepare(t) + defer cleanup() + + if test.wantFail { + assert.Error(t, puppet.Check()) + } else { + assert.NoError(t, puppet.Check()) + } + }) + } +} + +func TestPuppet_Collect(t *testing.T) { + tests := map[string]struct { + prepare func(t *testing.T) (*Puppet, func()) + wantMetrics map[string]int64 + }{ + "success default config": { + prepare: prepareCaseOkDefault, + wantMetrics: map[string]int64{ + "cpu_usage": 49, + "fd_max": 524288, + "fd_used": 234, + "gc_cpu_usage": 0, + "jvm_heap_committed": 1073741824, + "jvm_heap_init": 1073741824, + "jvm_heap_max": 1073741824, + "jvm_heap_used": 550502400, + "jvm_nonheap_committed": 334102528, + "jvm_nonheap_init": 7667712, + "jvm_nonheap_max": -1, + "jvm_nonheap_used": 291591160, + }, + }, + "fails on unexpected json response": { + prepare: prepareCaseUnexpectedJsonResponse, + }, + "fails on invalid format response": { + prepare: prepareCaseInvalidFormatResponse, + }, + "fails on connection refused": { + prepare: prepareCaseConnectionRefused, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + puppet, cleanup := test.prepare(t) + defer cleanup() + + mx := puppet.Collect() + + require.Equal(t, test.wantMetrics, mx) + if len(test.wantMetrics) > 0 { + testMetricsHasAllChartsDims(t, puppet, mx) + } + }) + } +} + +func testMetricsHasAllChartsDims(t *testing.T, puppet *Puppet, mx map[string]int64) { + for _, chart := range *puppet.Charts() { + if chart.Obsolete { + 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) + } + } +} + +func prepareCaseOkDefault(t *testing.T) (*Puppet, func()) { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/status/v1/services": + if r.URL.RawQuery != urlQueryStatusService { + w.WriteHeader(http.StatusNotFound) + } else { + _, _ = w.Write(serviceStatusResponse) + } + default: + w.WriteHeader(http.StatusNotFound) + } + })) + + puppet := New() + puppet.URL = srv.URL + require.NoError(t, puppet.Init()) + + return puppet, srv.Close +} + +func prepareCaseUnexpectedJsonResponse(t *testing.T) (*Puppet, func()) { + t.Helper() + resp := ` +{ + "elephant": { + "burn": false, + "mountain": true, + "fog": false, + "skin": -1561907625, + "burst": "anyway", + "shadow": 1558616893 + }, + "start": "ever", + "base": 2093056027, + "mission": -2007590351, + "victory": 999053756, + "die": false +} +` + srv := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte(resp)) + })) + + puppet := New() + puppet.URL = srv.URL + require.NoError(t, puppet.Init()) + + return puppet, srv.Close +} + +func prepareCaseInvalidFormatResponse(t *testing.T) (*Puppet, func()) { + t.Helper() + srv := httptest.NewServer(http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write([]byte("hello and\n goodbye")) + })) + + puppet := New() + puppet.URL = srv.URL + require.NoError(t, puppet.Init()) + + return puppet, srv.Close +} + +func prepareCaseConnectionRefused(t *testing.T) (*Puppet, func()) { + t.Helper() + puppet := New() + puppet.URL = "http://127.0.0.1:65001" + require.NoError(t, puppet.Init()) + + return puppet, func() {} +} diff --git a/src/go/plugin/go.d/modules/puppet/response.go b/src/go/plugin/go.d/modules/puppet/response.go new file mode 100644 index 000000000..dc903d0a9 --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/response.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package puppet + +type statusServiceResponse struct { + StatusService *struct { + Status struct { + Experimental struct { + JVMMetrics *struct { + CPUUsage float64 `json:"cpu-usage" stm:"cpu_usage,1000,1"` + GCCPUUsage float64 `json:"gc-cpu-usage" stm:"gc_cpu_usage,1000,1"` + HeapMemory struct { + Committed int64 `json:"committed" stm:"committed"` + Init int64 `json:"init" stm:"init"` + Max int64 `json:"max" stm:"max"` + Used int64 `json:"used" stm:"used"` + } `json:"heap-memory" stm:"jvm_heap"` + FileDescriptors struct { + Used int `json:"used" stm:"used"` + Max int `json:"max" stm:"max"` + } `json:"file-descriptors" stm:"fd"` + NonHeapMemory struct { + Committed int64 `json:"committed" stm:"committed"` + Init int64 `json:"init" stm:"init"` + Max int64 `json:"max" stm:"max"` + Used int64 `json:"used" stm:"used"` + } `json:"non-heap-memory" stm:"jvm_nonheap"` + } `json:"jvm-metrics" stm:""` + } `json:"experimental" stm:""` + } `json:"status" stm:""` + } `json:"status-service" stm:""` +} diff --git a/src/go/plugin/go.d/modules/puppet/testdata/config.json b/src/go/plugin/go.d/modules/puppet/testdata/config.json new file mode 100644 index 000000000..984c3ed6e --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/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/plugin/go.d/modules/puppet/testdata/config.yaml b/src/go/plugin/go.d/modules/puppet/testdata/config.yaml new file mode 100644 index 000000000..8558b61cc --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/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/plugin/go.d/modules/puppet/testdata/serviceStatusResponse.json b/src/go/plugin/go.d/modules/puppet/testdata/serviceStatusResponse.json new file mode 100644 index 000000000..a0eee8693 --- /dev/null +++ b/src/go/plugin/go.d/modules/puppet/testdata/serviceStatusResponse.json @@ -0,0 +1,497 @@ +{ + "puppet-profiler": { + "service_version": "8.4.0", + "service_status_version": 1, + "detail_level": "debug", + "state": "running", + "status": { + "experimental": { + "function-metrics": [], + "resource-metrics": [], + "catalog-metrics": [], + "puppetdb-metrics": [], + "inline-metrics": [] + } + }, + "active_alerts": [] + }, + "jruby-metrics": { + "service_version": "8.4.0", + "service_status_version": 1, + "detail_level": "debug", + "state": "running", + "status": { + "experimental": { + "jruby-pool-lock-status": { + "current-state": ":not-in-use", + "last-change-time": "2024-07-05T06:23:20.120Z" + }, + "metrics": { + "average-lock-wait-time": 0, + "num-free-jrubies": 4, + "borrow-count": 0, + "average-requested-jrubies": 0.0, + "borrow-timeout-count": 0, + "return-count": 0, + "borrow-timers": { + "total": { + "count": 0, + "mean": 0, + "max": 0, + "rate": 0.0 + } + }, + "borrow-retry-count": 0, + "borrowed-instances": [], + "average-borrow-time": 0, + "num-jrubies": 4, + "requested-count": 0, + "queue-limit-hit-rate": 0.0, + "average-lock-held-time": 0, + "requested-instances": [], + "queue-limit-hit-count": 0, + "average-free-jrubies": 3.3019592583652217, + "num-pool-locks": 0, + "average-wait-time": 0 + } + } + }, + "active_alerts": [] + }, + "ca": { + "service_version": "8.4.0", + "service_status_version": 1, + "detail_level": "debug", + "state": "running", + "status": {}, + "active_alerts": [] + }, + "master": { + "service_version": "8.4.0", + "service_status_version": 1, + "detail_level": "debug", + "state": "running", + "status": { + "experimental": { + "http-metrics": [ + { + "route-id": "puppet-v3-static_file_content-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-file_content-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-environments", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-tasks-:module-name-:task-name", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-file_metadata-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-facts-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "other", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-tasks", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-compile", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-report-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-node-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-catalog-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-plans-:module-name-:plan-name", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-file_metadatas-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-file_bucket_file-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v4-catalog", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "total", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-environment_modules-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-environment_classes-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-plans", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-environment_transports-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + } + ], + "http-client-metrics": [] + } + }, + "active_alerts": [] + }, + "server": { + "service_version": "8.4.0", + "service_status_version": 1, + "detail_level": "debug", + "state": "running", + "status": { + "experimental": { + "http-metrics": [ + { + "route-id": "puppet-v3-static_file_content-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-file_content-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-environments", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-tasks-:module-name-:task-name", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-file_metadata-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-facts-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "other", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-tasks", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-compile", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-report-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-node-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-catalog-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-plans-:module-name-:plan-name", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-file_metadatas-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-file_bucket_file-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v4-catalog", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "total", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-environment_modules-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-environment_classes-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-plans", + "count": 0, + "mean": 0, + "aggregate": 0 + }, + { + "route-id": "puppet-v3-environment_transports-/*/", + "count": 0, + "mean": 0, + "aggregate": 0 + } + ], + "http-client-metrics": [] + } + }, + "active_alerts": [] + }, + "status-service": { + "service_version": "1.1.1", + "service_status_version": 1, + "detail_level": "debug", + "state": "running", + "status": { + "experimental": { + "jvm-metrics": { + "cpu-usage": 0.04997002, + "up-time-ms": 51328, + "memory-pools": { + "Metaspace": { + "type": "NON_HEAP", + "usage": { + "committed": 214106112, + "init": 0, + "max": -1, + "used": 183450600 + } + }, + "CodeHeap 'non-nmethods'": { + "type": "NON_HEAP", + "usage": { + "committed": 2555904, + "init": 2555904, + "max": 5840896, + "used": 1923072 + } + }, + "CodeHeap 'profiled nmethods'": { + "type": "NON_HEAP", + "usage": { + "committed": 52559872, + "init": 2555904, + "max": 122908672, + "used": 52545664 + } + }, + "Compressed Class Space": { + "type": "NON_HEAP", + "usage": { + "committed": 49020928, + "init": 0, + "max": 1073741824, + "used": 37887856 + } + }, + "G1 Eden Space": { + "type": "HEAP", + "usage": { + "committed": 542113792, + "init": 53477376, + "max": -1, + "used": 146800640 + } + }, + "G1 Old Gen": { + "type": "HEAP", + "usage": { + "committed": 462422016, + "init": 1020264448, + "max": 1073741824, + "used": 335020032 + } + }, + "G1 Survivor Space": { + "type": "HEAP", + "usage": { + "committed": 69206016, + "init": 0, + "max": -1, + "used": 68681728 + } + }, + "CodeHeap 'non-profiled nmethods'": { + "type": "NON_HEAP", + "usage": { + "committed": 15597568, + "init": 2555904, + "max": 122908672, + "used": 15588736 + } + } + }, + "gc-cpu-usage": 0.0, + "threading": { + "thread-count": 59, + "peak-thread-count": 59 + }, + "heap-memory": { + "committed": 1073741824, + "init": 1073741824, + "max": 1073741824, + "used": 550502400 + }, + "gc-stats": { + "G1 Young Generation": { + "count": 18, + "total-time-ms": 550, + "last-gc-info": { + "duration-ms": 75 + } + }, + "G1 Old Generation": { + "count": 0, + "total-time-ms": 0 + }, + "G1 Concurrent GC": { + "count": 10, + "total-time-ms": 49, + "last-gc-info": { + "duration-ms": 0 + } + } + }, + "start-time-ms": 1720160584298, + "file-descriptors": { + "used": 234, + "max": 524288 + }, + "non-heap-memory": { + "committed": 334102528, + "init": 7667712, + "max": -1, + "used": 291591160 + }, + "nio-buffer-pools": { + "mapped": { + "count": 0, + "memory-used": 0, + "total-capacity": 0 + }, + "direct": { + "count": 11, + "memory-used": 197631, + "total-capacity": 197631 + }, + "mapped - 'non-volatile memory'": { + "count": 0, + "memory-used": 0, + "total-capacity": 0 + } + } + } + } + }, + "active_alerts": [] + } +} |