Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for endpoints in httpcheckreceiver #37265

Merged
merged 15 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .chloggen/add-multiple-endpoints-support-httpcheckreceiver.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# 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: httpcheckreceiver

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: "Added support for specifying multiple endpoints in the `httpcheckreceiver` using the `endpoints` field. Users can now monitor multiple URLs with a single configuration block, improving flexibility and reducing redundancy."

# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
issues: [37121]
43 changes: 32 additions & 11 deletions receiver/httpcheckreceiver/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ httpcheck.status{http.status_class:4xx, http.status_code:200,...} = 0
httpcheck.status{http.status_class:5xx, http.status_code:200,...} = 0
```


VenuEmmadi marked this conversation as resolved.
Show resolved Hide resolved
## Configuration

The following configuration settings are available:
Expand All @@ -35,26 +36,46 @@ The following configuration settings are available:

Each target has the following properties:

- `endpoint` (required): the URL to be monitored
- `method` (optional, default: `GET`): The HTTP method used to call the endpoint
- `endpoint` (optional): A single URL to be monitored.
- `endpoints` (optional): A list of URLs to be monitored.
VenuEmmadi marked this conversation as resolved.
Show resolved Hide resolved
- `method` (optional, default: `GET`): The HTTP method used to call the endpoint or endpoints.

Additionally, each target supports the client configuration options of [confighttp].
At least one of `endpoint` or `endpoints` must be specified. Additionally, each target supports the client configuration options of [confighttp].

### Example Configuration

```yaml
receivers:
httpcheck:
collection_interval: 30s
targets:
- endpoint: http://endpoint:80
method: GET
- endpoint: http://localhost:8080/health
method: GET
- endpoint: http://localhost:8081/health
method: POST
- method: "GET"
endpoints:
- "https://opentelemetry.io"
- method: "GET"
endpoints:
- "http://localhost:8080/hello1"
- "http://localhost:8080/hello2"
headers:
Authorization: "Bearer <your_bearer_token>"
- method: "GET"
endpoint: "http://localhost:8080/hello"
headers:
test-header: "test-value"
collection_interval: 10s
Authorization: "Bearer <your_bearer_token>"
processors:
batch:
send_batch_max_size: 1000
send_batch_size: 100
timeout: 10s
exporters:
debug:
verbosity: detailed
service:
pipelines:
metrics:
receivers: [httpcheck]
processors: [batch]
exporters: [debug]
```

## Metrics
Expand Down
31 changes: 23 additions & 8 deletions receiver/httpcheckreceiver/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ import (

// Predefined error responses for configuration validation failures
var (
errMissingEndpoint = errors.New(`"endpoint" must be specified`)
// errMissingEndpoint = errors.New(`"endpoint" must be specified`)
VenuEmmadi marked this conversation as resolved.
Show resolved Hide resolved
errInvalidEndpoint = errors.New(`"endpoint" must be in the form of <scheme>://<hostname>[:<port>]`)
errMissingEndpoint = errors.New("at least one of 'endpoint' or 'endpoints' must be specified")
)

// Config defines the configuration for the various elements of the receiver agent.
Expand All @@ -28,35 +29,49 @@ type Config struct {
Targets []*targetConfig `mapstructure:"targets"`
}

// targetConfig defines configuration for individual HTTP checks.
type targetConfig struct {
confighttp.ClientConfig `mapstructure:",squash"`
Method string `mapstructure:"method"`
Method string `mapstructure:"method"`
Endpoints []string `mapstructure:"endpoints"` // Field for a list of endpoints
}

// Validate validates the configuration by checking for missing or invalid fields
// Validate validates an individual targetConfig.
func (cfg *targetConfig) Validate() error {
var err error

if cfg.Endpoint == "" {
// Ensure at least one of 'endpoint' or 'endpoints' is specified.
VenuEmmadi marked this conversation as resolved.
Show resolved Hide resolved
if cfg.ClientConfig.Endpoint == "" && len(cfg.Endpoints) == 0 {
err = multierr.Append(err, errMissingEndpoint)
} else {
_, parseErr := url.ParseRequestURI(cfg.Endpoint)
if parseErr != nil {
}

// Validate the single endpoint in ClientConfig.
if cfg.ClientConfig.Endpoint != "" {
if _, parseErr := url.ParseRequestURI(cfg.ClientConfig.Endpoint); parseErr != nil {
err = multierr.Append(err, fmt.Errorf("%s: %w", errInvalidEndpoint.Error(), parseErr))
}
}

// Validate each endpoint in the Endpoints list.
for _, endpoint := range cfg.Endpoints {
if _, parseErr := url.ParseRequestURI(endpoint); parseErr != nil {
err = multierr.Append(err, fmt.Errorf("%s: %w", errInvalidEndpoint.Error(), parseErr))
}
}

return err
}

// Validate validates the configuration by checking for missing or invalid fields
// Validate validates the top-level Config by checking each targetConfig.
func (cfg *Config) Validate() error {
var err error

// Ensure at least one target is configured.
if len(cfg.Targets) == 0 {
err = multierr.Append(err, errors.New("no targets configured"))
}

// Validate each targetConfig.
for _, target := range cfg.Targets {
err = multierr.Append(err, target.Validate())
}
Expand Down
92 changes: 92 additions & 0 deletions receiver/httpcheckreceiver/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,98 @@ func TestValidate(t *testing.T) {
},
expectedErr: nil,
},
{
desc: "missing both endpoint and endpoints",
cfg: &Config{
Targets: []*targetConfig{
{
ClientConfig: confighttp.ClientConfig{},
},
},
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
},
expectedErr: multierr.Combine(
errMissingEndpoint,
),
},
{
desc: "invalid single endpoint",
cfg: &Config{
Targets: []*targetConfig{
{
ClientConfig: confighttp.ClientConfig{
Endpoint: "invalid://endpoint: 12efg",
},
},
},
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
},
expectedErr: multierr.Combine(
fmt.Errorf("%w: %s", errInvalidEndpoint, `parse "invalid://endpoint: 12efg": invalid port ": 12efg" after host`),
),
},
{
desc: "invalid endpoint in endpoints list",
cfg: &Config{
Targets: []*targetConfig{
{
Endpoints: []string{
"https://valid.endpoint",
"invalid://endpoint: 12efg",
},
},
},
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
},
expectedErr: multierr.Combine(
fmt.Errorf("%w: %s", errInvalidEndpoint, `parse "invalid://endpoint: 12efg": invalid port ": 12efg" after host`),
),
},
{
desc: "missing scheme in single endpoint",
cfg: &Config{
Targets: []*targetConfig{
{
ClientConfig: confighttp.ClientConfig{
Endpoint: "www.opentelemetry.io/docs",
},
},
},
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
},
expectedErr: multierr.Combine(
fmt.Errorf("%w: %s", errInvalidEndpoint, `parse "www.opentelemetry.io/docs": invalid URI for request`),
),
},
{
desc: "valid single endpoint",
cfg: &Config{
Targets: []*targetConfig{
{
ClientConfig: confighttp.ClientConfig{
Endpoint: "https://opentelemetry.io",
},
},
},
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
},
expectedErr: nil,
},
{
desc: "valid endpoints list",
cfg: &Config{
Targets: []*targetConfig{
{
Endpoints: []string{
"https://opentelemetry.io",
"https://opentelemetry.io:80/docs",
},
},
},
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
},
expectedErr: nil,
},
}

for _, tc := range testCases {
Expand Down
82 changes: 70 additions & 12 deletions receiver/httpcheckreceiver/scraper.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,43 @@ type httpcheckScraper struct {
mb *metadata.MetricsBuilder
}

// start starts the scraper by creating a new HTTP Client on the scraper
// start initializes the scraper by creating HTTP clients for each endpoint.
func (h *httpcheckScraper) start(ctx context.Context, host component.Host) (err error) {
var expandedTargets []*targetConfig

for _, target := range h.cfg.Targets {
client, clentErr := target.ToClient(ctx, host, h.settings)
if clentErr != nil {
err = multierr.Append(err, clentErr)
// Create a unified list of endpoints
var allEndpoints []string
if len(target.Endpoints) > 0 {
allEndpoints = append(allEndpoints, target.Endpoints...) // Add all endpoints
}
if target.ClientConfig.Endpoint != "" {
allEndpoints = append(allEndpoints, target.ClientConfig.Endpoint) // Add single endpoint
}

// Process each endpoint in the unified list
for _, endpoint := range allEndpoints {
client, clientErr := target.ToClient(ctx, host, h.settings)
if clientErr != nil {
h.settings.Logger.Error("failed to initialize HTTP client", zap.String("endpoint", endpoint), zap.Error(clientErr))
err = multierr.Append(err, clientErr)
continue
}

// Clone the target and assign the specific endpoint
targetClone := *target
targetClone.ClientConfig.Endpoint = endpoint

h.clients = append(h.clients, client)
expandedTargets = append(expandedTargets, &targetClone) // Add the cloned target to expanded targets
}
h.clients = append(h.clients, client)
}

h.cfg.Targets = expandedTargets // Replace targets with expanded targets
return
}

// scrape connects to the endpoint and produces metrics based on the response
// scrape performs the HTTP checks and records metrics based on responses.
func (h *httpcheckScraper) scrape(ctx context.Context) (pmetric.Metrics, error) {
if len(h.clients) == 0 {
return pmetric.NewMetrics(), errClientNotInit
Expand All @@ -60,37 +84,71 @@ func (h *httpcheckScraper) scrape(ctx context.Context) (pmetric.Metrics, error)

now := pcommon.NewTimestampFromTime(time.Now())

req, err := http.NewRequestWithContext(ctx, h.cfg.Targets[targetIndex].Method, h.cfg.Targets[targetIndex].Endpoint, http.NoBody)
req, err := http.NewRequestWithContext(
ctx,
h.cfg.Targets[targetIndex].Method,
h.cfg.Targets[targetIndex].ClientConfig.Endpoint, // Use the ClientConfig.Endpoint
http.NoBody,
)
if err != nil {
h.settings.Logger.Error("failed to create request", zap.Error(err))
return
}

// Add headers to the request
for key, value := range h.cfg.Targets[targetIndex].Headers {
req.Header.Set(key, value.String()) // Convert configopaque.String to string
}

// Send the request and measure response time
start := time.Now()
resp, err := targetClient.Do(req)
mux.Lock()
h.mb.RecordHttpcheckDurationDataPoint(now, time.Since(start).Milliseconds(), h.cfg.Targets[targetIndex].Endpoint)
h.mb.RecordHttpcheckDurationDataPoint(
now,
time.Since(start).Milliseconds(),
h.cfg.Targets[targetIndex].ClientConfig.Endpoint, // Use the correct endpoint
)

statusCode := 0
if err != nil {
h.mb.RecordHttpcheckErrorDataPoint(now, int64(1), h.cfg.Targets[targetIndex].Endpoint, err.Error())
h.mb.RecordHttpcheckErrorDataPoint(
now,
int64(1),
h.cfg.Targets[targetIndex].ClientConfig.Endpoint,
err.Error(),
)
} else {
statusCode = resp.StatusCode
}

// Record HTTP status class metrics
for class, intVal := range httpResponseClasses {
if statusCode/100 == intVal {
h.mb.RecordHttpcheckStatusDataPoint(now, int64(1), h.cfg.Targets[targetIndex].Endpoint, int64(statusCode), req.Method, class)
h.mb.RecordHttpcheckStatusDataPoint(
now,
int64(1),
h.cfg.Targets[targetIndex].ClientConfig.Endpoint,
int64(statusCode),
req.Method,
class,
)
} else {
h.mb.RecordHttpcheckStatusDataPoint(now, int64(0), h.cfg.Targets[targetIndex].Endpoint, int64(statusCode), req.Method, class)
h.mb.RecordHttpcheckStatusDataPoint(
now,
int64(0),
h.cfg.Targets[targetIndex].ClientConfig.Endpoint,
int64(statusCode),
req.Method,
class,
)
}
}
mux.Unlock()
}(client, idx)
}

wg.Wait()

return h.mb.Emit(), nil
}

Expand Down
Loading