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 d20c7b2e091d..e729e240d693 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/receiver/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 2aa2c47cd4bc..3b2b5c7b65ae 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/receiver/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..e14b57ffb40c 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.endpoint | Endpoint at which the certificate was accessed. | Any Str | diff --git a/receiver/tlscheckreceiver/factory.go b/receiver/tlscheckreceiver/factory.go index de3e0fcaa265..7604a144110f 100644 --- a/receiver/tlscheckreceiver/factory.go +++ b/receiver/tlscheckreceiver/factory.go @@ -8,17 +8,17 @@ 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/receiver/scraperhelper" - "go.opentelemetry.io/collector/scraper" + collectorscraper "go.opentelemetry.io/collector/scraper" "github.com/open-telemetry/opentelemetry-collector-contrib/receiver/tlscheckreceiver/internal/metadata" ) 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 2ca0e3d0ff0c..b3ee353e8345 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/receiver/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 08754d1c7b81..103e44cb1b0e 100644 --- a/receiver/tlscheckreceiver/go.mod +++ b/receiver/tlscheckreceiver/go.mod @@ -7,14 +7,14 @@ require ( github.com/stretchr/testify v1.10.0 go.opentelemetry.io/collector/component v0.115.1-0.20241206185113-3f3e208e71b8 go.opentelemetry.io/collector/component/componenttest v0.115.1-0.20241206185113-3f3e208e71b8 + go.opentelemetry.io/collector/config/confignet v1.21.0 go.opentelemetry.io/collector/confmap v1.21.1-0.20241206185113-3f3e208e71b8 go.opentelemetry.io/collector/consumer v1.21.1-0.20241206185113-3f3e208e71b8 go.opentelemetry.io/collector/consumer/consumertest v0.115.1-0.20241206185113-3f3e208e71b8 - go.opentelemetry.io/collector/filter v0.115.1-0.20241206185113-3f3e208e71b8 go.opentelemetry.io/collector/pdata v1.21.1-0.20241206185113-3f3e208e71b8 go.opentelemetry.io/collector/receiver v0.115.1-0.20241206185113-3f3e208e71b8 go.opentelemetry.io/collector/receiver/receivertest v0.115.1-0.20241206185113-3f3e208e71b8 - go.opentelemetry.io/collector/scraper v0.115.1-0.20241206185113-3f3e208e71b8 + go.opentelemetry.io/collector/scraper v0.115.0 go.uber.org/goleak v1.3.0 go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 diff --git a/receiver/tlscheckreceiver/go.sum b/receiver/tlscheckreceiver/go.sum index ede5aa60c910..dc6db4f8db4f 100644 --- a/receiver/tlscheckreceiver/go.sum +++ b/receiver/tlscheckreceiver/go.sum @@ -52,6 +52,8 @@ go.opentelemetry.io/collector/component v0.115.1-0.20241206185113-3f3e208e71b8 h go.opentelemetry.io/collector/component v0.115.1-0.20241206185113-3f3e208e71b8/go.mod h1:oIUFiH7w1eOimdeYhFI+gAIxYSiLDocKVJ0PTvX7d6s= go.opentelemetry.io/collector/component/componenttest v0.115.1-0.20241206185113-3f3e208e71b8 h1:Bic9twYk1GtkTNvzlt9rPCJEavRc5QYdSTN6Ug3hi9Q= go.opentelemetry.io/collector/component/componenttest v0.115.1-0.20241206185113-3f3e208e71b8/go.mod h1:PzXvNqKLCiSADZGZFKH+IOHMkaQ0GTHuzysfVbTPKYY= +go.opentelemetry.io/collector/config/confignet v1.21.0 h1:PeQ5YrMnfftysFL/WVaSrjPOWjD6DfeABY50pf9CZxU= +go.opentelemetry.io/collector/config/confignet v1.21.0/go.mod h1:ZppUH1hgUJOubawEsxsQ9MzEYFytqo2GnVSS7d4CVxc= go.opentelemetry.io/collector/config/configtelemetry v0.115.1-0.20241206185113-3f3e208e71b8 h1:b+0cqGeO0ZdILW5lsTzX29llVu1Me/Bxv0ya6iwOxcc= go.opentelemetry.io/collector/config/configtelemetry v0.115.1-0.20241206185113-3f3e208e71b8/go.mod h1:SlBEwQg0qly75rXZ6W1Ig8jN25KBVBkFIIAUI1GiAAE= go.opentelemetry.io/collector/confmap v1.21.1-0.20241206185113-3f3e208e71b8 h1:CNLAB32cTRsaRJCnb+1T9y6XxJzmDtEbo2svat6/b4g= @@ -64,8 +66,6 @@ go.opentelemetry.io/collector/consumer/consumerprofiles v0.115.1-0.2024120618511 go.opentelemetry.io/collector/consumer/consumerprofiles v0.115.1-0.20241206185113-3f3e208e71b8/go.mod h1:IzEmZ91Tp7TBxVDq8Cc9xvLsmO7H08njr6Pu9P5d9ns= go.opentelemetry.io/collector/consumer/consumertest v0.115.1-0.20241206185113-3f3e208e71b8 h1:zinrZujQGjMJhWo926FIwcIy4nMgwoYXnMe99nn0xDQ= go.opentelemetry.io/collector/consumer/consumertest v0.115.1-0.20241206185113-3f3e208e71b8/go.mod h1:ybjALRJWR6aKNOzEMy1T1ruCULVDEjj4omtOJMrH/kU= -go.opentelemetry.io/collector/filter v0.115.1-0.20241206185113-3f3e208e71b8 h1:cjX5D7OAa6CGzO9PGjVtRavxDbTs4w4xD+l2pJa35TE= -go.opentelemetry.io/collector/filter v0.115.1-0.20241206185113-3f3e208e71b8/go.mod h1:aewQ+jmvpH88gPVWpNXiWSm+wwJVxTK4f23ex2NMd2c= go.opentelemetry.io/collector/pdata v1.21.1-0.20241206185113-3f3e208e71b8 h1:PUaCJ1XIIomqXvFBF6hMFikhZIwoBc57UP7xlaRT//I= go.opentelemetry.io/collector/pdata v1.21.1-0.20241206185113-3f3e208e71b8/go.mod h1:GKb1/zocKJMvxKbS+sl0W85lxhYBTFJ6h6I1tphVyDU= go.opentelemetry.io/collector/pdata/pprofile v0.115.1-0.20241206185113-3f3e208e71b8 h1:+RGyM6ZhtNHRaiNbIiJ82Ik6TFmk3BCOgLvhw509Hc4= @@ -80,8 +80,8 @@ go.opentelemetry.io/collector/receiver/receiverprofiles v0.115.1-0.2024120618511 go.opentelemetry.io/collector/receiver/receiverprofiles v0.115.1-0.20241206185113-3f3e208e71b8/go.mod h1:05E5hGujWeeXJmzKZwTdHyZ/+rRyrQlQB5p5Q2XY39M= go.opentelemetry.io/collector/receiver/receivertest v0.115.1-0.20241206185113-3f3e208e71b8 h1:cOsmTAvpuiDHh5ggc/JnsF3nBFC9dQaswFvTDpujJqs= go.opentelemetry.io/collector/receiver/receivertest v0.115.1-0.20241206185113-3f3e208e71b8/go.mod h1:Y8Z9U/bz9Xpyt8GI8DxZZgryw3mnnIw+AeKVLTD2cP8= -go.opentelemetry.io/collector/scraper v0.115.1-0.20241206185113-3f3e208e71b8 h1:+Rim4sM6L+RqjaDYXmwSq+aaHEbE3svEvcHoIXmG48U= -go.opentelemetry.io/collector/scraper v0.115.1-0.20241206185113-3f3e208e71b8/go.mod h1:7YoCO6/4PeExLiX1FokcydJGCQUa7lUqZsqXokJ5VZ4= +go.opentelemetry.io/collector/scraper v0.115.0 h1:hbfebO7x1Xm96OwqeuLz5w7QAaB3ZMlwOkUo0XzPadc= +go.opentelemetry.io/collector/scraper v0.115.0/go.mod h1:7YoCO6/4PeExLiX1FokcydJGCQUa7lUqZsqXokJ5VZ4= go.opentelemetry.io/otel v1.32.0 h1:WnBN+Xjcteh0zdk01SVqV55d/m62NJLJdIyb4y/WO5U= go.opentelemetry.io/otel v1.32.0/go.mod h1:00DCVSB0RQcnzlwyTfqtxSm+DRr9hpYrHjNGiBHVQIg= go.opentelemetry.io/otel/metric v1.32.0 h1:xV2umtmNcThh2/a/aCP+h64Xx5wsj8qqnkYZktzNa0M= 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 31b6a74389ea..480057c85aca 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) - diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{}, ResourceAttributeConfig{})) + diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(MetricConfig{})) require.Emptyf(t, diff, "Config mismatch (-expected +actual):\n%s", diff) }) } @@ -62,46 +56,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) - diff := cmp.Diff(tt.want, cfg, cmpopts.IgnoreUnexported(ResourceAttributeConfig{})) - require.Emptyf(t, diff, "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..816b68a5184d 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, tlscheckEndpointAttributeValue 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.endpoint", tlscheckEndpointAttributeValue) } // 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, tlscheckEndpointAttributeValue string) { + mb.metricTlscheckTimeLeft.recordDataPoint(mb.startTime, ts, val, tlscheckX509IssuerAttributeValue, tlscheckX509CnAttributeValue, tlscheckEndpointAttributeValue) } // 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..11ef1fb0b6a0 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.endpoint-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.endpoint") + assert.True(t, ok) + assert.EqualValues(t, "tlscheck.endpoint-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..df834b74a18d 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.endpoint: + enabled: true + description: Endpoint 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.endpoint] \ No newline at end of file diff --git a/receiver/tlscheckreceiver/scraper.go b/receiver/tlscheckreceiver/scraper.go index 7866afccd427..51e079fc58e2 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,74 @@ 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 component.TelemetrySettings + mb *metadata.MetricsBuilder + 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, 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() + s.mb.RecordTlscheckTimeLeftDataPoint(now, timeLeftInt, issuer, commonName, endpoint) +} + +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 + + for _, target := range s.cfg.Targets { + go s.scrapeEndpoint(target.Endpoint, &wg, &mux) + } + + wg.Wait() + return s.mb.Emit(), 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.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..06e5a4b8d943 --- /dev/null +++ b/receiver/tlscheckreceiver/scraper_test.go @@ -0,0 +1,146 @@ +// 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" +) + +//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"}, + }, + } + 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: []*confignet.TCPAddrConfig{ + {Endpoint: "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, 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"}, + }, + } + 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") +}