Skip to content

Commit 4cddb27

Browse files
committed
init
0 parents  commit 4cddb27

File tree

2 files changed

+202
-0
lines changed

2 files changed

+202
-0
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Promgate
2+
3+
Promgate is a pure go-stdlib mutal tls reverse proxy to merge multiple prometheus exporters together.
4+
5+
All exporters are are queried in parallel and appendend line wise. The goal is to transmit the metrics, not to keep the help texts.
6+
7+
It's configured entirely using environment variables. You may scream for flags, but i happen like Systemd's environment files.
8+
9+
Usage:
10+
11+
```
12+
CA=ca.pem \
13+
CRL=crl.pem \
14+
CERT=cert.pem \
15+
KEY=key.pem \
16+
URLS=http://localhost:9100/metrics,http://localhost:9101 \
17+
go run promgate.go
18+
```
19+
20+
**Note**: you have to supply all of these options.
21+
22+
`URLS` is a single URL or a list of comma sperated urls. The scheme (i.e. http://) is required.

promgate.go

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"context"
6+
"crypto/tls"
7+
"crypto/x509"
8+
"crypto/x509/pkix"
9+
"errors"
10+
"fmt"
11+
"io/ioutil"
12+
"log"
13+
"net/http"
14+
"net/url"
15+
"os"
16+
"regexp"
17+
"strings"
18+
"sync"
19+
"time"
20+
)
21+
22+
type configStore struct {
23+
urls []string
24+
listen string
25+
certFile string
26+
certKey string
27+
clientCAs *x509.CertPool
28+
crl *pkix.CertificateList
29+
}
30+
31+
var (
32+
config configStore
33+
re *regexp.Regexp
34+
)
35+
36+
func init() {
37+
urls := getEnv("URLS", "http://localhost:9100")
38+
listen := getEnv("LISTEN", "0.0.0.0:9443")
39+
40+
caFile := getEnv("CA", "")
41+
caData, err := ioutil.ReadFile(caFile)
42+
if err != nil {
43+
log.Fatal("Unable to load ca from $CA.")
44+
}
45+
clientCAs := x509.NewCertPool()
46+
if ok := clientCAs.AppendCertsFromPEM(caData); !ok {
47+
log.Fatal("Unable to parse $CA as ca.")
48+
49+
}
50+
51+
crlFile := getEnv("CRL", "")
52+
crlData, err := ioutil.ReadFile(crlFile)
53+
if err != nil {
54+
log.Fatal("Unable to load crl from $CRL.")
55+
}
56+
crl, err := x509.ParseCRL(crlData)
57+
if err != nil {
58+
log.Fatal("Unable to parse $CRL as crl.")
59+
}
60+
61+
certFile := getEnv("CERT", "")
62+
keyFile := getEnv("KEY", "")
63+
64+
config = configStore{
65+
strings.Split(urls, ","),
66+
listen,
67+
certFile,
68+
keyFile,
69+
clientCAs,
70+
crl,
71+
}
72+
73+
re = regexp.MustCompile("^(?P<name>[^#][^ {]+)({(?P<labels>.*)})? (?P<value>.*)")
74+
}
75+
76+
func main() {
77+
tlsConfig := &tls.Config{
78+
ClientAuth: tls.RequireAndVerifyClientCert,
79+
ClientCAs: config.clientCAs,
80+
}
81+
s := &http.Server{
82+
Addr: config.listen,
83+
TLSConfig: tlsConfig,
84+
}
85+
http.HandleFunc("/metrics", withTlsClientCheck(metrics))
86+
log.Fatal(s.ListenAndServeTLS(config.certFile, config.certKey))
87+
}
88+
89+
func getEnv(key string, fallback string) string {
90+
if value, ok := os.LookupEnv(key); ok {
91+
return value
92+
}
93+
return fallback
94+
}
95+
96+
func withTlsClientCheck(next http.HandlerFunc) http.HandlerFunc {
97+
return func(w http.ResponseWriter, r *http.Request) {
98+
for _, peer := range r.TLS.PeerCertificates {
99+
for _, revoked := range config.crl.TBSCertList.RevokedCertificates {
100+
if peer.SerialNumber.Cmp(revoked.SerialNumber) == 0 {
101+
log.Printf("Revoked certificate: ", peer.Subject)
102+
w.WriteHeader(403)
103+
return
104+
}
105+
}
106+
107+
}
108+
next.ServeHTTP(w, r)
109+
}
110+
}
111+
112+
func metrics(w http.ResponseWriter, r *http.Request) {
113+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
114+
defer cancel()
115+
116+
w.WriteHeader(200)
117+
118+
c := make(chan string)
119+
var wg sync.WaitGroup
120+
for _, url := range config.urls {
121+
wg.Add(1)
122+
go fetch(url, c, ctx, &wg)
123+
}
124+
go func() {
125+
wg.Wait()
126+
close(c)
127+
}()
128+
129+
for res := range c {
130+
fmt.Fprintf(w, "%s\n", res)
131+
}
132+
}
133+
134+
func extend(line string, source string) (string, error) {
135+
url, err := url.Parse(source)
136+
if err != nil {
137+
return line, err
138+
}
139+
label := fmt.Sprintf("%s%s", url.Host, url.Path)
140+
141+
match := re.FindStringSubmatch(line)
142+
if len(match) != 5 {
143+
return line, errors.New("Invalid Line.")
144+
}
145+
146+
lineName := match[1]
147+
lineLabels := match[3]
148+
lineValue := match[4]
149+
if lineLabels == "" {
150+
lineLabels = fmt.Sprintf("sub_instance=\"%s\"", label)
151+
} else {
152+
lineLabels = fmt.Sprintf("%s,sub_instance=\"%s\"", lineLabels, label)
153+
}
154+
line = fmt.Sprintf("%s{%s} %s", lineName, lineLabels, lineValue)
155+
return line, nil
156+
}
157+
158+
func fetch(url string, c chan string, ctx context.Context, wg *sync.WaitGroup) {
159+
defer wg.Done()
160+
req, err := http.NewRequest("GET", url, nil)
161+
if err != nil {
162+
log.Printf("Request Error %s.", err)
163+
return
164+
}
165+
req = req.WithContext(ctx)
166+
resp, err := http.DefaultClient.Do(req)
167+
if err != nil {
168+
log.Printf("Context Error %s.", err)
169+
return
170+
}
171+
scanner := bufio.NewScanner(resp.Body)
172+
for scanner.Scan() {
173+
line := scanner.Text()
174+
line, err = extend(line, url)
175+
c <- line
176+
}
177+
if err := scanner.Err(); err != nil {
178+
log.Printf("Scanner Error %s.", err)
179+
}
180+
}

0 commit comments

Comments
 (0)