Skip to content

Commit b96ac1d

Browse files
VenuEmmadiatoulme
authored andcommitted
Added support for endpoints in httpcheckreceiver (open-telemetry#37265)
#### Description This PR enhances the `httpcheckreceiver` by adding support for multiple endpoints (`endpoints`). Users can now specify a list of endpoints in addition to a single `endpoint` for each target. This improves flexibility and reduces redundancy when monitoring multiple similar endpoints. Additional changes include: - Updates to `config.go` to handle `endpoints`. - Updates to `scraper.go` to iterate over and scrape all specified endpoints. - Added unit tests for the new functionality in `config_test.go` and `scraper_test.go`. - Updated documentation (`README.md`) to reflect the changes. <!-- Issue number (e.g. open-telemetry#1234) or full URL to issue, if applicable. --> #### Link to Tracking Issue Fixes open-telemetry#37121 <!-- Describe what testing was performed and which tests were added. --> #### Testing - All existing and new tests pass. - Tested the `httpcheckreceiver` manually using the following configuration: ```yaml receivers: httpcheck: collection_interval: 30s targets: - method: "GET" endpoints: - "https://opentelemetry.io" - method: "GET" endpoints: - "http://localhost:8080/hello" - "http://localhost:8080/hello" headers: Authorization: "Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJqYXZhaW51c2UiLCJleHAiOjE3MzcwMzMzMTcsImlhdCI6MTczNzAxNTMxN30.qNb_hckvlqfWmnnaw2xP9ie2AKGO6ljzGxcMotoFZg3CwcYSTGu7VE6ERsvX_nHlcZOYZHgPc7_9WSBlCZ9M_w" - method: "GET" endpoint: "http://localhost:8080/hello" headers: Authorization: "Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJqYXZhaW51c2UiLCJleHAiOjE3MzcwMzMzMTcsImlhdCI6MTczNzAxNTMxN30.qNb_hckvlqfWmnnaw2xP9ie2AKGO6ljzGxcMotoFZg3CwcYSTGu7VE6ERsvX_nHlcZOYZHgPc7_9WSBlCZ9M_w" 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] ``` #### **Documentation** Describe any documentation changes or additions: ```markdown <!-- Describe the documentation added. --> #### Documentation - Updated the `README.md` to include examples for `endpoints`. - Verified `documentation.md` for metric output consistency. --------- Co-authored-by: Antoine Toulme <[email protected]>
1 parent e6de839 commit b96ac1d

File tree

5 files changed

+228
-31
lines changed

5 files changed

+228
-31
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: enhancement
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver)
7+
component: httpcheckreceiver
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
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."
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [37121]

receiver/httpcheckreceiver/README.md

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,46 @@ The following configuration settings are available:
3535

3636
Each target has the following properties:
3737

38-
- `endpoint` (required): the URL to be monitored
39-
- `method` (optional, default: `GET`): The HTTP method used to call the endpoint
38+
- `endpoint` (optional): A single URL to be monitored.
39+
- `endpoints` (optional): A list of URLs to be monitored.
40+
- `method` (optional, default: `GET`): The HTTP method used to call the endpoint or endpoints.
4041

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

4344
### Example Configuration
4445

4546
```yaml
4647
receivers:
4748
httpcheck:
49+
collection_interval: 30s
4850
targets:
49-
- endpoint: http://endpoint:80
50-
method: GET
51-
- endpoint: http://localhost:8080/health
52-
method: GET
53-
- endpoint: http://localhost:8081/health
54-
method: POST
51+
- method: "GET"
52+
endpoints:
53+
- "https://opentelemetry.io"
54+
- method: "GET"
55+
endpoints:
56+
- "http://localhost:8080/hello1"
57+
- "http://localhost:8080/hello2"
5558
headers:
56-
test-header: "test-value"
57-
collection_interval: 10s
59+
Authorization: "Bearer <your_bearer_token>"
60+
- method: "GET"
61+
endpoint: "http://localhost:8080/hello"
62+
headers:
63+
Authorization: "Bearer <your_bearer_token>"
64+
processors:
65+
batch:
66+
send_batch_max_size: 1000
67+
send_batch_size: 100
68+
timeout: 10s
69+
exporters:
70+
debug:
71+
verbosity: detailed
72+
service:
73+
pipelines:
74+
metrics:
75+
receivers: [httpcheck]
76+
processors: [batch]
77+
exporters: [debug]
5878
```
5979
6080
## Metrics

receiver/httpcheckreceiver/config.go

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import (
1717

1818
// Predefined error responses for configuration validation failures
1919
var (
20-
errMissingEndpoint = errors.New(`"endpoint" must be specified`)
2120
errInvalidEndpoint = errors.New(`"endpoint" must be in the form of <scheme>://<hostname>[:<port>]`)
21+
errMissingEndpoint = errors.New("at least one of 'endpoint' or 'endpoints' must be specified")
2222
)
2323

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

31+
// targetConfig defines configuration for individual HTTP checks.
3132
type targetConfig struct {
3233
confighttp.ClientConfig `mapstructure:",squash"`
33-
Method string `mapstructure:"method"`
34+
Method string `mapstructure:"method"`
35+
Endpoints []string `mapstructure:"endpoints"` // Field for a list of endpoints
3436
}
3537

36-
// Validate validates the configuration by checking for missing or invalid fields
38+
// Validate validates an individual targetConfig.
3739
func (cfg *targetConfig) Validate() error {
3840
var err error
3941

40-
if cfg.Endpoint == "" {
42+
// Ensure at least one of 'endpoint' or 'endpoints' is specified.
43+
if cfg.ClientConfig.Endpoint == "" && len(cfg.Endpoints) == 0 {
4144
err = multierr.Append(err, errMissingEndpoint)
42-
} else {
43-
_, parseErr := url.ParseRequestURI(cfg.Endpoint)
44-
if parseErr != nil {
45+
}
46+
47+
// Validate the single endpoint in ClientConfig.
48+
if cfg.ClientConfig.Endpoint != "" {
49+
if _, parseErr := url.ParseRequestURI(cfg.ClientConfig.Endpoint); parseErr != nil {
50+
err = multierr.Append(err, fmt.Errorf("%s: %w", errInvalidEndpoint.Error(), parseErr))
51+
}
52+
}
53+
54+
// Validate each endpoint in the Endpoints list.
55+
for _, endpoint := range cfg.Endpoints {
56+
if _, parseErr := url.ParseRequestURI(endpoint); parseErr != nil {
4557
err = multierr.Append(err, fmt.Errorf("%s: %w", errInvalidEndpoint.Error(), parseErr))
4658
}
4759
}
4860

4961
return err
5062
}
5163

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

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

73+
// Validate each targetConfig.
6074
for _, target := range cfg.Targets {
6175
err = multierr.Append(err, target.Validate())
6276
}

receiver/httpcheckreceiver/config_test.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,98 @@ func TestValidate(t *testing.T) {
105105
},
106106
expectedErr: nil,
107107
},
108+
{
109+
desc: "missing both endpoint and endpoints",
110+
cfg: &Config{
111+
Targets: []*targetConfig{
112+
{
113+
ClientConfig: confighttp.ClientConfig{},
114+
},
115+
},
116+
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
117+
},
118+
expectedErr: multierr.Combine(
119+
errMissingEndpoint,
120+
),
121+
},
122+
{
123+
desc: "invalid single endpoint",
124+
cfg: &Config{
125+
Targets: []*targetConfig{
126+
{
127+
ClientConfig: confighttp.ClientConfig{
128+
Endpoint: "invalid://endpoint: 12efg",
129+
},
130+
},
131+
},
132+
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
133+
},
134+
expectedErr: multierr.Combine(
135+
fmt.Errorf("%w: %s", errInvalidEndpoint, `parse "invalid://endpoint: 12efg": invalid port ": 12efg" after host`),
136+
),
137+
},
138+
{
139+
desc: "invalid endpoint in endpoints list",
140+
cfg: &Config{
141+
Targets: []*targetConfig{
142+
{
143+
Endpoints: []string{
144+
"https://valid.endpoint",
145+
"invalid://endpoint: 12efg",
146+
},
147+
},
148+
},
149+
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
150+
},
151+
expectedErr: multierr.Combine(
152+
fmt.Errorf("%w: %s", errInvalidEndpoint, `parse "invalid://endpoint: 12efg": invalid port ": 12efg" after host`),
153+
),
154+
},
155+
{
156+
desc: "missing scheme in single endpoint",
157+
cfg: &Config{
158+
Targets: []*targetConfig{
159+
{
160+
ClientConfig: confighttp.ClientConfig{
161+
Endpoint: "www.opentelemetry.io/docs",
162+
},
163+
},
164+
},
165+
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
166+
},
167+
expectedErr: multierr.Combine(
168+
fmt.Errorf("%w: %s", errInvalidEndpoint, `parse "www.opentelemetry.io/docs": invalid URI for request`),
169+
),
170+
},
171+
{
172+
desc: "valid single endpoint",
173+
cfg: &Config{
174+
Targets: []*targetConfig{
175+
{
176+
ClientConfig: confighttp.ClientConfig{
177+
Endpoint: "https://opentelemetry.io",
178+
},
179+
},
180+
},
181+
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
182+
},
183+
expectedErr: nil,
184+
},
185+
{
186+
desc: "valid endpoints list",
187+
cfg: &Config{
188+
Targets: []*targetConfig{
189+
{
190+
Endpoints: []string{
191+
"https://opentelemetry.io",
192+
"https://opentelemetry.io:80/docs",
193+
},
194+
},
195+
},
196+
ControllerConfig: scraperhelper.NewDefaultControllerConfig(),
197+
},
198+
expectedErr: nil,
199+
},
108200
}
109201

110202
for _, tc := range testCases {

receiver/httpcheckreceiver/scraper.go

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,43 @@ type httpcheckScraper struct {
3232
mb *metadata.MetricsBuilder
3333
}
3434

35-
// start starts the scraper by creating a new HTTP Client on the scraper
35+
// start initializes the scraper by creating HTTP clients for each endpoint.
3636
func (h *httpcheckScraper) start(ctx context.Context, host component.Host) (err error) {
37+
var expandedTargets []*targetConfig
38+
3739
for _, target := range h.cfg.Targets {
38-
client, clentErr := target.ToClient(ctx, host, h.settings)
39-
if clentErr != nil {
40-
err = multierr.Append(err, clentErr)
40+
// Create a unified list of endpoints
41+
var allEndpoints []string
42+
if len(target.Endpoints) > 0 {
43+
allEndpoints = append(allEndpoints, target.Endpoints...) // Add all endpoints
44+
}
45+
if target.ClientConfig.Endpoint != "" {
46+
allEndpoints = append(allEndpoints, target.ClientConfig.Endpoint) // Add single endpoint
47+
}
48+
49+
// Process each endpoint in the unified list
50+
for _, endpoint := range allEndpoints {
51+
client, clientErr := target.ToClient(ctx, host, h.settings)
52+
if clientErr != nil {
53+
h.settings.Logger.Error("failed to initialize HTTP client", zap.String("endpoint", endpoint), zap.Error(clientErr))
54+
err = multierr.Append(err, clientErr)
55+
continue
56+
}
57+
58+
// Clone the target and assign the specific endpoint
59+
targetClone := *target
60+
targetClone.ClientConfig.Endpoint = endpoint
61+
62+
h.clients = append(h.clients, client)
63+
expandedTargets = append(expandedTargets, &targetClone) // Add the cloned target to expanded targets
4164
}
42-
h.clients = append(h.clients, client)
4365
}
66+
67+
h.cfg.Targets = expandedTargets // Replace targets with expanded targets
4468
return
4569
}
4670

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

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

63-
req, err := http.NewRequestWithContext(ctx, h.cfg.Targets[targetIndex].Method, h.cfg.Targets[targetIndex].Endpoint, http.NoBody)
87+
req, err := http.NewRequestWithContext(
88+
ctx,
89+
h.cfg.Targets[targetIndex].Method,
90+
h.cfg.Targets[targetIndex].ClientConfig.Endpoint, // Use the ClientConfig.Endpoint
91+
http.NoBody,
92+
)
6493
if err != nil {
6594
h.settings.Logger.Error("failed to create request", zap.Error(err))
6695
return
6796
}
6897

98+
// Add headers to the request
99+
for key, value := range h.cfg.Targets[targetIndex].Headers {
100+
req.Header.Set(key, value.String()) // Convert configopaque.String to string
101+
}
102+
103+
// Send the request and measure response time
69104
start := time.Now()
70105
resp, err := targetClient.Do(req)
71106
mux.Lock()
72-
h.mb.RecordHttpcheckDurationDataPoint(now, time.Since(start).Milliseconds(), h.cfg.Targets[targetIndex].Endpoint)
107+
h.mb.RecordHttpcheckDurationDataPoint(
108+
now,
109+
time.Since(start).Milliseconds(),
110+
h.cfg.Targets[targetIndex].ClientConfig.Endpoint, // Use the correct endpoint
111+
)
73112

74113
statusCode := 0
75114
if err != nil {
76-
h.mb.RecordHttpcheckErrorDataPoint(now, int64(1), h.cfg.Targets[targetIndex].Endpoint, err.Error())
115+
h.mb.RecordHttpcheckErrorDataPoint(
116+
now,
117+
int64(1),
118+
h.cfg.Targets[targetIndex].ClientConfig.Endpoint,
119+
err.Error(),
120+
)
77121
} else {
78122
statusCode = resp.StatusCode
79123
}
80124

125+
// Record HTTP status class metrics
81126
for class, intVal := range httpResponseClasses {
82127
if statusCode/100 == intVal {
83-
h.mb.RecordHttpcheckStatusDataPoint(now, int64(1), h.cfg.Targets[targetIndex].Endpoint, int64(statusCode), req.Method, class)
128+
h.mb.RecordHttpcheckStatusDataPoint(
129+
now,
130+
int64(1),
131+
h.cfg.Targets[targetIndex].ClientConfig.Endpoint,
132+
int64(statusCode),
133+
req.Method,
134+
class,
135+
)
84136
} else {
85-
h.mb.RecordHttpcheckStatusDataPoint(now, int64(0), h.cfg.Targets[targetIndex].Endpoint, int64(statusCode), req.Method, class)
137+
h.mb.RecordHttpcheckStatusDataPoint(
138+
now,
139+
int64(0),
140+
h.cfg.Targets[targetIndex].ClientConfig.Endpoint,
141+
int64(statusCode),
142+
req.Method,
143+
class,
144+
)
86145
}
87146
}
88147
mux.Unlock()
89148
}(client, idx)
90149
}
91150

92151
wg.Wait()
93-
94152
return h.mb.Emit(), nil
95153
}
96154

0 commit comments

Comments
 (0)