From 05448b6ad94e2bb53f33c5391c2d8d4d172c996b Mon Sep 17 00:00:00 2001 From: Michael Burt Date: Mon, 21 Oct 2024 22:37:17 -0600 Subject: [PATCH] [receiver/tlscheck] Implement Scraper --- .../tlscheckreceiver-implementation.yaml | 27 +++ receiver/tlscheckreceiver/README.md | 4 +- receiver/tlscheckreceiver/config.go | 47 +++-- receiver/tlscheckreceiver/config_test.go | 37 ++-- receiver/tlscheckreceiver/documentation.md | 7 +- receiver/tlscheckreceiver/factory.go | 9 +- receiver/tlscheckreceiver/go.mod | 1 - receiver/tlscheckreceiver/go.sum | 2 - .../internal/metadata/generated_config.go | 46 +---- .../metadata/generated_config_test.go | 52 +----- .../internal/metadata/generated_metrics.go | 55 ++---- .../metadata/generated_metrics_test.go | 18 +- .../internal/metadata/generated_resource.go | 36 ---- .../metadata/generated_resource_test.go | 40 ----- .../internal/metadata/testdata/config.yaml | 18 -- receiver/tlscheckreceiver/metadata.yaml | 11 +- receiver/tlscheckreceiver/scraper.go | 72 +++++++- receiver/tlscheckreceiver/scraper_test.go | 163 ++++++++++++++++++ 18 files changed, 351 insertions(+), 294 deletions(-) create mode 100644 .chloggen/tlscheckreceiver-implementation.yaml delete mode 100644 receiver/tlscheckreceiver/internal/metadata/generated_resource.go delete mode 100644 receiver/tlscheckreceiver/internal/metadata/generated_resource_test.go create mode 100644 receiver/tlscheckreceiver/scraper_test.go diff --git a/.chloggen/tlscheckreceiver-implementation.yaml b/.chloggen/tlscheckreceiver-implementation.yaml new file mode 100644 index 000000000000..d9695418d232 --- /dev/null +++ b/.chloggen/tlscheckreceiver-implementation.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: tlscheckreceiver + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Implement TLS Check Receiver for host-based checks + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [35842] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/receiver/tlscheckreceiver/README.md b/receiver/tlscheckreceiver/README.md index 1a5a89bf0317..a5b617719c95 100644 --- a/receiver/tlscheckreceiver/README.md +++ b/receiver/tlscheckreceiver/README.md @@ -24,8 +24,8 @@ By default, the TLS Check Receiver will emit a single metric, `tlscheck.time_lef receivers: tlscheck: targets: - - url: https://example.com - - url: https://foobar.com:8080 + - host: example.com:443 + - host: foobar.com:8080 ``` ## Certificate Verification diff --git a/receiver/tlscheckreceiver/config.go b/receiver/tlscheckreceiver/config.go index d20c7b2e091d..dae86fc0ce94 100644 --- a/receiver/tlscheckreceiver/config.go +++ b/receiver/tlscheckreceiver/config.go @@ -6,7 +6,9 @@ package tlscheckreceiver // import "github.com/open-telemetry/opentelemetry-coll import ( "errors" "fmt" - "net/url" + "net" + "strconv" + "strings" "go.opentelemetry.io/collector/receiver/scraperhelper" "go.uber.org/multierr" @@ -16,8 +18,7 @@ import ( // Predefined error responses for configuration validation failures var ( - errMissingURL = errors.New(`"url" must be specified`) - errInvalidURL = errors.New(`"url" must be in the form of ://[:]`) + errInvalidHost = errors.New(`"host" must be in the form of :`) ) // Config defines the configuration for the various elements of the receiver agent. @@ -28,31 +29,49 @@ type Config struct { } type targetConfig struct { - URL string `mapstructure:"url"` + Host string `mapstructure:"host"` +} + +func validatePort(port string) error { + portNum, err := strconv.Atoi(port) + if err != nil { + return fmt.Errorf("provided port is not a number: %s", port) + } + if portNum < 1 || portNum > 65535 { + return fmt.Errorf("provided port is out of valid range (1-65535): %d", portNum) + } + return nil } -// Validate validates the configuration by checking for missing or invalid fields func (cfg *targetConfig) Validate() error { var err error - if cfg.URL == "" { - err = multierr.Append(err, errMissingURL) - } else { - _, parseErr := url.ParseRequestURI(cfg.URL) - if parseErr != nil { - err = multierr.Append(err, fmt.Errorf("%s: %w", errInvalidURL.Error(), parseErr)) - } + if cfg.Host == "" { + return ErrMissingTargets + } + + if strings.Contains(cfg.Host, "://") { + return fmt.Errorf("host contains a scheme, which is not allowed: %s", cfg.Host) + } + + _, port, parseErr := net.SplitHostPort(cfg.Host) + if parseErr != nil { + return fmt.Errorf("%s: %w", errInvalidHost.Error(), parseErr) + } + + portParseErr := validatePort(port) + if portParseErr != nil { + return fmt.Errorf("%s: %w", errInvalidHost.Error(), portParseErr) } return err } -// Validate validates the configuration by checking for missing or invalid fields func (cfg *Config) Validate() error { var err error if len(cfg.Targets) == 0 { - err = multierr.Append(err, errMissingURL) + err = multierr.Append(err, ErrMissingTargets) } for _, target := range cfg.Targets { diff --git a/receiver/tlscheckreceiver/config_test.go b/receiver/tlscheckreceiver/config_test.go index 54e1748352c9..e3fa89c0d877 100644 --- a/receiver/tlscheckreceiver/config_test.go +++ b/receiver/tlscheckreceiver/config_test.go @@ -23,56 +23,71 @@ func TestValidate(t *testing.T) { Targets: []*targetConfig{}, ControllerConfig: scraperhelper.NewDefaultControllerConfig(), }, - expectedErr: errMissingURL, + expectedErr: ErrMissingTargets, }, { - desc: "invalid url", + desc: "invalid host", cfg: &Config{ Targets: []*targetConfig{ { - URL: "invalid://endpoint: 12efg", + Host: "endpoint: 12efg", }, }, ControllerConfig: scraperhelper.NewDefaultControllerConfig(), }, - expectedErr: fmt.Errorf("%w: %s", errInvalidURL, `parse "invalid://endpoint: 12efg": invalid port ": 12efg" after host`), + expectedErr: fmt.Errorf("%w: %s", errInvalidHost, "provided port is not a number: 12efg"), }, { desc: "invalid config with multiple targets", cfg: &Config{ Targets: []*targetConfig{ { - URL: "invalid://endpoint: 12efg", + Host: "endpoint: 12efg", }, { - URL: "https://example.com", + Host: "https://example.com:80", }, }, ControllerConfig: scraperhelper.NewDefaultControllerConfig(), }, - expectedErr: fmt.Errorf("%w: %s", errInvalidURL, `parse "invalid://endpoint: 12efg": invalid port ": 12efg" after host`), + expectedErr: fmt.Errorf("%w: %s", errInvalidHost, `provided port is not a number: 12efg; host contains a scheme, which is not allowed: https://example.com:80`), + }, + { + desc: "port out of range", + cfg: &Config{ + Targets: []*targetConfig{ + { + Host: "www.opentelemetry.io:67000", + }, + }, + ControllerConfig: scraperhelper.NewDefaultControllerConfig(), + }, + expectedErr: fmt.Errorf("%w: %s", errInvalidHost, `provided port is out of valid range (1-65535): 67000`), }, { desc: "missing scheme", cfg: &Config{ Targets: []*targetConfig{ { - URL: "www.opentelemetry.io/docs", + Host: "www.opentelemetry.io/docs", }, }, ControllerConfig: scraperhelper.NewDefaultControllerConfig(), }, - expectedErr: fmt.Errorf("%w: %s", errInvalidURL, `parse "www.opentelemetry.io/docs": invalid URI for request`), + expectedErr: fmt.Errorf("%w: %s", errInvalidHost, `address www.opentelemetry.io/docs: missing port in address`), }, { desc: "valid config", cfg: &Config{ Targets: []*targetConfig{ { - URL: "https://opentelemetry.io", + Host: "opentelemetry.io:443", + }, + { + Host: "opentelemetry.io:8080", }, { - URL: "https://opentelemetry.io:80/docs", + Host: "111.222.33.44:10000", }, }, ControllerConfig: scraperhelper.NewDefaultControllerConfig(), diff --git a/receiver/tlscheckreceiver/documentation.md b/receiver/tlscheckreceiver/documentation.md index 1b0d5e7e5cf6..cccf198f3681 100644 --- a/receiver/tlscheckreceiver/documentation.md +++ b/receiver/tlscheckreceiver/documentation.md @@ -26,9 +26,4 @@ Time in seconds until certificate expiry, as specified by `NotAfter` field in th | ---- | ----------- | ------ | | tlscheck.x509.issuer | The entity that issued the certificate. | Any Str | | tlscheck.x509.cn | The commonName in the subject of the certificate. | Any Str | - -## Resource Attributes - -| Name | Description | Values | Enabled | -| ---- | ----------- | ------ | ------- | -| tlscheck.url | Url at which the certificate was accessed. | Any Str | true | +| tlscheck.host | Host at which the certificate was accessed. | Any Str | diff --git a/receiver/tlscheckreceiver/factory.go b/receiver/tlscheckreceiver/factory.go index bc99c145abf7..7433fe4afdc4 100644 --- a/receiver/tlscheckreceiver/factory.go +++ b/receiver/tlscheckreceiver/factory.go @@ -6,6 +6,7 @@ package tlscheckreceiver // import "github.com/open-telemetry/opentelemetry-coll import ( "context" "errors" + "time" "go.opentelemetry.io/collector/component" "go.opentelemetry.io/collector/consumer" @@ -19,7 +20,6 @@ var ( errConfigNotTLSCheck = errors.New(`invalid config`) ) -// NewFactory creates a new filestats receiver factory. func NewFactory() receiver.Factory { return receiver.NewFactory( metadata.Type, @@ -28,8 +28,11 @@ func NewFactory() receiver.Factory { } func newDefaultConfig() component.Config { + cfg := scraperhelper.NewDefaultControllerConfig() + cfg.CollectionInterval = 60 * time.Second + return &Config{ - ControllerConfig: scraperhelper.NewDefaultControllerConfig(), + ControllerConfig: cfg, MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), Targets: []*targetConfig{}, } @@ -46,7 +49,7 @@ func newReceiver( return nil, errConfigNotTLSCheck } - mp := newScraper(tlsCheckConfig, settings) + mp := newScraper(tlsCheckConfig, settings, getConnectionState) s, err := scraperhelper.NewScraper(metadata.Type, mp.scrape) if err != nil { return nil, err diff --git a/receiver/tlscheckreceiver/go.mod b/receiver/tlscheckreceiver/go.mod index 30e7601ee72a..e9b9accfb1f9 100644 --- a/receiver/tlscheckreceiver/go.mod +++ b/receiver/tlscheckreceiver/go.mod @@ -9,7 +9,6 @@ require ( go.opentelemetry.io/collector/confmap v1.17.1-0.20241008154146-ea48c09c31ae go.opentelemetry.io/collector/consumer v0.111.1-0.20241008154146-ea48c09c31ae go.opentelemetry.io/collector/consumer/consumertest v0.111.1-0.20241008154146-ea48c09c31ae - go.opentelemetry.io/collector/filter v0.111.1-0.20241008154146-ea48c09c31ae go.opentelemetry.io/collector/pdata v1.17.1-0.20241008154146-ea48c09c31ae go.opentelemetry.io/collector/receiver v0.111.1-0.20241008154146-ea48c09c31ae go.uber.org/goleak v1.3.0 diff --git a/receiver/tlscheckreceiver/go.sum b/receiver/tlscheckreceiver/go.sum index 968b2bb483d2..b2d1f7ab2c0f 100644 --- a/receiver/tlscheckreceiver/go.sum +++ b/receiver/tlscheckreceiver/go.sum @@ -60,8 +60,6 @@ go.opentelemetry.io/collector/consumer/consumerprofiles v0.111.1-0.2024100815414 go.opentelemetry.io/collector/consumer/consumerprofiles v0.111.1-0.20241008154146-ea48c09c31ae/go.mod h1:GK0QMMiRBWl4IhIF/7ZKgzBlR9SdRSpRlqyNInN4ZoU= go.opentelemetry.io/collector/consumer/consumertest v0.111.1-0.20241008154146-ea48c09c31ae h1:HFj6D19fJYm3KV8QidQmMApmLjzoNkzh8El5OkTGySo= go.opentelemetry.io/collector/consumer/consumertest v0.111.1-0.20241008154146-ea48c09c31ae/go.mod h1:UDZRrSgaFAwWO6I34fj0KjabVAuBCAnmizsleyIe3I4= -go.opentelemetry.io/collector/filter v0.111.1-0.20241008154146-ea48c09c31ae h1:fLRV9bU33PJWQ/eZCwzfKdV0I9ljhP84Zoq9+tBhcLU= -go.opentelemetry.io/collector/filter v0.111.1-0.20241008154146-ea48c09c31ae/go.mod h1:74Acew42eexKiuLu3tVehyMK4b5XJPWXoJyNjK2FM+U= go.opentelemetry.io/collector/internal/globalsignal v0.111.0 h1:oq0nSD+7K2Q1Fx5d3s6lPRdKZeTL0FEg4sIaR7ZJzIc= go.opentelemetry.io/collector/internal/globalsignal v0.111.0/go.mod h1:GqMXodPWOxK5uqpX8MaMXC2389y2XJTa5nPwf8FYDK8= go.opentelemetry.io/collector/pdata v1.17.1-0.20241008154146-ea48c09c31ae h1:PcwZe1RD8tC4SZExhf0f5HqK+ZuWGsowHaBBU4PiUv0= diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_config.go b/receiver/tlscheckreceiver/internal/metadata/generated_config.go index 96e738301b15..a3a498b525c1 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_config.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_config.go @@ -4,7 +4,6 @@ package metadata import ( "go.opentelemetry.io/collector/confmap" - "go.opentelemetry.io/collector/filter" ) // MetricConfig provides common config for a particular metric. @@ -39,54 +38,13 @@ func DefaultMetricsConfig() MetricsConfig { } } -// ResourceAttributeConfig provides common config for a particular resource attribute. -type ResourceAttributeConfig struct { - Enabled bool `mapstructure:"enabled"` - // Experimental: MetricsInclude defines a list of filters for attribute values. - // If the list is not empty, only metrics with matching resource attribute values will be emitted. - MetricsInclude []filter.Config `mapstructure:"metrics_include"` - // Experimental: MetricsExclude defines a list of filters for attribute values. - // If the list is not empty, metrics with matching resource attribute values will not be emitted. - // MetricsInclude has higher priority than MetricsExclude. - MetricsExclude []filter.Config `mapstructure:"metrics_exclude"` - - enabledSetByUser bool -} - -func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { - if parser == nil { - return nil - } - err := parser.Unmarshal(rac) - if err != nil { - return err - } - rac.enabledSetByUser = parser.IsSet("enabled") - return nil -} - -// ResourceAttributesConfig provides config for tlscheck resource attributes. -type ResourceAttributesConfig struct { - TlscheckURL ResourceAttributeConfig `mapstructure:"tlscheck.url"` -} - -func DefaultResourceAttributesConfig() ResourceAttributesConfig { - return ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{ - Enabled: true, - }, - } -} - // MetricsBuilderConfig is a configuration for tlscheck metrics builder. type MetricsBuilderConfig struct { - Metrics MetricsConfig `mapstructure:"metrics"` - ResourceAttributes ResourceAttributesConfig `mapstructure:"resource_attributes"` + Metrics MetricsConfig `mapstructure:"metrics"` } func DefaultMetricsBuilderConfig() MetricsBuilderConfig { return MetricsBuilderConfig{ - Metrics: DefaultMetricsConfig(), - ResourceAttributes: DefaultResourceAttributesConfig(), + Metrics: DefaultMetricsConfig(), } } diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_config_test.go b/receiver/tlscheckreceiver/internal/metadata/generated_config_test.go index 7c84015ab981..fa7e3f38c50d 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_config_test.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_config_test.go @@ -27,9 +27,6 @@ func TestMetricsBuilderConfig(t *testing.T) { Metrics: MetricsConfig{ TlscheckTimeLeft: MetricConfig{Enabled: true}, }, - ResourceAttributes: ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{Enabled: true}, - }, }, }, { @@ -38,16 +35,13 @@ func TestMetricsBuilderConfig(t *testing.T) { Metrics: MetricsConfig{ TlscheckTimeLeft: MetricConfig{Enabled: false}, }, - ResourceAttributes: ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{Enabled: false}, - }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := loadMetricsBuilderConfig(t, tt.name) - if diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{}, ResourceAttributeConfig{})); diff != "" { + if diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{})); diff != "" { t.Errorf("Config mismatch (-expected +actual):\n%s", diff) } }) @@ -63,47 +57,3 @@ func loadMetricsBuilderConfig(t *testing.T, name string) MetricsBuilderConfig { require.NoError(t, sub.Unmarshal(&cfg)) return cfg } - -func TestResourceAttributesConfig(t *testing.T) { - tests := []struct { - name string - want ResourceAttributesConfig - }{ - { - name: "default", - want: DefaultResourceAttributesConfig(), - }, - { - name: "all_set", - want: ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{Enabled: true}, - }, - }, - { - name: "none_set", - want: ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{Enabled: false}, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cfg := loadResourceAttributesConfig(t, tt.name) - if diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(ResourceAttributeConfig{})); diff != "" { - t.Errorf("Config mismatch (-expected +actual):\n%s", diff) - } - }) - } -} - -func loadResourceAttributesConfig(t *testing.T, name string) ResourceAttributesConfig { - cm, err := confmaptest.LoadConf(filepath.Join("testdata", "config.yaml")) - require.NoError(t, err) - sub, err := cm.Sub(name) - require.NoError(t, err) - sub, err = sub.Sub("resource_attributes") - require.NoError(t, err) - cfg := DefaultResourceAttributesConfig() - require.NoError(t, sub.Unmarshal(&cfg)) - return cfg -} diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_metrics.go b/receiver/tlscheckreceiver/internal/metadata/generated_metrics.go index 988cf4f1cdcb..7aa27a53bfaf 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_metrics.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_metrics.go @@ -6,7 +6,6 @@ import ( "time" "go.opentelemetry.io/collector/component" - "go.opentelemetry.io/collector/filter" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/receiver" @@ -27,7 +26,7 @@ func (m *metricTlscheckTimeLeft) init() { m.data.Gauge().DataPoints().EnsureCapacity(m.capacity) } -func (m *metricTlscheckTimeLeft) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, tlscheckX509IssuerAttributeValue string, tlscheckX509CnAttributeValue string) { +func (m *metricTlscheckTimeLeft) recordDataPoint(start pcommon.Timestamp, ts pcommon.Timestamp, val int64, tlscheckX509IssuerAttributeValue string, tlscheckX509CnAttributeValue string, tlscheckHostAttributeValue string) { if !m.config.Enabled { return } @@ -37,6 +36,7 @@ func (m *metricTlscheckTimeLeft) recordDataPoint(start pcommon.Timestamp, ts pco dp.SetIntValue(val) dp.Attributes().PutStr("tlscheck.x509.issuer", tlscheckX509IssuerAttributeValue) dp.Attributes().PutStr("tlscheck.x509.cn", tlscheckX509CnAttributeValue) + dp.Attributes().PutStr("tlscheck.host", tlscheckHostAttributeValue) } // updateCapacity saves max length of data point slices that will be used for the slice capacity. @@ -67,14 +67,12 @@ func newMetricTlscheckTimeLeft(cfg MetricConfig) metricTlscheckTimeLeft { // MetricsBuilder provides an interface for scrapers to report metrics while taking care of all the transformations // required to produce metric representation defined in metadata and user config. type MetricsBuilder struct { - config MetricsBuilderConfig // config of the metrics builder. - startTime pcommon.Timestamp // start time that will be applied to all recorded data points. - metricsCapacity int // maximum observed number of metrics per resource. - metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. - buildInfo component.BuildInfo // contains version information. - resourceAttributeIncludeFilter map[string]filter.Filter - resourceAttributeExcludeFilter map[string]filter.Filter - metricTlscheckTimeLeft metricTlscheckTimeLeft + config MetricsBuilderConfig // config of the metrics builder. + startTime pcommon.Timestamp // start time that will be applied to all recorded data points. + metricsCapacity int // maximum observed number of metrics per resource. + metricsBuffer pmetric.Metrics // accumulates metrics data before emitting. + buildInfo component.BuildInfo // contains version information. + metricTlscheckTimeLeft metricTlscheckTimeLeft } // MetricBuilderOption applies changes to default metrics builder. @@ -97,19 +95,11 @@ func WithStartTime(startTime pcommon.Timestamp) MetricBuilderOption { func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, options ...MetricBuilderOption) *MetricsBuilder { mb := &MetricsBuilder{ - config: mbc, - startTime: pcommon.NewTimestampFromTime(time.Now()), - metricsBuffer: pmetric.NewMetrics(), - buildInfo: settings.BuildInfo, - metricTlscheckTimeLeft: newMetricTlscheckTimeLeft(mbc.Metrics.TlscheckTimeLeft), - resourceAttributeIncludeFilter: make(map[string]filter.Filter), - resourceAttributeExcludeFilter: make(map[string]filter.Filter), - } - if mbc.ResourceAttributes.TlscheckURL.MetricsInclude != nil { - mb.resourceAttributeIncludeFilter["tlscheck.url"] = filter.CreateFilter(mbc.ResourceAttributes.TlscheckURL.MetricsInclude) - } - if mbc.ResourceAttributes.TlscheckURL.MetricsExclude != nil { - mb.resourceAttributeExcludeFilter["tlscheck.url"] = filter.CreateFilter(mbc.ResourceAttributes.TlscheckURL.MetricsExclude) + config: mbc, + startTime: pcommon.NewTimestampFromTime(time.Now()), + metricsBuffer: pmetric.NewMetrics(), + buildInfo: settings.BuildInfo, + metricTlscheckTimeLeft: newMetricTlscheckTimeLeft(mbc.Metrics.TlscheckTimeLeft), } for _, op := range options { @@ -118,11 +108,6 @@ func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, opt return mb } -// NewResourceBuilder returns a new resource builder that should be used to build a resource associated with for the emitted metrics. -func (mb *MetricsBuilder) NewResourceBuilder() *ResourceBuilder { - return NewResourceBuilder(mb.config.ResourceAttributes) -} - // updateCapacity updates max length of metrics and resource attributes that will be used for the slice capacity. func (mb *MetricsBuilder) updateCapacity(rm pmetric.ResourceMetrics) { if mb.metricsCapacity < rm.ScopeMetrics().At(0).Metrics().Len() { @@ -185,16 +170,6 @@ func (mb *MetricsBuilder) EmitForResource(options ...ResourceMetricsOption) { for _, op := range options { op.apply(rm) } - for attr, filter := range mb.resourceAttributeIncludeFilter { - if val, ok := rm.Resource().Attributes().Get(attr); ok && !filter.Matches(val.AsString()) { - return - } - } - for attr, filter := range mb.resourceAttributeExcludeFilter { - if val, ok := rm.Resource().Attributes().Get(attr); ok && filter.Matches(val.AsString()) { - return - } - } if ils.Metrics().Len() > 0 { mb.updateCapacity(rm) @@ -213,8 +188,8 @@ func (mb *MetricsBuilder) Emit(options ...ResourceMetricsOption) pmetric.Metrics } // RecordTlscheckTimeLeftDataPoint adds a data point to tlscheck.time_left metric. -func (mb *MetricsBuilder) RecordTlscheckTimeLeftDataPoint(ts pcommon.Timestamp, val int64, tlscheckX509IssuerAttributeValue string, tlscheckX509CnAttributeValue string) { - mb.metricTlscheckTimeLeft.recordDataPoint(mb.startTime, ts, val, tlscheckX509IssuerAttributeValue, tlscheckX509CnAttributeValue) +func (mb *MetricsBuilder) RecordTlscheckTimeLeftDataPoint(ts pcommon.Timestamp, val int64, tlscheckX509IssuerAttributeValue string, tlscheckX509CnAttributeValue string, tlscheckHostAttributeValue string) { + mb.metricTlscheckTimeLeft.recordDataPoint(mb.startTime, ts, val, tlscheckX509IssuerAttributeValue, tlscheckX509CnAttributeValue, tlscheckHostAttributeValue) } // Reset resets metrics builder to its initial state. It should be used when external metrics source is restarted, diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_metrics_test.go b/receiver/tlscheckreceiver/internal/metadata/generated_metrics_test.go index b81713f24b82..5c37211d987f 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_metrics_test.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_metrics_test.go @@ -42,15 +42,6 @@ func TestMetricsBuilder(t *testing.T) { resAttrsSet: testDataSetNone, expectEmpty: true, }, - { - name: "filter_set_include", - resAttrsSet: testDataSetAll, - }, - { - name: "filter_set_exclude", - resAttrsSet: testDataSetAll, - expectEmpty: true, - }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -70,11 +61,9 @@ func TestMetricsBuilder(t *testing.T) { defaultMetricsCount++ allMetricsCount++ - mb.RecordTlscheckTimeLeftDataPoint(ts, 1, "tlscheck.x509.issuer-val", "tlscheck.x509.cn-val") + mb.RecordTlscheckTimeLeftDataPoint(ts, 1, "tlscheck.x509.issuer-val", "tlscheck.x509.cn-val", "tlscheck.host-val") - rb := mb.NewResourceBuilder() - rb.SetTlscheckURL("tlscheck.url-val") - res := rb.Emit() + res := pcommon.NewResource() metrics := mb.Emit(WithResource(res)) if tt.expectEmpty { @@ -114,6 +103,9 @@ func TestMetricsBuilder(t *testing.T) { attrVal, ok = dp.Attributes().Get("tlscheck.x509.cn") assert.True(t, ok) assert.EqualValues(t, "tlscheck.x509.cn-val", attrVal.Str()) + attrVal, ok = dp.Attributes().Get("tlscheck.host") + assert.True(t, ok) + assert.EqualValues(t, "tlscheck.host-val", attrVal.Str()) } } }) diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_resource.go b/receiver/tlscheckreceiver/internal/metadata/generated_resource.go deleted file mode 100644 index e51961b1db39..000000000000 --- a/receiver/tlscheckreceiver/internal/metadata/generated_resource.go +++ /dev/null @@ -1,36 +0,0 @@ -// Code generated by mdatagen. DO NOT EDIT. - -package metadata - -import ( - "go.opentelemetry.io/collector/pdata/pcommon" -) - -// ResourceBuilder is a helper struct to build resources predefined in metadata.yaml. -// The ResourceBuilder is not thread-safe and must not to be used in multiple goroutines. -type ResourceBuilder struct { - config ResourceAttributesConfig - res pcommon.Resource -} - -// NewResourceBuilder creates a new ResourceBuilder. This method should be called on the start of the application. -func NewResourceBuilder(rac ResourceAttributesConfig) *ResourceBuilder { - return &ResourceBuilder{ - config: rac, - res: pcommon.NewResource(), - } -} - -// SetTlscheckURL sets provided value as "tlscheck.url" attribute. -func (rb *ResourceBuilder) SetTlscheckURL(val string) { - if rb.config.TlscheckURL.Enabled { - rb.res.Attributes().PutStr("tlscheck.url", val) - } -} - -// Emit returns the built resource and resets the internal builder state. -func (rb *ResourceBuilder) Emit() pcommon.Resource { - r := rb.res - rb.res = pcommon.NewResource() - return r -} diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_resource_test.go b/receiver/tlscheckreceiver/internal/metadata/generated_resource_test.go deleted file mode 100644 index 4a67d0fd5ad5..000000000000 --- a/receiver/tlscheckreceiver/internal/metadata/generated_resource_test.go +++ /dev/null @@ -1,40 +0,0 @@ -// Code generated by mdatagen. DO NOT EDIT. - -package metadata - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestResourceBuilder(t *testing.T) { - for _, tt := range []string{"default", "all_set", "none_set"} { - t.Run(tt, func(t *testing.T) { - cfg := loadResourceAttributesConfig(t, tt) - rb := NewResourceBuilder(cfg) - rb.SetTlscheckURL("tlscheck.url-val") - - res := rb.Emit() - assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource - - switch tt { - case "default": - assert.Equal(t, 1, res.Attributes().Len()) - case "all_set": - assert.Equal(t, 1, res.Attributes().Len()) - case "none_set": - assert.Equal(t, 0, res.Attributes().Len()) - return - default: - assert.Failf(t, "unexpected test case: %s", tt) - } - - val, ok := res.Attributes().Get("tlscheck.url") - assert.True(t, ok) - if ok { - assert.EqualValues(t, "tlscheck.url-val", val.Str()) - } - }) - } -} diff --git a/receiver/tlscheckreceiver/internal/metadata/testdata/config.yaml b/receiver/tlscheckreceiver/internal/metadata/testdata/config.yaml index 7dc13e51f71c..b8974d99aa9d 100644 --- a/receiver/tlscheckreceiver/internal/metadata/testdata/config.yaml +++ b/receiver/tlscheckreceiver/internal/metadata/testdata/config.yaml @@ -3,25 +3,7 @@ all_set: metrics: tlscheck.time_left: enabled: true - resource_attributes: - tlscheck.url: - enabled: true none_set: metrics: tlscheck.time_left: enabled: false - resource_attributes: - tlscheck.url: - enabled: false -filter_set_include: - resource_attributes: - tlscheck.url: - enabled: true - metrics_include: - - regexp: ".*" -filter_set_exclude: - resource_attributes: - tlscheck.url: - enabled: true - metrics_exclude: - - strict: "tlscheck.url-val" diff --git a/receiver/tlscheckreceiver/metadata.yaml b/receiver/tlscheckreceiver/metadata.yaml index 843444b4c35f..c5cad04d1a24 100644 --- a/receiver/tlscheckreceiver/metadata.yaml +++ b/receiver/tlscheckreceiver/metadata.yaml @@ -8,14 +8,13 @@ status: codeowners: active: [atoulme, michael-burt] - resource_attributes: - tlscheck.url: - enabled: true - description: Url at which the certificate was accessed. - type: string attributes: + tlscheck.host: + enabled: true + description: Host at which the certificate was accessed. + type: string tlscheck.x509.issuer: enabled: true description: The entity that issued the certificate. @@ -32,4 +31,4 @@ metrics: gauge: value_type: int unit: "s" - attributes: [tlscheck.x509.issuer, tlscheck.x509.cn] \ No newline at end of file + attributes: [tlscheck.x509.issuer, tlscheck.x509.cn, tlscheck.host] \ No newline at end of file diff --git a/receiver/tlscheckreceiver/scraper.go b/receiver/tlscheckreceiver/scraper.go index c4807cc78eff..234c2e7be965 100644 --- a/receiver/tlscheckreceiver/scraper.go +++ b/receiver/tlscheckreceiver/scraper.go @@ -5,7 +5,13 @@ package tlscheckreceiver // import "github.com/open-telemetry/opentelemetry-coll import ( "context" + "crypto/tls" + "errors" + "sync" + "time" + "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/receiver" "go.uber.org/zap" @@ -13,19 +19,71 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/tlscheckreceiver/internal/metadata" ) +var ( + ErrMissingTargets = errors.New(`No targets specified`) +) + type scraper struct { - // include string - logger *zap.Logger - mb *metadata.MetricsBuilder + cfg *Config + settings component.TelemetrySettings + mb *metadata.MetricsBuilder + getConnectionState func(host string) (tls.ConnectionState, error) +} + +func getConnectionState(host string) (tls.ConnectionState, error) { + conn, err := tls.Dial("tcp", host, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return tls.ConnectionState{}, err + } + defer conn.Close() + return conn.ConnectionState(), nil } func (s *scraper) scrape(_ context.Context) (pmetric.Metrics, error) { - return pmetric.NewMetrics(), nil + if s.cfg == nil || len(s.cfg.Targets) == 0 { + return pmetric.NewMetrics(), ErrMissingTargets + } + + var wg sync.WaitGroup + wg.Add(len(s.cfg.Targets)) + var mux sync.Mutex + for _, target := range s.cfg.Targets { + go func(host string) { + defer wg.Done() + + now := pcommon.NewTimestampFromTime(time.Now()) + state, err := s.getConnectionState(target.Host) + mux.Lock() + if err != nil { + s.settings.Logger.Error("TCP connection error encountered", zap.String("host", target.Host), zap.Error(err)) + } + + s.settings.Logger.Error("Peer Certificates", zap.Int("certificates_count", len(state.PeerCertificates))) + if len(state.PeerCertificates) == 0 { + s.settings.Logger.Error("No TLS certificates found. Verify the host serves TLS certificates.", zap.String("host", target.Host)) + } + + cert := state.PeerCertificates[0] + issuer := cert.Issuer.String() + commonName := cert.Subject.CommonName + currentTime := time.Now() + timeLeft := cert.NotAfter.Sub(currentTime).Seconds() + timeLeftInt := int64(timeLeft) + s.mb.RecordTlscheckTimeLeftDataPoint(now, timeLeftInt, issuer, commonName, host) + mux.Unlock() + + }(target.Host) + } + + wg.Wait() + return s.mb.Emit(), nil } -func newScraper(cfg *Config, settings receiver.Settings) *scraper { +func newScraper(cfg *Config, settings receiver.Settings, getConnectionState func(host string) (tls.ConnectionState, error)) *scraper { return &scraper{ - logger: settings.TelemetrySettings.Logger, - mb: metadata.NewMetricsBuilder(cfg.MetricsBuilderConfig, settings), + cfg: cfg, + settings: settings.TelemetrySettings, + mb: metadata.NewMetricsBuilder(metadata.DefaultMetricsBuilderConfig(), settings), + getConnectionState: getConnectionState, } } diff --git a/receiver/tlscheckreceiver/scraper_test.go b/receiver/tlscheckreceiver/scraper_test.go new file mode 100644 index 000000000000..c02744c9e759 --- /dev/null +++ b/receiver/tlscheckreceiver/scraper_test.go @@ -0,0 +1,163 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package tlscheckreceiver + +import ( + "context" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/receiver/receivertest" +) + +func mockGetConnectionStateValid(host string) (tls.ConnectionState, error) { //nolint:revive + cert := &x509.Certificate{ + NotBefore: time.Now().Add(-1 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + Subject: pkix.Name{CommonName: "valid.com"}, + Issuer: pkix.Name{CommonName: "ValidIssuer"}, + } + return tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{cert}, + }, nil +} + +func mockGetConnectionStateExpired(host string) (tls.ConnectionState, error) { //nolint:revive + cert := &x509.Certificate{ + NotBefore: time.Now().Add(-48 * time.Hour), + NotAfter: time.Now().Add(-24 * time.Hour), + Subject: pkix.Name{CommonName: "expired.com"}, + Issuer: pkix.Name{CommonName: "ExpiredIssuer"}, + } + return tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{cert}, + }, nil +} + +func mockGetConnectionStateNotYetValid(host string) (tls.ConnectionState, error) { //nolint:revive + cert := &x509.Certificate{ + NotBefore: time.Now().Add(48 * time.Hour), + NotAfter: time.Now().Add(24 * time.Hour), + Subject: pkix.Name{CommonName: "notyetvalid.com"}, + Issuer: pkix.Name{CommonName: "NotYetValidIssuer"}, + } + return tls.ConnectionState{ + PeerCertificates: []*x509.Certificate{cert}, + }, nil +} + +func TestScrape_MulitpleHosts(t *testing.T) { + cfg := &Config{ + Targets: []*targetConfig{ + {Host: "example.com:443"}, + {Host: "foobar.com:443"}, + {Host: "testing.com:8080"}, + {Host: "localhost:8080"}, + {Host: "192.168.0.1:8080"}, + {Host: "8.8.8.8:53"}, + }, + } + settings := receivertest.NewNopSettings() + s := newScraper(cfg, settings, mockGetConnectionStateValid) + + metrics, err := s.scrape(context.Background()) + require.NoError(t, err) + + assert.Equal(t, 6, metrics.DataPointCount()) +} + +func TestScrape_ValidCertificate(t *testing.T) { + cfg := &Config{ + Targets: []*targetConfig{ + {Host: "example.com:443"}, + }, + } + settings := receivertest.NewNopSettings() + s := newScraper(cfg, settings, mockGetConnectionStateValid) + + metrics, err := s.scrape(context.Background()) + require.NoError(t, err) + + assert.Equal(t, 1, metrics.DataPointCount()) + + rm := metrics.ResourceMetrics().At(0) + ilms := rm.ScopeMetrics().At(0) + metric := ilms.Metrics().At(0) + dp := metric.Gauge().DataPoints().At(0) + + attributes := dp.Attributes() + issuer, _ := attributes.Get("tlscheck.x509.issuer") + commonName, _ := attributes.Get("tlscheck.x509.cn") + + assert.Equal(t, "CN=ValidIssuer", issuer.AsString()) + assert.Equal(t, "valid.com", commonName.AsString()) +} + +func TestScrape_ExpiredCertificate(t *testing.T) { + cfg := &Config{ + Targets: []*targetConfig{ + {Host: "expired.com:443"}, + }, + } + settings := receivertest.NewNopSettings() + s := newScraper(cfg, settings, mockGetConnectionStateExpired) + + metrics, err := s.scrape(context.Background()) + require.NoError(t, err) + + assert.Equal(t, 1, metrics.DataPointCount()) + + rm := metrics.ResourceMetrics().At(0) + ilms := rm.ScopeMetrics().At(0) + metric := ilms.Metrics().At(0) + dp := metric.Gauge().DataPoints().At(0) + + attributes := dp.Attributes() + issuer, _ := attributes.Get("tlscheck.x509.issuer") + commonName, _ := attributes.Get("tlscheck.x509.cn") + + assert.Equal(t, "CN=ExpiredIssuer", issuer.AsString()) + assert.Equal(t, "expired.com", commonName.AsString()) + + // Ensure that timeLeft is negative for an expired cert + timeLeft := dp.IntValue() + assert.Negative(t, timeLeft, "Time left should be negative for an expired certificate") +} + +func TestScrape_NotYetValidCertificate(t *testing.T) { + cfg := &Config{ + Targets: []*targetConfig{ + {Host: "expired.com:443"}, + }, + } + settings := receivertest.NewNopSettings() + s := newScraper(cfg, settings, mockGetConnectionStateNotYetValid) + + metrics, err := s.scrape(context.Background()) + require.NoError(t, err) + + assert.Equal(t, 1, metrics.DataPointCount()) + + rm := metrics.ResourceMetrics().At(0) + ilms := rm.ScopeMetrics().At(0) + metric := ilms.Metrics().At(0) + dp := metric.Gauge().DataPoints().At(0) + + attributes := dp.Attributes() + issuer, _ := attributes.Get("tlscheck.x509.issuer") + commonName, _ := attributes.Get("tlscheck.x509.cn") + + assert.Equal(t, "CN=NotYetValidIssuer", issuer.AsString()) + assert.Equal(t, "notyetvalid.com", commonName.AsString()) + + // Ensure that timeLeft is positive for a not-yet-valid cert + timeLeft := dp.IntValue() + assert.Positive(t, timeLeft, "Time left should be positive for a not-yet-valid cert") +}