Skip to content

Commit 0d0fb65

Browse files
enhancement: support http SD in scrapeconfigs (#4826)
* enhancement: support http SD in scrapeconfigs * Update CHANGELOG.md * Apply suggestions from code review * update-documentation * Update docs/sources/reference/components/prometheus/prometheus.operator.scrapeconfigs.md Co-authored-by: Clayton Cornell <[email protected]> * Update docs/sources/reference/components/prometheus/prometheus.operator.scrapeconfigs.md Co-authored-by: Clayton Cornell <[email protected]> * Update docs/sources/reference/components/prometheus/prometheus.operator.scrapeconfigs.md Co-authored-by: Clayton Cornell <[email protected]> --------- Co-authored-by: Clayton Cornell <[email protected]>
1 parent 5c599e6 commit 0d0fb65

File tree

4 files changed

+264
-16
lines changed

4 files changed

+264
-16
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ Main (unreleased)
2222

2323
- update promtail converter to use `file_match` block for `loki.source.file` instead of going through `local.file_match`. (@kalleep)
2424

25-
- Added `send_traceparent` option for `tracing` config to enable traceparent header propagation. (@MyDigitalLife)
25+
- Add `send_traceparent` option for `tracing` config to enable traceparent header propagation. (@MyDigitalLife)
26+
27+
- Add support for HTTP service discovery in `prometheus.operator.scrapeconfigs` component using `httpSDConfigs` in ScrapeConfig CRDs. (@QuentinBisson)
2628

2729
- Add `delay` option to `prometheus.exporter.cloudwatch` component to delay scraping of metrics to account for CloudWatch ingestion latency. (@tmeijn)
2830

docs/sources/reference/components/prometheus/prometheus.operator.scrapeconfigs.md

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,48 @@ You can run {{< param "PRODUCT_NAME" >}} from outside the cluster by supplying c
2626
`scrapeconfigs` may reference secrets for authenticating to targets to scrape them.
2727
In these cases, the secrets are loaded and refreshed only when the ScrapeConfig is updated or when this component refreshes its internal state, which happens on a 5-minute refresh cycle.
2828

29+
## Service Discovery Methods
30+
31+
ScrapeConfig resources support multiple service discovery mechanisms:
32+
33+
### Static Configuration
34+
35+
Static configurations define a fixed list of targets to scrape. This is useful when targets are known in advance and don't change frequently.
36+
37+
### HTTP Service Discovery
38+
39+
HTTP service discovery allows dynamic target discovery by querying an HTTP endpoint that returns target information in JSON format. The endpoint is polled at regular intervals to discover new targets or remove stale ones. This is particularly useful for:
40+
41+
- Dynamic environments where targets are frequently added or removed
42+
- Integration with external service registries
43+
- Custom service discovery implementations
44+
45+
The HTTP endpoint returns a JSON array of target groups, where each target group contains:
46+
47+
- `targets`: Array of `host:port` combinations to scrape
48+
- `labels`: Optional labels to apply to all targets in the group
49+
50+
Example JSON response:
51+
52+
```json
53+
[
54+
{
55+
"targets": ["service1.example.com:8080", "service2.example.com:8080"],
56+
"labels": {
57+
"job": "web-servers",
58+
"env": "production"
59+
}
60+
},
61+
{
62+
"targets": ["db1.example.com:9090"],
63+
"labels": {
64+
"job": "databases",
65+
"env": "production"
66+
}
67+
}
68+
]
69+
```
70+
2971
## Usage
3072

3173
```alloy
@@ -238,6 +280,74 @@ prometheus.operator.scrapeconfigs "scrapeconfigs" {
238280
}
239281
```
240282

283+
### Static Configuration Example
284+
285+
This example shows a ScrapeConfig resource using static target discovery:
286+
287+
```yaml
288+
apiVersion: monitoring.coreos.com/v1alpha1
289+
kind: ScrapeConfig
290+
metadata:
291+
name: static-targets
292+
namespace: monitoring
293+
spec:
294+
staticConfigs:
295+
- targets:
296+
- "web-server-1.example.com:8080"
297+
- "web-server-2.example.com:8080"
298+
labels:
299+
job: "web-servers"
300+
env: "production"
301+
metricsPath: /metrics
302+
scrapeInterval: 30s
303+
```
304+
305+
### HTTP Service Discovery Example
306+
307+
This example shows a ScrapeConfig resource using HTTP service discovery:
308+
309+
```yaml
310+
apiVersion: monitoring.coreos.com/v1alpha1
311+
kind: ScrapeConfig
312+
metadata:
313+
name: http-discovery
314+
namespace: monitoring
315+
spec:
316+
httpSDConfigs:
317+
- url: "http://service-registry.internal:8080/discover"
318+
refreshInterval: 60s
319+
metricsPath: /metrics
320+
scrapeInterval: 30s
321+
scrapeTimeout: 10s
322+
```
323+
324+
The HTTP endpoint (`http://service-registry.internal:8080/discover`) returns JSON in this format:
325+
326+
```json
327+
[
328+
{
329+
"targets": [
330+
"api-server-1.example.com:8080",
331+
"api-server-2.example.com:8080"
332+
],
333+
"labels": {
334+
"service": "api",
335+
"version": "v1.2.3"
336+
}
337+
},
338+
{
339+
"targets": [
340+
"worker-1.example.com:9090",
341+
"worker-2.example.com:9090"
342+
],
343+
"labels": {
344+
"service": "worker",
345+
"version": "v2.1.0"
346+
}
347+
}
348+
]
349+
```
350+
241351
## Extra Metric Labels
242352

243353
`prometheus.operator.scrapeconfigs` adds the following extra

internal/component/prometheus/operator/configgen/config_gen_scrapeconfig.go

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,46 @@ package configgen
55
import (
66
"fmt"
77
"strings"
8+
"time"
89

910
promopv1alpha1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1alpha1"
1011
"github.com/prometheus-operator/prometheus-operator/pkg/namespacelabeler"
12+
commonConfig "github.com/prometheus/common/config"
1113
"github.com/prometheus/common/model"
1214
"github.com/prometheus/prometheus/config"
1315
"github.com/prometheus/prometheus/discovery"
16+
"github.com/prometheus/prometheus/discovery/http"
1417
"github.com/prometheus/prometheus/discovery/targetgroup"
1518
"github.com/prometheus/prometheus/model/relabel"
1619
)
1720

1821
func (cg *ConfigGenerator) GenerateScrapeConfigConfigs(m *promopv1alpha1.ScrapeConfig) (cfg []*config.ScrapeConfig, errors []error) {
19-
cfg, errors = cg.generateStaticScrapeConfigConfigs(m, cfg, errors)
20-
return
21-
}
22-
23-
func (cg *ConfigGenerator) generateStaticScrapeConfigConfigs(m *promopv1alpha1.ScrapeConfig, cfg []*config.ScrapeConfig, errors []error) ([]*config.ScrapeConfig, []error) {
2422
for i, ep := range m.Spec.StaticConfigs {
25-
scrapeConfig, err := cg.generateStaticScrapeConfigConfig(m, ep, i)
26-
if err != nil {
23+
if scrapeConfig, err := cg.generateStaticScrapeConfigConfig(m, ep, i); err != nil {
24+
errors = append(errors, err)
25+
} else {
26+
cfg = append(cfg, scrapeConfig)
27+
}
28+
}
29+
for i, ep := range m.Spec.HTTPSDConfigs {
30+
if scrapeConfig, err := cg.generateHTTPScrapeConfigConfig(m, ep, i); err != nil {
2731
errors = append(errors, err)
2832
} else {
2933
cfg = append(cfg, scrapeConfig)
3034
}
3135
}
32-
return cfg, errors
36+
return
3337
}
3438

3539
func (cg *ConfigGenerator) generateStaticScrapeConfigConfig(m *promopv1alpha1.ScrapeConfig, sc promopv1alpha1.StaticConfig, i int) (cfg *config.ScrapeConfig, err error) {
3640
relabels := cg.initRelabelings()
3741
metricRelabels := relabeler{}
3842
cfg, err = cg.commonScrapeConfigConfig(m, i, &relabels, &metricRelabels)
39-
cfg.JobName = fmt.Sprintf("scrapeConfig/%s/%s/static/%d", m.Namespace, m.Name, i)
4043
if err != nil {
4144
return nil, err
4245
}
46+
cfg.JobName = fmt.Sprintf("scrapeConfig/%s/%s/static/%d", m.Namespace, m.Name, i)
47+
4348
targets := []model.LabelSet{}
4449
for _, target := range sc.Targets {
4550
targets = append(targets, model.LabelSet{
@@ -63,16 +68,57 @@ func (cg *ConfigGenerator) generateStaticScrapeConfigConfig(m *promopv1alpha1.Sc
6368
},
6469
}
6570
cfg.ServiceDiscoveryConfigs = append(cfg.ServiceDiscoveryConfigs, discoveryCfg)
66-
cfg.RelabelConfigs = relabels.configs
67-
cfg.MetricRelabelConfigs = metricRelabels.configs
68-
if m.Spec.ScrapeProtocols != nil {
69-
protocols, err := convertScrapeProtocols(m.Spec.ScrapeProtocols)
71+
return cg.finalizeScrapeConfig(cfg, &relabels, &metricRelabels)
72+
}
73+
74+
func (cg *ConfigGenerator) generateHTTPScrapeConfigConfig(m *promopv1alpha1.ScrapeConfig, httpSD promopv1alpha1.HTTPSDConfig, i int) (cfg *config.ScrapeConfig, err error) {
75+
relabels := cg.initRelabelings()
76+
metricRelabels := relabeler{}
77+
cfg, err = cg.commonScrapeConfigConfig(m, i, &relabels, &metricRelabels)
78+
if err != nil {
79+
return nil, err
80+
}
81+
cfg.JobName = fmt.Sprintf("scrapeConfig/%s/%s/http/%d", m.Namespace, m.Name, i)
82+
83+
// Convert HTTPSDConfig to Prometheus HTTP SD config
84+
httpSDConfig := &http.SDConfig{
85+
HTTPClientConfig: commonConfig.DefaultHTTPClientConfig,
86+
RefreshInterval: model.Duration(30 * time.Second), // Default refresh interval
87+
URL: httpSD.URL,
88+
}
89+
90+
// Set refresh interval if specified
91+
if httpSD.RefreshInterval != nil {
92+
if httpSDConfig.RefreshInterval, err = model.ParseDuration(string(*httpSD.RefreshInterval)); err != nil {
93+
return nil, fmt.Errorf("parsing refresh interval from HTTPSDConfig: %w", err)
94+
}
95+
}
96+
97+
// Add TLS configuration if specified
98+
if httpSD.TLSConfig != nil {
99+
if httpSDConfig.HTTPClientConfig.TLSConfig, err = cg.generateSafeTLS(*httpSD.TLSConfig, m.Namespace); err != nil {
100+
return nil, err
101+
}
102+
}
103+
104+
// Add BasicAuth if specified
105+
if httpSD.BasicAuth != nil {
106+
httpSDConfig.HTTPClientConfig.BasicAuth, err = cg.generateBasicAuth(*httpSD.BasicAuth, m.Namespace)
70107
if err != nil {
71108
return nil, err
72109
}
73-
cfg.ScrapeProtocols = protocols
74110
}
75-
return cfg, cfg.Validate(cg.ScrapeOptions.GlobalConfig())
111+
112+
// Add Authorization if specified
113+
if httpSD.Authorization != nil {
114+
httpSDConfig.HTTPClientConfig.Authorization, err = cg.generateAuthorization(*httpSD.Authorization, m.Namespace)
115+
if err != nil {
116+
return nil, err
117+
}
118+
}
119+
120+
cfg.ServiceDiscoveryConfigs = append(cfg.ServiceDiscoveryConfigs, httpSDConfig)
121+
return cg.finalizeScrapeConfig(cfg, &relabels, &metricRelabels)
76122
}
77123

78124
func (cg *ConfigGenerator) commonScrapeConfigConfig(m *promopv1alpha1.ScrapeConfig, _ int, relabels *relabeler, metricRelabels *relabeler) (cfg *config.ScrapeConfig, err error) {
@@ -93,6 +139,13 @@ func (cg *ConfigGenerator) commonScrapeConfigConfig(m *promopv1alpha1.ScrapeConf
93139
return nil, fmt.Errorf("parsing timeout from scrapeConfig: %w", err)
94140
}
95141
}
142+
if m.Spec.ScrapeProtocols != nil {
143+
protocols, err := convertScrapeProtocols(m.Spec.ScrapeProtocols)
144+
if err != nil {
145+
return nil, fmt.Errorf("converting scrape protocols: %w", err)
146+
}
147+
cfg.ScrapeProtocols = protocols
148+
}
96149
if m.Spec.MetricsPath != nil {
97150
cfg.MetricsPath = *m.Spec.MetricsPath
98151
}
@@ -143,3 +196,10 @@ func (cg *ConfigGenerator) commonScrapeConfigConfig(m *promopv1alpha1.ScrapeConf
143196
cfg.LabelValueLengthLimit = uint(defaultIfNil(m.Spec.LabelValueLengthLimit, 0))
144197
return cfg, err
145198
}
199+
200+
// finalizeScrapeConfig applies common finalization steps to a scrape config
201+
func (cg *ConfigGenerator) finalizeScrapeConfig(cfg *config.ScrapeConfig, relabels *relabeler, metricRelabels *relabeler) (*config.ScrapeConfig, error) {
202+
cfg.RelabelConfigs = relabels.configs
203+
cfg.MetricRelabelConfigs = metricRelabels.configs
204+
return cfg, cfg.Validate(cg.ScrapeOptions.GlobalConfig())
205+
}

internal/component/prometheus/operator/configgen/config_gen_scrapeconfig_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/prometheus/common/model"
1313
"github.com/prometheus/prometheus/config"
1414
"github.com/prometheus/prometheus/discovery"
15+
"github.com/prometheus/prometheus/discovery/http"
1516
"github.com/prometheus/prometheus/discovery/targetgroup"
1617
"github.com/prometheus/prometheus/model/relabel"
1718
"github.com/stretchr/testify/assert"
@@ -255,3 +256,78 @@ func TestGenerateStaticScrapeConfigConfig(t *testing.T) {
255256
})
256257
}
257258
}
259+
260+
func TestGenerateHTTPScrapeConfigConfig(t *testing.T) {
261+
suite := []struct {
262+
name string
263+
m *promopv1alpha1.ScrapeConfig
264+
ep promopv1alpha1.HTTPSDConfig
265+
expected *config.ScrapeConfig
266+
}{
267+
{
268+
name: "http service discovery",
269+
m: &promopv1alpha1.ScrapeConfig{
270+
ObjectMeta: metav1.ObjectMeta{
271+
Namespace: "test-namespace",
272+
Name: "test-scrapeconfig",
273+
},
274+
Spec: promopv1alpha1.ScrapeConfigSpec{
275+
MetricsPath: ptr.To("/metrics"),
276+
ScrapeInterval: ptr.To(promopv1.Duration("60s")),
277+
},
278+
},
279+
ep: promopv1alpha1.HTTPSDConfig{
280+
URL: "http://example-service.test-namespace:8080/sd",
281+
RefreshInterval: ptr.To(promopv1.Duration("15s")),
282+
},
283+
expected: &config.ScrapeConfig{
284+
JobName: "scrapeConfig/test-namespace/test-scrapeconfig/http/0",
285+
HonorTimestamps: true,
286+
ScrapeInterval: model.Duration(60 * time.Second),
287+
ScrapeTimeout: model.Duration(10 * time.Second),
288+
MetricsPath: "/metrics",
289+
Scheme: "http",
290+
ServiceDiscoveryConfigs: discovery.Configs{
291+
&http.SDConfig{
292+
HTTPClientConfig: commonConfig.DefaultHTTPClientConfig,
293+
RefreshInterval: model.Duration(15 * time.Second),
294+
URL: "http://example-service.test-namespace:8080/sd",
295+
},
296+
},
297+
},
298+
},
299+
}
300+
301+
for _, tc := range suite {
302+
t.Run(tc.name, func(t *testing.T) {
303+
cg := &ConfigGenerator{
304+
Client: &kubernetes.ClientArguments{},
305+
AdditionalRelabelConfigs: []*alloy_relabel.Config{
306+
{TargetLabel: "__meta_foo", Replacement: "bar"},
307+
},
308+
ScrapeOptions: operator.ScrapeOptions{
309+
DefaultScrapeInterval: time.Hour,
310+
DefaultScrapeTimeout: 42 * time.Second,
311+
},
312+
}
313+
got, err := cg.generateHTTPScrapeConfigConfig(tc.m, tc.ep, 0)
314+
require.NoError(t, err)
315+
316+
// Check job name
317+
assert.Equal(t, tc.expected.JobName, got.JobName)
318+
319+
// Check metrics path
320+
assert.Equal(t, tc.expected.MetricsPath, got.MetricsPath)
321+
322+
// Check scrape interval
323+
assert.Equal(t, tc.expected.ScrapeInterval, got.ScrapeInterval)
324+
325+
// Check service discovery configs
326+
require.Len(t, got.ServiceDiscoveryConfigs, 1)
327+
httpSD, ok := got.ServiceDiscoveryConfigs[0].(*http.SDConfig)
328+
require.True(t, ok, "Expected HTTP SD config")
329+
assert.Equal(t, "http://example-service.test-namespace:8080/sd", httpSD.URL)
330+
assert.Equal(t, model.Duration(15*time.Second), httpSD.RefreshInterval)
331+
})
332+
}
333+
}

0 commit comments

Comments
 (0)