Skip to content

Commit 5ba4976

Browse files
feat: add HTTP output method
1 parent 0020522 commit 5ba4976

File tree

5 files changed

+235
-4
lines changed

5 files changed

+235
-4
lines changed

internal/config/config.go

+10-2
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,11 @@ type PrometheusSDConfig struct {
4242

4343
// OutputConfig describes output configuration
4444
type OutputConfig struct {
45-
Method OutputMethod `short:"o" long:"method" description:"Output method." choice:"stdout" choice:"file" choice:"k8s-secret" env:"OUTPUT_METHOD" default:"stdout"`
46-
Format OutputFormat `long:"format" description:"Output format." choice:"scrape-configs" choice:"static-configs" choice:"merged-static-configs" env:"OUTPUT_FORMAT" default:"scrape-configs"`
45+
Method OutputMethod `short:"o" long:"method" description:"Output method." choice:"stdout" choice:"file" choice:"http" choice:"k8s-secret" env:"OUTPUT_METHOD" default:"stdout"`
46+
Format OutputFormat `long:"format" description:"Output format (irrelevant for HTTP output)." choice:"scrape-configs" choice:"static-configs" choice:"merged-static-configs" env:"OUTPUT_FORMAT" default:"scrape-configs"`
4747
Stdout StdoutOutputConfig `group:"Stdout Output Configuration" namespace:"stdout"`
4848
File FileOutputConfig `group:"File Output Configuration" namespace:"file"`
49+
Http HttpOutputConfig `group:"HTTP Output Configuration" namespace:"http"`
4950
K8sSecret K8sSecretOutputConfig `group:"Kubernetes Secret Output Configuration" namespace:"k8s-secret"`
5051
}
5152

@@ -65,6 +66,11 @@ type FileOutputConfig struct {
6566
Directory string `long:"directory" description:"Output directory." env:"OUTPUT_DIRECTORY" default:"/etc/prometheus/puppetdb-sd"`
6667
}
6768

69+
// HttpOutputConfig describes HTTP output configuration
70+
type HttpOutputConfig struct {
71+
Port int `long:"port" description:"HTTP server port." default:"8080"`
72+
}
73+
6874
// K8sSecretOutputConfig describes Kubernetes secret output configuration
6975
type K8sSecretOutputConfig struct {
7076
SecretName string `long:"secret-name" description:"Kubernetes secret name." env:"OUTPUT_K8S_SECRET_NAME"`
@@ -81,6 +87,8 @@ const (
8187
Stdout OutputMethod = "stdout"
8288
// File output method stores Prometheus configuration into files
8389
File OutputMethod = "file"
90+
// HTTP output method serves Prometheus configuration through an HTTP server
91+
Http OutputMethod = "http"
8492
// K8sSecret output method stores Prometheus configuration into Kubernetes secret
8593
K8sSecret OutputMethod = "k8s-secret"
8694

internal/outputs/http.go

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package outputs
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"errors"
8+
"fmt"
9+
"io"
10+
"net/http"
11+
"sync/atomic"
12+
13+
log "github.com/sirupsen/logrus"
14+
15+
"github.com/camptocamp/prometheus-puppetdb-sd/internal/config"
16+
"github.com/camptocamp/prometheus-puppetdb-sd/internal/types"
17+
)
18+
19+
// HttpOutput stores values needed by the HTTP output
20+
type HttpOutput struct {
21+
server *http.Server
22+
output atomic.Pointer[bytes.Buffer]
23+
}
24+
25+
func setupHttpOutput(cfg *config.OutputConfig) (*HttpOutput, error) {
26+
o := &HttpOutput{
27+
server: &http.Server{
28+
Addr: fmt.Sprintf(":%d", cfg.Http.Port),
29+
},
30+
}
31+
32+
http.Handle("/", o)
33+
34+
go func() {
35+
if err := o.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
36+
log.Fatalf("Failed to start HTTP server: %s", err)
37+
}
38+
}()
39+
40+
return o, nil
41+
}
42+
43+
// Write serves Prometheus configuration through an HTTP server
44+
func (o *HttpOutput) ServeHTTP(w http.ResponseWriter, _ *http.Request) {
45+
if o.output.Load() == nil {
46+
w.WriteHeader(404)
47+
return
48+
}
49+
50+
w.Header().Add("Content-Type", "application/json")
51+
52+
_, err := io.Copy(w, o.output.Load())
53+
if err != nil {
54+
log.Errorf("Failed to write response: %s", err)
55+
}
56+
}
57+
58+
// Write formats Prometheus configuration to be served through HTTP
59+
func (o *HttpOutput) Write(ctx context.Context, scrapeConfigs []*types.ScrapeConfig) (err error) {
60+
var staticConfigs []*types.StaticConfig
61+
62+
for _, scrapeConfig := range scrapeConfigs {
63+
staticConfigs = append(staticConfigs, scrapeConfig.StaticConfigs...)
64+
}
65+
66+
var b bytes.Buffer
67+
68+
encoder := json.NewEncoder(&b)
69+
encoder.SetIndent("", "\t")
70+
71+
err = encoder.Encode(staticConfigs)
72+
if err != nil {
73+
return
74+
}
75+
76+
o.output.Store(&b)
77+
78+
return
79+
}
80+
81+
// Close shuts down the HTTP server serving Prometheus configuration
82+
func (o *HttpOutput) Close(ctx context.Context) (err error) {
83+
return o.server.Shutdown(ctx)
84+
}

internal/outputs/http_test.go

+133
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
package outputs
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strings"
10+
"syscall"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
15+
"github.com/camptocamp/prometheus-puppetdb-sd/internal/config"
16+
)
17+
18+
var expectedHttpOutputs = []string{
19+
`
20+
[
21+
{
22+
"targets": [
23+
"server-1.example.com:9100"
24+
],
25+
"labels": {
26+
"certname": "server-1.example.com",
27+
"environment": "production",
28+
"team": "team-1"
29+
}
30+
},
31+
{
32+
"targets": [
33+
"server-2.example.com:9100"
34+
],
35+
"labels": {
36+
"certname": "server-2.example.com",
37+
"environment": "development",
38+
"team": "team-1"
39+
}
40+
},
41+
{
42+
"targets": [
43+
"server-1.example.com:9117"
44+
],
45+
"labels": {
46+
"certname": "server-1.example.com",
47+
"environment": "production",
48+
"team": "team-2"
49+
}
50+
}
51+
]
52+
`,
53+
`
54+
[
55+
{
56+
"targets": [
57+
"server-1.example.com:9100"
58+
],
59+
"labels": {
60+
"certname": "server-1.example.com",
61+
"environment": "production",
62+
"team": "team-1"
63+
}
64+
},
65+
{
66+
"targets": [
67+
"server-2.example.com:9100"
68+
],
69+
"labels": {
70+
"certname": "server-2.example.com",
71+
"environment": "development",
72+
"team": "team-2"
73+
}
74+
}
75+
]
76+
`,
77+
}
78+
79+
func TestHttpOutput(t *testing.T) {
80+
ctx, cancelFunc := context.WithCancel(context.Background())
81+
defer cancelFunc()
82+
83+
cfg := config.OutputConfig{
84+
Http: config.HttpOutputConfig{
85+
Port: 8000,
86+
},
87+
}
88+
89+
o, err := setupHttpOutput(&cfg)
90+
91+
url := fmt.Sprintf("http://localhost:%d", cfg.Http.Port)
92+
93+
assert.Nil(t, err)
94+
95+
resp, err := http.Get(url)
96+
if err != nil {
97+
assert.Fail(t, "Failed to request HTTP server", err)
98+
}
99+
100+
assert.Equal(t, 404, resp.StatusCode)
101+
102+
for i := range scrapeConfigs {
103+
err := o.Write(ctx, scrapeConfigs[i])
104+
if err != nil {
105+
assert.FailNow(t, "Failed to write output", err.Error())
106+
}
107+
108+
resp, err := http.Get(url)
109+
if err != nil {
110+
assert.Fail(t, "Failed to request HTTP server", err)
111+
}
112+
113+
assert.Equal(t, 200, resp.StatusCode)
114+
assert.Equal(t, "application/json", resp.Header.Get("Content-Type"))
115+
116+
output, err := io.ReadAll(resp.Body)
117+
if err != nil {
118+
assert.Fail(t, "Failed to read HTTP response", err)
119+
}
120+
121+
expectedOutput := expectedHttpOutputs[i]
122+
123+
assert.Equal(t, strings.TrimSpace(expectedOutput), strings.TrimSpace(string(output)))
124+
}
125+
126+
err = o.Close(ctx)
127+
if err != nil {
128+
assert.FailNow(t, "Failed to close output", err.Error())
129+
}
130+
131+
_, err = http.Get(url)
132+
assert.True(t, errors.Is(err, syscall.ECONNREFUSED))
133+
}

internal/outputs/outputs.go

+2
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ func Setup(cfg *config.OutputConfig) (Output, error) {
2121
return setupStdoutOutput(cfg)
2222
case config.File:
2323
return setupFileOutput(cfg)
24+
case config.Http:
25+
return setupHttpOutput(cfg)
2426
case config.K8sSecret:
2527
return setupK8sSecretOutput(cfg)
2628
default:

prometheus-puppetdb-sd.1

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.TH prometheus-puppetdb-sd 1 "26 October 2023"
1+
.TH prometheus-puppetdb-sd 1 "15 January 2025"
22
.SH NAME
33
prometheus-puppetdb-sd \- PuppetDB based service discovery for Prometheus
44
.SH SYNOPSIS
@@ -45,7 +45,7 @@ Prometheus target scraping proxy URL.
4545
Output method.
4646
.TP
4747
\fB\fB\-\-output.format\fR <default: \fI"scrape-configs"\fR>\fP
48-
Output format.
48+
Output format (irrelevant for HTTP output).
4949
.SS File Output Configuration
5050
.TP
5151
\fB\fB\-f\fR, \fB\-\-output.file.filename\fR <default: \fI"puppetdb-sd.yml"\fR>\fP
@@ -56,6 +56,10 @@ Output filename pattern ('*' is the placeholder).
5656
.TP
5757
\fB\fB\-\-output.file.directory\fR <default: \fI"/etc/prometheus/puppetdb-sd"\fR>\fP
5858
Output directory.
59+
.SS HTTP Output Configuration
60+
.TP
61+
\fB\fB\-\-output.http.port\fR <default: \fI"8080"\fR>\fP
62+
HTTP server port.
5963
.SS Kubernetes Secret Output Configuration
6064
.TP
6165
\fB\fB\-\-output.k8s-secret.secret-name\fR <default: \fI$OUTPUT_K8S_SECRET_NAME\fR>\fP

0 commit comments

Comments
 (0)