diff --git a/.chloggen/tlscheckreceiver-implementation.yaml b/.chloggen/tlscheckreceiver-implementation.yaml new file mode 100644 index 000000000000..85c44788abbe --- /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: breaking + +# 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: Changing configuration scheme to use standard confignet TCP client + +# 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 291df4b3deb0..f92b3483e907 100644 --- a/receiver/tlscheckreceiver/README.md +++ b/receiver/tlscheckreceiver/README.md @@ -20,12 +20,19 @@ By default, the TLS Check Receiver will emit a single metric, `tlscheck.time_lef ## Example Configuration +Targets are + ```yaml receivers: tlscheck: targets: - - url: https://example.com - - url: https://foobar.com:8080 + - endpoint: example.com:443 + dialer: + timeout: 15s + - endpoint: foobar.com:8080 + dialer: + timeout: 15s + - endpoint: localhost:10901 ``` ## Certificate Verification diff --git a/receiver/tlscheckreceiver/config.go b/receiver/tlscheckreceiver/config.go index 55fc395362a8..6f0e47f92347 100644 --- a/receiver/tlscheckreceiver/config.go +++ b/receiver/tlscheckreceiver/config.go @@ -6,8 +6,11 @@ package tlscheckreceiver // import "github.com/open-telemetry/opentelemetry-coll import ( "errors" "fmt" - "net/url" + "net" + "strconv" + "strings" + "go.opentelemetry.io/collector/config/confignet" "go.opentelemetry.io/collector/scraper/scraperhelper" "go.uber.org/multierr" @@ -15,48 +18,59 @@ 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 ://[:]`) -) +var errInvalidEndpoint = errors.New(`"endpoint" must be in the form of :`) // Config defines the configuration for the various elements of the receiver agent. type Config struct { scraperhelper.ControllerConfig `mapstructure:",squash"` metadata.MetricsBuilderConfig `mapstructure:",squash"` - Targets []*targetConfig `mapstructure:"targets"` + Targets []*confignet.TCPAddrConfig `mapstructure:"targets"` } -type targetConfig struct { - URL string `mapstructure:"url"` +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 { +func validateTarget(cfg *confignet.TCPAddrConfig) 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.Endpoint == "" { + return errMissingTargets + } + + if strings.Contains(cfg.Endpoint, "://") { + return fmt.Errorf("endpoint contains a scheme, which is not allowed: %s", cfg.Endpoint) + } + + _, port, parseErr := net.SplitHostPort(cfg.Endpoint) + if parseErr != nil { + return fmt.Errorf("%s: %w", errInvalidEndpoint.Error(), parseErr) + } + + portParseErr := validatePort(port) + if portParseErr != nil { + return fmt.Errorf("%s: %w", errInvalidEndpoint.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 { - err = multierr.Append(err, target.Validate()) + err = multierr.Append(err, validateTarget(target)) } return err diff --git a/receiver/tlscheckreceiver/config_test.go b/receiver/tlscheckreceiver/config_test.go index 4f0a9b46c06f..77f6b365a8c0 100644 --- a/receiver/tlscheckreceiver/config_test.go +++ b/receiver/tlscheckreceiver/config_test.go @@ -6,8 +6,10 @@ package tlscheckreceiver // import "github.com/open-telemetry/opentelemetry-coll import ( "fmt" "testing" + "time" "github.com/stretchr/testify/require" + "go.opentelemetry.io/collector/config/confignet" "go.opentelemetry.io/collector/scraper/scraperhelper" ) @@ -18,61 +20,88 @@ func TestValidate(t *testing.T) { expectedErr error }{ { - desc: "missing url", + desc: "missing targets", cfg: &Config{ - Targets: []*targetConfig{}, + Targets: []*confignet.TCPAddrConfig{}, ControllerConfig: scraperhelper.NewDefaultControllerConfig(), }, - expectedErr: errMissingURL, + expectedErr: errMissingTargets, }, { - desc: "invalid url", + desc: "invalid endpoint", cfg: &Config{ - Targets: []*targetConfig{ + Targets: []*confignet.TCPAddrConfig{ { - URL: "invalid://endpoint: 12efg", + Endpoint: "bad-endpoint: 12efg", + DialerConfig: confignet.DialerConfig{ + Timeout: 12 * time.Second, + }, }, }, ControllerConfig: scraperhelper.NewDefaultControllerConfig(), }, - expectedErr: fmt.Errorf("%w: %s", errInvalidURL, `parse "invalid://endpoint: 12efg": invalid port ": 12efg" after host`), + expectedErr: fmt.Errorf("%w: %s", errInvalidEndpoint, "provided port is not a number: 12efg"), }, { desc: "invalid config with multiple targets", cfg: &Config{ - Targets: []*targetConfig{ + Targets: []*confignet.TCPAddrConfig{ { - URL: "invalid://endpoint: 12efg", + Endpoint: "endpoint: 12efg", }, { - URL: "https://example.com", + Endpoint: "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", errInvalidEndpoint, `provided port is not a number: 12efg; endpoint contains a scheme, which is not allowed: https://example.com:80`), }, { - desc: "missing scheme", + desc: "port out of range", cfg: &Config{ - Targets: []*targetConfig{ + Targets: []*confignet.TCPAddrConfig{ { - URL: "www.opentelemetry.io/docs", + Endpoint: "www.opentelemetry.io:67000", }, }, ControllerConfig: scraperhelper.NewDefaultControllerConfig(), }, - expectedErr: fmt.Errorf("%w: %s", errInvalidURL, `parse "www.opentelemetry.io/docs": invalid URI for request`), + expectedErr: fmt.Errorf("%w: %s", errInvalidEndpoint, `provided port is out of valid range (1-65535): 67000`), + }, + { + desc: "missing port", + cfg: &Config{ + Targets: []*confignet.TCPAddrConfig{ + { + Endpoint: "www.opentelemetry.io/docs", + }, + }, + ControllerConfig: scraperhelper.NewDefaultControllerConfig(), + }, + expectedErr: fmt.Errorf("%w: %s", errInvalidEndpoint, `address www.opentelemetry.io/docs: missing port in address`), }, { desc: "valid config", cfg: &Config{ - Targets: []*targetConfig{ + Targets: []*confignet.TCPAddrConfig{ + { + Endpoint: "opentelemetry.io:443", + DialerConfig: confignet.DialerConfig{ + Timeout: 3 * time.Second, + }, + }, { - URL: "https://opentelemetry.io", + Endpoint: "opentelemetry.io:8080", + DialerConfig: confignet.DialerConfig{ + Timeout: 1 * time.Second, + }, }, { - URL: "https://opentelemetry.io:80/docs", + Endpoint: "111.222.33.44:10000", + DialerConfig: confignet.DialerConfig{ + Timeout: 5 * time.Second, + }, }, }, ControllerConfig: scraperhelper.NewDefaultControllerConfig(), diff --git a/receiver/tlscheckreceiver/documentation.md b/receiver/tlscheckreceiver/documentation.md index 1b0d5e7e5cf6..c0e4703b26ac 100644 --- a/receiver/tlscheckreceiver/documentation.md +++ b/receiver/tlscheckreceiver/documentation.md @@ -31,4 +31,4 @@ Time in seconds until certificate expiry, as specified by `NotAfter` field in th | Name | Description | Values | Enabled | | ---- | ----------- | ------ | ------- | -| tlscheck.url | Url at which the certificate was accessed. | Any Str | true | +| tlscheck.endpoint | Endpoint at which the certificate was accessed. | Any Str | true | diff --git a/receiver/tlscheckreceiver/factory.go b/receiver/tlscheckreceiver/factory.go index c2a2ba044972..d916615ed8f9 100644 --- a/receiver/tlscheckreceiver/factory.go +++ b/receiver/tlscheckreceiver/factory.go @@ -8,9 +8,10 @@ import ( "errors" "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/confignet" "go.opentelemetry.io/collector/consumer" "go.opentelemetry.io/collector/receiver" - "go.opentelemetry.io/collector/scraper" + collectorscraper "go.opentelemetry.io/collector/scraper" "go.opentelemetry.io/collector/scraper/scraperhelper" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/tlscheckreceiver/internal/metadata" @@ -18,7 +19,6 @@ import ( var errConfigNotTLSCheck = errors.New(`invalid config`) -// NewFactory creates a new filestats receiver factory. func NewFactory() receiver.Factory { return receiver.NewFactory( metadata.Type, @@ -27,10 +27,12 @@ func NewFactory() receiver.Factory { } func newDefaultConfig() component.Config { + cfg := scraperhelper.NewDefaultControllerConfig() + return &Config{ - ControllerConfig: scraperhelper.NewDefaultControllerConfig(), + ControllerConfig: cfg, MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), - Targets: []*targetConfig{}, + Targets: []*confignet.TCPAddrConfig{}, } } @@ -45,8 +47,8 @@ func newReceiver( return nil, errConfigNotTLSCheck } - mp := newScraper(tlsCheckConfig, settings) - s, err := scraper.NewMetrics(mp.scrape) + mp := newScraper(tlsCheckConfig, settings, getConnectionState) + s, err := collectorscraper.NewMetrics(mp.scrape) if err != nil { return nil, err } diff --git a/receiver/tlscheckreceiver/factory_test.go b/receiver/tlscheckreceiver/factory_test.go index dd102a4bfe37..80bde47fec24 100644 --- a/receiver/tlscheckreceiver/factory_test.go +++ b/receiver/tlscheckreceiver/factory_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" "go.opentelemetry.io/collector/component" + "go.opentelemetry.io/collector/config/confignet" "go.opentelemetry.io/collector/consumer/consumertest" "go.opentelemetry.io/collector/receiver/receivertest" "go.opentelemetry.io/collector/scraper/scraperhelper" @@ -40,7 +41,7 @@ func TestNewFactory(t *testing.T) { InitialDelay: time.Second, }, MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), - Targets: []*targetConfig{}, + Targets: []*confignet.TCPAddrConfig{}, } require.Equal(t, expectedCfg, factory.CreateDefaultConfig()) diff --git a/receiver/tlscheckreceiver/go.mod b/receiver/tlscheckreceiver/go.mod index b869272450a2..ad5aa845439e 100644 --- a/receiver/tlscheckreceiver/go.mod +++ b/receiver/tlscheckreceiver/go.mod @@ -7,6 +7,7 @@ require ( github.com/stretchr/testify v1.10.0 go.opentelemetry.io/collector/component v1.27.0 go.opentelemetry.io/collector/component/componenttest v0.121.0 + go.opentelemetry.io/collector/config/confignet v1.27.0 go.opentelemetry.io/collector/confmap v1.27.0 go.opentelemetry.io/collector/consumer v1.27.0 go.opentelemetry.io/collector/consumer/consumertest v0.121.0 diff --git a/receiver/tlscheckreceiver/go.sum b/receiver/tlscheckreceiver/go.sum index 57cd825716c2..dc615e7c1fe9 100644 --- a/receiver/tlscheckreceiver/go.sum +++ b/receiver/tlscheckreceiver/go.sum @@ -56,6 +56,8 @@ go.opentelemetry.io/collector/component v1.27.0 h1:6wk0K23YT9lSprX8BH9x5w8ssAORE go.opentelemetry.io/collector/component v1.27.0/go.mod h1:fIyBHoa7vDyZL3Pcidgy45cx24tBe7iHWne097blGgo= go.opentelemetry.io/collector/component/componenttest v0.121.0 h1:4q1/7WnP9LPKaY4HAd8/OkzhllZpRACKAOlWsqbrzqc= go.opentelemetry.io/collector/component/componenttest v0.121.0/go.mod h1:H7bEXDPMYNeWcHal0xyKlVfRPByVxale7hCJ+Myjq3Q= +go.opentelemetry.io/collector/config/confignet v1.27.0 h1:ows3rrFrEChC95nPjWTnbAvjlZoZY1zQ1BggsjqTY7I= +go.opentelemetry.io/collector/config/confignet v1.27.0/go.mod h1:HgpLwdRLzPTwbjpUXR0Wdt6pAHuYzaIr8t4yECKrEvo= go.opentelemetry.io/collector/confmap v1.27.0 h1:OIjPcjij1NxkVQsQVmHro4+t1eYNFiUGib9+J9YBZhM= go.opentelemetry.io/collector/confmap v1.27.0/go.mod h1:tmOa6iw3FJsEgfBHKALqvcdfRtf71JZGor0wSM5MoH8= go.opentelemetry.io/collector/consumer v1.27.0 h1:JoXdoCeFDJG3d9TYrKHvTT4eBhzKXDVTkWW5mDfnLiY= diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_config.go b/receiver/tlscheckreceiver/internal/metadata/generated_config.go index 96e738301b15..1c4ac5f210b6 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_config.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_config.go @@ -67,12 +67,12 @@ func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { // ResourceAttributesConfig provides config for tlscheck resource attributes. type ResourceAttributesConfig struct { - TlscheckURL ResourceAttributeConfig `mapstructure:"tlscheck.url"` + TlscheckEndpoint ResourceAttributeConfig `mapstructure:"tlscheck.endpoint"` } func DefaultResourceAttributesConfig() ResourceAttributesConfig { return ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{ + TlscheckEndpoint: ResourceAttributeConfig{ Enabled: true, }, } diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_config_test.go b/receiver/tlscheckreceiver/internal/metadata/generated_config_test.go index 31b6a74389ea..cf169774b334 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_config_test.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_config_test.go @@ -28,7 +28,7 @@ func TestMetricsBuilderConfig(t *testing.T) { TlscheckTimeLeft: MetricConfig{Enabled: true}, }, ResourceAttributes: ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{Enabled: true}, + TlscheckEndpoint: ResourceAttributeConfig{Enabled: true}, }, }, }, @@ -39,7 +39,7 @@ func TestMetricsBuilderConfig(t *testing.T) { TlscheckTimeLeft: MetricConfig{Enabled: false}, }, ResourceAttributes: ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{Enabled: false}, + TlscheckEndpoint: ResourceAttributeConfig{Enabled: false}, }, }, }, @@ -75,13 +75,13 @@ func TestResourceAttributesConfig(t *testing.T) { { name: "all_set", want: ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{Enabled: true}, + TlscheckEndpoint: ResourceAttributeConfig{Enabled: true}, }, }, { name: "none_set", want: ResourceAttributesConfig{ - TlscheckURL: ResourceAttributeConfig{Enabled: false}, + TlscheckEndpoint: ResourceAttributeConfig{Enabled: false}, }, }, } diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_metrics.go b/receiver/tlscheckreceiver/internal/metadata/generated_metrics.go index 32aa706428fd..66d1745a2af9 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_metrics.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_metrics.go @@ -104,11 +104,11 @@ func NewMetricsBuilder(mbc MetricsBuilderConfig, settings receiver.Settings, opt 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.TlscheckEndpoint.MetricsInclude != nil { + mb.resourceAttributeIncludeFilter["tlscheck.endpoint"] = filter.CreateFilter(mbc.ResourceAttributes.TlscheckEndpoint.MetricsInclude) } - if mbc.ResourceAttributes.TlscheckURL.MetricsExclude != nil { - mb.resourceAttributeExcludeFilter["tlscheck.url"] = filter.CreateFilter(mbc.ResourceAttributes.TlscheckURL.MetricsExclude) + if mbc.ResourceAttributes.TlscheckEndpoint.MetricsExclude != nil { + mb.resourceAttributeExcludeFilter["tlscheck.endpoint"] = filter.CreateFilter(mbc.ResourceAttributes.TlscheckEndpoint.MetricsExclude) } for _, op := range options { diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_metrics_test.go b/receiver/tlscheckreceiver/internal/metadata/generated_metrics_test.go index fdfcd2708cbf..a48e73919250 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_metrics_test.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_metrics_test.go @@ -73,7 +73,7 @@ func TestMetricsBuilder(t *testing.T) { mb.RecordTlscheckTimeLeftDataPoint(ts, 1, "tlscheck.x509.issuer-val", "tlscheck.x509.cn-val") rb := mb.NewResourceBuilder() - rb.SetTlscheckURL("tlscheck.url-val") + rb.SetTlscheckEndpoint("tlscheck.endpoint-val") res := rb.Emit() metrics := mb.Emit(WithResource(res)) diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_resource.go b/receiver/tlscheckreceiver/internal/metadata/generated_resource.go index e51961b1db39..5fcdaeb1e0b6 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_resource.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_resource.go @@ -21,10 +21,10 @@ func NewResourceBuilder(rac ResourceAttributesConfig) *ResourceBuilder { } } -// 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) +// SetTlscheckEndpoint sets provided value as "tlscheck.endpoint" attribute. +func (rb *ResourceBuilder) SetTlscheckEndpoint(val string) { + if rb.config.TlscheckEndpoint.Enabled { + rb.res.Attributes().PutStr("tlscheck.endpoint", val) } } diff --git a/receiver/tlscheckreceiver/internal/metadata/generated_resource_test.go b/receiver/tlscheckreceiver/internal/metadata/generated_resource_test.go index 4a67d0fd5ad5..f6e9901b119e 100644 --- a/receiver/tlscheckreceiver/internal/metadata/generated_resource_test.go +++ b/receiver/tlscheckreceiver/internal/metadata/generated_resource_test.go @@ -13,7 +13,7 @@ func TestResourceBuilder(t *testing.T) { t.Run(tt, func(t *testing.T) { cfg := loadResourceAttributesConfig(t, tt) rb := NewResourceBuilder(cfg) - rb.SetTlscheckURL("tlscheck.url-val") + rb.SetTlscheckEndpoint("tlscheck.endpoint-val") res := rb.Emit() assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource @@ -30,10 +30,10 @@ func TestResourceBuilder(t *testing.T) { assert.Failf(t, "unexpected test case: %s", tt) } - val, ok := res.Attributes().Get("tlscheck.url") + val, ok := res.Attributes().Get("tlscheck.endpoint") assert.True(t, ok) if ok { - assert.EqualValues(t, "tlscheck.url-val", val.Str()) + assert.EqualValues(t, "tlscheck.endpoint-val", val.Str()) } }) } diff --git a/receiver/tlscheckreceiver/internal/metadata/testdata/config.yaml b/receiver/tlscheckreceiver/internal/metadata/testdata/config.yaml index 7dc13e51f71c..db63aada104d 100644 --- a/receiver/tlscheckreceiver/internal/metadata/testdata/config.yaml +++ b/receiver/tlscheckreceiver/internal/metadata/testdata/config.yaml @@ -4,24 +4,24 @@ all_set: tlscheck.time_left: enabled: true resource_attributes: - tlscheck.url: + tlscheck.endpoint: enabled: true none_set: metrics: tlscheck.time_left: enabled: false resource_attributes: - tlscheck.url: + tlscheck.endpoint: enabled: false filter_set_include: resource_attributes: - tlscheck.url: + tlscheck.endpoint: enabled: true metrics_include: - regexp: ".*" filter_set_exclude: resource_attributes: - tlscheck.url: + tlscheck.endpoint: enabled: true metrics_exclude: - - strict: "tlscheck.url-val" + - strict: "tlscheck.endpoint-val" diff --git a/receiver/tlscheckreceiver/metadata.yaml b/receiver/tlscheckreceiver/metadata.yaml index 843444b4c35f..b2d52ff7653b 100644 --- a/receiver/tlscheckreceiver/metadata.yaml +++ b/receiver/tlscheckreceiver/metadata.yaml @@ -8,11 +8,10 @@ status: codeowners: active: [atoulme, michael-burt] - resource_attributes: - tlscheck.url: + tlscheck.endpoint: enabled: true - description: Url at which the certificate was accessed. + description: Endpoint at which the certificate was accessed. type: string attributes: diff --git a/receiver/tlscheckreceiver/scraper.go b/receiver/tlscheckreceiver/scraper.go index 7866afccd427..f02f7de24513 100644 --- a/receiver/tlscheckreceiver/scraper.go +++ b/receiver/tlscheckreceiver/scraper.go @@ -5,7 +5,12 @@ package tlscheckreceiver // import "github.com/open-telemetry/opentelemetry-coll import ( "context" + "crypto/tls" + "errors" + "sync" + "time" + "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/pdata/pmetric" "go.opentelemetry.io/collector/receiver" "go.uber.org/zap" @@ -13,19 +18,80 @@ import ( "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/tlscheckreceiver/internal/metadata" ) -type tlsCheckScraper struct { - // include string - logger *zap.Logger - mb *metadata.MetricsBuilder +var errMissingTargets = errors.New(`No targets specified`) + +type scraper struct { + cfg *Config + settings receiver.Settings + getConnectionState func(endpoint string) (tls.ConnectionState, error) } -func (s *tlsCheckScraper) scrape(_ context.Context) (pmetric.Metrics, error) { - return pmetric.NewMetrics(), nil +func getConnectionState(endpoint string) (tls.ConnectionState, error) { + conn, err := tls.Dial("tcp", endpoint, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return tls.ConnectionState{}, err + } + defer conn.Close() + return conn.ConnectionState(), nil +} + +func (s *scraper) scrapeEndpoint(endpoint string, metrics *pmetric.Metrics, wg *sync.WaitGroup, mux *sync.Mutex) { + defer wg.Done() + + state, err := s.getConnectionState(endpoint) + if err != nil { + s.settings.Logger.Error("TCP connection error encountered", zap.String("endpoint", endpoint), zap.Error(err)) + return + } + + s.settings.Logger.Info("Peer Certificates", zap.Int("certificates_count", len(state.PeerCertificates))) + if len(state.PeerCertificates) == 0 { + s.settings.Logger.Error("No TLS certificates found. Verify the endpoint serves TLS certificates.", zap.String("endpoint", endpoint)) + return + } + + cert := state.PeerCertificates[0] + issuer := cert.Issuer.String() + commonName := cert.Subject.CommonName + currentTime := time.Now() + timeLeft := cert.NotAfter.Sub(currentTime).Seconds() + timeLeftInt := int64(timeLeft) + now := pcommon.NewTimestampFromTime(time.Now()) + + mux.Lock() + defer mux.Unlock() + + mb := metadata.NewMetricsBuilder(s.cfg.MetricsBuilderConfig, s.settings, metadata.WithStartTime(pcommon.NewTimestampFromTime(time.Now()))) + rb := mb.NewResourceBuilder() + rb.SetTlscheckEndpoint(endpoint) + mb.RecordTlscheckTimeLeftDataPoint(now, timeLeftInt, issuer, commonName) + resourceMetrics := mb.Emit(metadata.WithResource(rb.Emit())) + resourceMetrics.ResourceMetrics().At(0).MoveTo(metrics.ResourceMetrics().AppendEmpty()) +} + +func (s *scraper) scrape(_ context.Context) (pmetric.Metrics, error) { + 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 + + metrics := pmetric.NewMetrics() + + for _, target := range s.cfg.Targets { + go s.scrapeEndpoint(target.Endpoint, &metrics, &wg, &mux) + } + + wg.Wait() + return metrics, nil } -func newScraper(cfg *Config, settings receiver.Settings) *tlsCheckScraper { - return &tlsCheckScraper{ - logger: settings.TelemetrySettings.Logger, - mb: metadata.NewMetricsBuilder(cfg.MetricsBuilderConfig, settings), +func newScraper(cfg *Config, settings receiver.Settings, getConnectionState func(endpoint string) (tls.ConnectionState, error)) *scraper { + return &scraper{ + cfg: cfg, + settings: settings, + getConnectionState: getConnectionState, } } diff --git a/receiver/tlscheckreceiver/scraper_test.go b/receiver/tlscheckreceiver/scraper_test.go new file mode 100644 index 000000000000..ecb8033db049 --- /dev/null +++ b/receiver/tlscheckreceiver/scraper_test.go @@ -0,0 +1,225 @@ +// 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/config/confignet" + "go.opentelemetry.io/collector/receiver/receivertest" + + "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/tlscheckreceiver/internal/metadata" +) + +//nolint:revive +func mockGetConnectionStateValid(endpoint string) (tls.ConnectionState, error) { + 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 +} + +//nolint:revive +func mockGetConnectionStateExpired(endpoint string) (tls.ConnectionState, error) { + 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 +} + +//nolint:revive +func mockGetConnectionStateNotYetValid(endpoint string) (tls.ConnectionState, error) { + 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_ValidCertificate(t *testing.T) { + cfg := &Config{ + Targets: []*confignet.TCPAddrConfig{ + {Endpoint: "example.com:443"}, + }, + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + } + factory := receivertest.NewNopFactory() + settings := receivertest.NewNopSettings(factory.Type()) + 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: []*confignet.TCPAddrConfig{ + {Endpoint: "expired.com:443"}, + }, + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + } + factory := receivertest.NewNopFactory() + settings := receivertest.NewNopSettings(factory.Type()) + 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, int64(0), "Time left should be negative for an expired certificate") +} + +func TestScrape_NotYetValidCertificate(t *testing.T) { + cfg := &Config{ + Targets: []*confignet.TCPAddrConfig{ + {Endpoint: "expired.com:443"}, + }, + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + } + factory := receivertest.NewNopFactory() + settings := receivertest.NewNopSettings(factory.Type()) + 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") +} + +func TestScrape_MultipleEndpoints(t *testing.T) { + cfg := &Config{ + Targets: []*confignet.TCPAddrConfig{ + {Endpoint: "example1.com:443"}, + {Endpoint: "example2.com:443"}, + {Endpoint: "example3.com:443"}, + }, + MetricsBuilderConfig: metadata.DefaultMetricsBuilderConfig(), + } + factory := receivertest.NewNopFactory() + settings := receivertest.NewNopSettings(factory.Type()) + s := newScraper(cfg, settings, mockGetConnectionStateValid) + + metrics, err := s.scrape(context.Background()) + require.NoError(t, err) + + // Verify we have metrics for all endpoints + assert.Equal(t, 3, metrics.ResourceMetrics().Len(), "Should have metrics for all endpoints") + + // Create a map of endpoints to their expected metrics + expectedMetrics := map[string]struct { + issuer string + commonName string + }{ + "example1.com:443": { + issuer: "CN=ValidIssuer", + commonName: "valid.com", + }, + "example2.com:443": { + issuer: "CN=ValidIssuer", + commonName: "valid.com", + }, + "example3.com:443": { + issuer: "CN=ValidIssuer", + commonName: "valid.com", + }, + } + + // Check each resource metric + for i := 0; i < metrics.ResourceMetrics().Len(); i++ { + rm := metrics.ResourceMetrics().At(i) + + // Get the endpoint resource attribute + endpoint, exists := rm.Resource().Attributes().Get("tlscheck.endpoint") + require.True(t, exists, "Resource should have tlscheck.endpoint attribute") + + endpointStr := endpoint.AsString() + expected, ok := expectedMetrics[endpointStr] + require.True(t, ok, "Unexpected endpoint found: %s", endpointStr) + + // Remove the endpoint from expected metrics as we've found it + delete(expectedMetrics, endpointStr) + + // Verify we have the expected metrics for this endpoint + ilms := rm.ScopeMetrics().At(0) + metric := ilms.Metrics().At(0) + dp := metric.Gauge().DataPoints().At(0) + + // Verify the metric attributes + attributes := dp.Attributes() + issuer, _ := attributes.Get("tlscheck.x509.issuer") + commonName, _ := attributes.Get("tlscheck.x509.cn") + + assert.Equal(t, expected.issuer, issuer.AsString(), "Incorrect issuer for endpoint %s", endpointStr) + assert.Equal(t, expected.commonName, commonName.AsString(), "Incorrect common name for endpoint %s", endpointStr) + } + + // Verify we found all expected endpoints + assert.Empty(t, expectedMetrics, "All expected endpoints should have been found") +}