Skip to content

Commit

Permalink
[receiver/tlscheck] Implement Scraper (#35823)
Browse files Browse the repository at this point in the history
<!--Ex. Fixing a bug - Describe the bug and how this fixes the issue.
Ex. Adding a feature - Explain what this achieves.-->
#### Description

Implements the scraper functionality of this receiver. It supports one
or more host-based checks which are executed in parallel.

<!-- Issue number (e.g. #1234) or full URL to issue, if applicable. -->
#### Link to tracking issue
Fixes #35842 

<!--Describe what testing was performed and which tests were added.-->
#### Testing

This includes tests for both valid and invalid certs. 

<!--Describe the documentation added.-->
#### Documentation

<!--Please delete paragraphs that you did not use before submitting.-->
  • Loading branch information
michael-burt authored Mar 7, 2025
1 parent 613a361 commit 1d3ff53
Show file tree
Hide file tree
Showing 19 changed files with 457 additions and 84 deletions.
27 changes: 27 additions & 0 deletions .chloggen/tlscheckreceiver-implementation.yaml
Original file line number Diff line number Diff line change
@@ -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]
11 changes: 9 additions & 2 deletions receiver/tlscheckreceiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 34 additions & 20 deletions receiver/tlscheckreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,57 +6,71 @@ 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"

"github.com/open-telemetry/opentelemetry-collector-contrib/receiver/tlscheckreceiver/internal/metadata"
)

// 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 <scheme>://<hostname>[:<port>]`)
)
var errInvalidEndpoint = errors.New(`"endpoint" must be in the form of <hostname>:<port>`)

// 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
Expand Down
65 changes: 47 additions & 18 deletions receiver/tlscheckreceiver/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -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(),
Expand Down
2 changes: 1 addition & 1 deletion receiver/tlscheckreceiver/documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
14 changes: 8 additions & 6 deletions receiver/tlscheckreceiver/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/scraper"
collectorscraper "go.opentelemetry.io/collector/scraper"
"go.opentelemetry.io/collector/scraper/scraperhelper"

"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,
Expand All @@ -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{},
}
}

Expand All @@ -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
}
Expand Down
3 changes: 2 additions & 1 deletion receiver/tlscheckreceiver/factory_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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())
Expand Down
1 change: 1 addition & 0 deletions receiver/tlscheckreceiver/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions receiver/tlscheckreceiver/go.sum

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 1d3ff53

Please sign in to comment.