diff options
Diffstat (limited to '')
12 files changed, 812 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/README.md b/src/go/collectors/go.d.plugin/modules/whoisquery/README.md new file mode 120000 index 000000000..8661481d1 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/README.md @@ -0,0 +1 @@ +integrations/domain_expiration_date.md
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/charts.go b/src/go/collectors/go.d.plugin/modules/whoisquery/charts.go new file mode 100644 index 000000000..49c508992 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/charts.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package whoisquery + +import "github.com/netdata/netdata/go/go.d.plugin/agent/module" + +var baseCharts = module.Charts{ + { + ID: "time_until_expiration", + Title: "Time Until Domain Expiration", + Units: "seconds", + Fam: "expiration time", + Ctx: "whoisquery.time_until_expiration", + Opts: module.Opts{StoreFirst: true}, + Dims: module.Dims{ + {ID: "expiry"}, + }, + Vars: module.Vars{ + {ID: "days_until_expiration_warning"}, + {ID: "days_until_expiration_critical"}, + }, + }, +} diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/collect.go b/src/go/collectors/go.d.plugin/modules/whoisquery/collect.go new file mode 100644 index 000000000..7bd8ed70f --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/collect.go @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package whoisquery + +import "fmt" + +func (w *WhoisQuery) collect() (map[string]int64, error) { + remainingTime, err := w.prov.remainingTime() + if err != nil { + return nil, fmt.Errorf("%v (source: %s)", err, w.Source) + } + + mx := make(map[string]int64) + w.collectExpiration(mx, remainingTime) + + return mx, nil +} + +func (w *WhoisQuery) collectExpiration(mx map[string]int64, remainingTime float64) { + mx["expiry"] = int64(remainingTime) + mx["days_until_expiration_warning"] = w.DaysUntilWarn + mx["days_until_expiration_critical"] = w.DaysUntilCrit +} diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/config_schema.json b/src/go/collectors/go.d.plugin/modules/whoisquery/config_schema.json new file mode 100644 index 000000000..e59fa8859 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/config_schema.json @@ -0,0 +1,60 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "WHOIS query collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 60 + }, + "source": { + "title": "Domain", + "description": "The domain for which WHOIS queries will be performed.", + "type": "string" + }, + "timeout": { + "title": "Timeout", + "description": "The timeout in seconds for the WHOIS query.", + "type": "number", + "minimum": 0.5, + "default": 5 + }, + "days_until_expiration_warning": { + "title": "Days until warning", + "description": "Number of days before the alarm status is set to warning.", + "type": "integer", + "minimum": 1, + "default": 90 + }, + "days_until_expiration_critical": { + "title": "Days until critical", + "description": "Number of days before the alarm status is set to critical.", + "type": "integer", + "minimum": 1, + "default": 30 + } + }, + "required": [ + "source" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "uiOptions": { + "fullPage": true + }, + "source": { + "ui:placeholder": "example.com" + }, + "timeout": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/init.go b/src/go/collectors/go.d.plugin/modules/whoisquery/init.go new file mode 100644 index 000000000..d42002095 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/init.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package whoisquery + +import ( + "errors" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" +) + +func (w *WhoisQuery) validateConfig() error { + if w.Source == "" { + return errors.New("source is not set") + } + return nil +} + +func (w *WhoisQuery) initProvider() (provider, error) { + return newProvider(w.Config) +} + +func (w *WhoisQuery) initCharts() *module.Charts { + charts := baseCharts.Copy() + + for _, chart := range *charts { + chart.Labels = []module.Label{ + {Key: "domain", Value: w.Source}, + } + } + + return charts +} diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/integrations/domain_expiration_date.md b/src/go/collectors/go.d.plugin/modules/whoisquery/integrations/domain_expiration_date.md new file mode 100644 index 000000000..727a508f9 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/integrations/domain_expiration_date.md @@ -0,0 +1,187 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/whoisquery/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/whoisquery/metadata.yaml" +sidebar_label: "Domain expiration date" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/Synthetic Checks" +most_popular: False +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# Domain expiration date + + +<img src="https://netdata.cloud/img/globe.svg" width="150"/> + + +Plugin: go.d.plugin +Module: whoisquery + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors the remaining time before the domain expires. + + + + +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 domain + +These metrics refer to the configured source. + +Labels: + +| Label | Description | +|:-----------|:----------------| +| domain | Configured source | + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| whoisquery.time_until_expiration | expiry | seconds | + + + +## Alerts + + +The following alerts are available: + +| Alert name | On metric | Description | +|:------------|:----------|:------------| +| [ whoisquery_days_until_expiration ](https://github.com/netdata/netdata/blob/master/src/health/health.d/whoisquery.conf) | whoisquery.time_until_expiration | time until the domain name registration expires | + + +## Setup + +### Prerequisites + +No action required. + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/whoisquery.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/whoisquery.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. | 60 | no | +| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no | +| source | Domain address. | | yes | +| days_until_expiration_warning | Number of days before the alarm status is warning. | 30 | no | +| days_until_expiration_critical | Number of days before the alarm status is critical. | 15 | no | +| timeout | The query timeout in seconds. | 5 | no | + +</details> + +#### Examples + +##### Basic + +Basic configuration example + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: my_site + source: my_site.com + +``` +</details> + +##### Multi-instance + +> **Note**: When you define more than one job, their names must be unique. + +Check the expiration status of the multiple domains. + + +<details open><summary>Config</summary> + +```yaml +jobs: + - name: my_site1 + source: my_site1.com + + - name: my_site2 + source: my_site2.com + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +To troubleshoot issues with the `whoisquery` 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 whoisquery + ``` + + diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/metadata.yaml b/src/go/collectors/go.d.plugin/modules/whoisquery/metadata.yaml new file mode 100644 index 000000000..eb826ebde --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/metadata.yaml @@ -0,0 +1,125 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-whoisquery + plugin_name: go.d.plugin + module_name: whoisquery + monitored_instance: + name: Domain expiration date + link: "" + icon_filename: globe.svg + categories: + - data-collection.synthetic-checks + keywords: + - whois + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + most_popular: false + overview: + data_collection: + metrics_description: | + This collector monitors the remaining time before the domain expires. + 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: [] + configuration: + file: + name: go.d/whoisquery.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: 60 + required: false + - name: autodetection_retry + description: Recheck interval in seconds. Zero means no recheck will be scheduled. + default_value: 0 + required: false + - name: source + description: Domain address. + default_value: "" + required: true + - name: days_until_expiration_warning + description: Number of days before the alarm status is warning. + default_value: 30 + required: false + - name: days_until_expiration_critical + description: Number of days before the alarm status is critical. + default_value: 15 + required: false + - name: timeout + description: The query timeout in seconds. + default_value: 5 + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Basic + description: Basic configuration example + config: | + jobs: + - name: my_site + source: my_site.com + - name: Multi-instance + description: | + > **Note**: When you define more than one job, their names must be unique. + + Check the expiration status of the multiple domains. + config: | + jobs: + - name: my_site1 + source: my_site1.com + + - name: my_site2 + source: my_site2.com + troubleshooting: + problems: + list: [] + alerts: + - name: whoisquery_days_until_expiration + metric: whoisquery.time_until_expiration + info: time until the domain name registration expires + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/whoisquery.conf + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: domain + description: These metrics refer to the configured source. + labels: + - name: domain + description: Configured source + metrics: + - name: whoisquery.time_until_expiration + description: Time Until Domain Expiration + unit: seconds + chart_type: line + dimensions: + - name: expiry diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/provider.go b/src/go/collectors/go.d.plugin/modules/whoisquery/provider.go new file mode 100644 index 000000000..f6164da7c --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/provider.go @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package whoisquery + +import ( + "errors" + "fmt" + "strings" + "time" + + "github.com/araddon/dateparse" + "github.com/likexian/whois" + whoisparser "github.com/likexian/whois-parser" +) + +type provider interface { + remainingTime() (float64, error) +} + +type whoisClient struct { + domainAddress string + client *whois.Client +} + +func newProvider(config Config) (provider, error) { + domain := config.Source + client := whois.NewClient() + client.SetTimeout(config.Timeout.Duration()) + + return &whoisClient{ + domainAddress: domain, + client: client, + }, nil +} + +func (c *whoisClient) remainingTime() (float64, error) { + info, err := c.queryWhoisInfo() + if err != nil { + return 0, err + } + + if info.Domain.ExpirationDate == "" { + if !strings.HasPrefix(c.domainAddress, "=") { + // some servers support requesting extended data + // https://github.com/netdata/netdata/issues/17907#issuecomment-2171758380 + c.domainAddress = fmt.Sprintf("= %s", c.domainAddress) + return c.remainingTime() + } + } + + return parseWhoisInfoExpirationDate(info) +} + +func (c *whoisClient) queryWhoisInfo() (*whoisparser.WhoisInfo, error) { + resp, err := c.client.Whois(c.domainAddress) + if err != nil { + return nil, err + } + + info, err := whoisparser.Parse(resp) + if err != nil { + return nil, err + } + + return &info, nil +} + +func parseWhoisInfoExpirationDate(info *whoisparser.WhoisInfo) (float64, error) { + if info == nil || info.Domain == nil { + return 0, errors.New("nil Whois Info") + } + + if info.Domain.ExpirationDateInTime != nil { + return time.Until(*info.Domain.ExpirationDateInTime).Seconds(), nil + } + + date := info.Domain.ExpirationDate + if date == "" { + return 0, errors.New("no expiration date") + } + + if strings.Contains(date, " ") { + // https://community.netdata.cloud/t/whois-query-monitor-cannot-parse-expiration-time/3485 + if v, err := time.Parse("2006.01.02 15:04:05", date); err == nil { + return time.Until(v).Seconds(), nil + } + } + + expire, err := dateparse.ParseAny(date) + if err != nil { + return 0, err + } + + return time.Until(expire).Seconds(), nil +} diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/testdata/config.json b/src/go/collectors/go.d.plugin/modules/whoisquery/testdata/config.json new file mode 100644 index 000000000..e633bd4ed --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/testdata/config.json @@ -0,0 +1,7 @@ +{ + "update_every": 123, + "source": "ok", + "timeout": 123.123, + "days_until_expiration_warning": 123, + "days_until_expiration_critical": 123 +} diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/whoisquery/testdata/config.yaml new file mode 100644 index 000000000..ad4c501c0 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/testdata/config.yaml @@ -0,0 +1,5 @@ +update_every: 123 +source: "ok" +timeout: 123.123 +days_until_expiration_warning: 123 +days_until_expiration_critical: 123 diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/whoisquery.go b/src/go/collectors/go.d.plugin/modules/whoisquery/whoisquery.go new file mode 100644 index 000000000..1982f910d --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/whoisquery.go @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package whoisquery + +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("whoisquery", module.Creator{ + JobConfigSchema: configSchema, + Defaults: module.Defaults{ + UpdateEvery: 60, + }, + Create: func() module.Module { return New() }, + Config: func() any { return &Config{} }, + }) +} + +func New() *WhoisQuery { + return &WhoisQuery{ + Config: Config{ + Timeout: web.Duration(time.Second * 5), + DaysUntilWarn: 30, + DaysUntilCrit: 15, + }, + } +} + +type Config struct { + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"` + Source string `yaml:"source" json:"source"` + Timeout web.Duration `yaml:"timeout,omitempty" json:"timeout"` + DaysUntilWarn int64 `yaml:"days_until_expiration_warning,omitempty" json:"days_until_expiration_warning"` + DaysUntilCrit int64 `yaml:"days_until_expiration_critical,omitempty" json:"days_until_expiration_critical"` +} + +type WhoisQuery struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + + prov provider +} + +func (w *WhoisQuery) Configuration() any { + return w.Config +} + +func (w *WhoisQuery) Init() error { + if err := w.validateConfig(); err != nil { + w.Errorf("config validation: %v", err) + return err + } + + prov, err := w.initProvider() + if err != nil { + w.Errorf("init whois provider: %v", err) + return err + } + w.prov = prov + + w.charts = w.initCharts() + + return nil +} + +func (w *WhoisQuery) Check() error { + mx, err := w.collect() + if err != nil { + w.Error(err) + return err + } + if len(mx) == 0 { + return errors.New("no metrics collected") + } + return nil +} + +func (w *WhoisQuery) Charts() *module.Charts { + return w.charts +} + +func (w *WhoisQuery) Collect() map[string]int64 { + mx, err := w.collect() + if err != nil { + w.Error(err) + } + + if len(mx) == 0 { + return nil + } + return mx +} + +func (w *WhoisQuery) Cleanup() {} diff --git a/src/go/collectors/go.d.plugin/modules/whoisquery/whoisquery_test.go b/src/go/collectors/go.d.plugin/modules/whoisquery/whoisquery_test.go new file mode 100644 index 000000000..59ec659b1 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/whoisquery/whoisquery_test.go @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package whoisquery + +import ( + "errors" + "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") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + } { + require.NotNil(t, data, name) + } +} + +func TestWhoisQuery_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &WhoisQuery{}, dataConfigJSON, dataConfigYAML) +} + +func TestWhoisQuery_Cleanup(t *testing.T) { + New().Cleanup() +} + +func TestWhoisQuery_Charts(t *testing.T) { + whoisquery := New() + whoisquery.Source = "example.com" + require.NoError(t, whoisquery.Init()) + + assert.NotNil(t, whoisquery.Charts()) +} + +func TestWhoisQuery_Init(t *testing.T) { + const net = iota + tests := map[string]struct { + config Config + providerType int + err bool + }{ + "ok from net": { + config: Config{Source: "example.org"}, + providerType: net, + }, + "empty source": { + config: Config{Source: ""}, + err: true, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + whoisquery := New() + whoisquery.Config = test.config + + if test.err { + assert.Error(t, whoisquery.Init()) + } else { + require.NoError(t, whoisquery.Init()) + + var typeOK bool + if test.providerType == net { + _, typeOK = whoisquery.prov.(*whoisClient) + } + + assert.True(t, typeOK) + } + }) + } +} + +func TestWhoisQuery_Check(t *testing.T) { + whoisquery := New() + whoisquery.prov = &mockProvider{remTime: 12345.678} + + assert.NoError(t, whoisquery.Check()) +} + +func TestWhoisQuery_Check_ReturnsFalseOnProviderError(t *testing.T) { + whoisquery := New() + whoisquery.prov = &mockProvider{err: true} + + assert.Error(t, whoisquery.Check()) +} + +func TestWhoisQuery_Collect(t *testing.T) { + whoisquery := New() + whoisquery.Source = "example.com" + require.NoError(t, whoisquery.Init()) + whoisquery.prov = &mockProvider{remTime: 12345} + + collected := whoisquery.Collect() + + expected := map[string]int64{ + "expiry": 12345, + "days_until_expiration_warning": 30, + "days_until_expiration_critical": 15, + } + + assert.NotZero(t, collected) + assert.Equal(t, expected, collected) + ensureCollectedHasAllChartsDimsVarsIDs(t, whoisquery, collected) +} + +func TestWhoisQuery_Collect_ReturnsNilOnProviderError(t *testing.T) { + whoisquery := New() + whoisquery.Source = "example.com" + require.NoError(t, whoisquery.Init()) + whoisquery.prov = &mockProvider{err: true} + + assert.Nil(t, whoisquery.Collect()) +} + +func ensureCollectedHasAllChartsDimsVarsIDs(t *testing.T, whoisquery *WhoisQuery, collected map[string]int64) { + for _, chart := range *whoisquery.Charts() { + 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) + } + } +} + +type mockProvider struct { + remTime float64 + err bool +} + +func (m mockProvider) remainingTime() (float64, error) { + if m.err { + return 0, errors.New("mock remaining time error") + } + return m.remTime, nil +} |