Skip to content

Commit d2fa7cc

Browse files
authored
Merge pull request #553 from Iceber/kube-state-metrics
kube-state-metrics: support for exposing single cluster metrics
2 parents 086063c + d086ad2 commit d2fa7cc

File tree

17 files changed

+3172
-63
lines changed

17 files changed

+3172
-63
lines changed

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ require (
66
github.com/DATA-DOG/go-sqlmock v1.5.0
77
github.com/clusterpedia-io/api v0.0.0
88
github.com/go-sql-driver/mysql v1.6.0
9+
github.com/gorilla/mux v1.8.0
910
github.com/jackc/pgconn v1.13.0
1011
github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa
1112
github.com/jackc/pgx/v4 v4.17.2

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
270270
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
271271
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
272272
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
273+
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
274+
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
273275
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
274276
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
275277
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
body {
2+
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,Liberation Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;
3+
margin: 0;
4+
}
5+
header {
6+
background-color: {{.HeaderColor}};
7+
color: #fff;
8+
font-size: 1rem;
9+
padding: 1rem;
10+
}
11+
main {
12+
padding: 1rem;
13+
}
14+
label {
15+
display: inline-block;
16+
width: {{.Form.Width}}em;
17+
}
18+
{{.ExtraCSS}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<html lang="en">
2+
<head>
3+
<meta charset="UTF-8">
4+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
5+
<title>{{.Name}}</title>
6+
<style>{{.CSS}}</style>
7+
</head>
8+
<body>
9+
<header>
10+
<h1>{{.Name}}</h1>
11+
</header>
12+
<main>
13+
{{if .Description}}<h2>{{.Description}}</h2>{{end}}
14+
{{if .Version}}<div>Version: {{.Version}}</div>{{end}}
15+
<div>
16+
<ul>
17+
{{ range .Links }}
18+
<li><a href="{{ .Address }}">{{.Text}}</a>{{if .Description}}: {{.Description}}{{end}}</li>
19+
{{ end }}
20+
</ul>
21+
</div>
22+
{{ if .Form.Action }}
23+
<div>
24+
<form action="{{ .Form.Action}}">
25+
{{ range .Form.Inputs }}
26+
<label>{{ .Label }}:</label>&nbsp;<input type="{{ .Type }}" name="{{ .Name }}" placeholder="{{ .Placeholder }}" value="{{ .Value }}"><br>
27+
{{ end }}
28+
<input type="submit" value="Submit">
29+
</form>
30+
</div>
31+
{{ end }}
32+
{{ .ExtraHTML }}
33+
</main>
34+
</body>
35+
</html>

pkg/kube_state_metrics/metrics_handler.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"net/http"
77
"strings"
88

9+
"github.com/gorilla/mux"
910
"github.com/prometheus/common/expfmt"
1011
"k8s.io/klog/v2"
1112
metricsstore "k8s.io/kube-state-metrics/v2/pkg/metrics_store"
@@ -34,8 +35,14 @@ func NewMetricsHandler(getter ClusterMetricsWriterListGetter, diableGZIPEncoding
3435
// metrics to the response body.
3536
func (m *MetricsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
3637
var writers metricsstore.MetricsWriterList
37-
for _, wl := range m.getter.GetMetricsWriterList() {
38-
writers = append(writers, wl...)
38+
39+
clusterWriters := m.getter.GetMetricsWriterList()
40+
if cluster := mux.Vars(r)["cluster"]; cluster != "" {
41+
writers = clusterWriters[cluster]
42+
} else {
43+
for _, wl := range clusterWriters {
44+
writers = append(writers, wl...)
45+
}
3946
}
4047

4148
resHeader := w.Header()

pkg/kube_state_metrics/server.go

+112-15
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
package kubestatemetrics
22

33
import (
4+
"bytes"
5+
_ "embed"
6+
"fmt"
7+
"net/http"
8+
"text/template"
9+
"time"
10+
11+
"github.com/gorilla/mux"
412
"github.com/prometheus/client_golang/prometheus"
513
"github.com/prometheus/client_golang/prometheus/promauto"
614
"github.com/prometheus/client_golang/prometheus/promhttp"
715
"github.com/prometheus/exporter-toolkit/web"
816
"k8s.io/klog/v2"
917

1018
"github.com/clusterpedia-io/clusterpedia/pkg/metrics"
19+
"github.com/clusterpedia-io/clusterpedia/pkg/version"
1120
)
1221

1322
type ServerConfig struct {
@@ -26,24 +35,112 @@ func RunServer(config ServerConfig, getter ClusterMetricsWriterListGetter) {
2635
}, []string{"method"},
2736
)
2837

29-
handlers := []metrics.Handler{
30-
{
31-
LandingName: "Metrics",
32-
Path: "/metrics",
33-
Handler: promhttp.InstrumentHandlerDuration(durationVec,
34-
NewMetricsHandler(getter, config.DisableGZIPEncoding),
35-
),
36-
},
38+
server := &http.Server{
39+
Handler: buildMetricsServer(config, getter, durationVec),
40+
ReadHeaderTimeout: 5 * time.Second,
41+
}
42+
43+
flags := &web.FlagConfig{
44+
WebListenAddresses: &[]string{config.Endpoint},
45+
WebSystemdSocket: new(bool),
46+
WebConfigFile: &config.TLSConfig,
3747
}
38-
server, flags := metrics.BuildMetricsServer(
39-
config.Endpoint,
40-
config.TLSConfig,
41-
"Clusterpedia kube-state-metrics",
42-
"Metrics for Kubernetes' state managed by Clusterpedia",
43-
handlers,
44-
)
4548

4649
klog.Info("Kube State Metrics Server is running...")
4750
// TODO(iceber): handle error
4851
_ = web.ListenAndServe(server, flags, metrics.Logger)
4952
}
53+
54+
func buildMetricsServer(config ServerConfig, getter ClusterMetricsWriterListGetter, durationObserver prometheus.ObserverVec) *mux.Router {
55+
handler := promhttp.InstrumentHandlerDuration(durationObserver,
56+
NewMetricsHandler(getter, config.DisableGZIPEncoding),
57+
)
58+
mux := mux.NewRouter()
59+
mux.Handle("/metrics", handler)
60+
mux.Handle("/clusters/{cluster}/metrics", handler)
61+
62+
// Add index
63+
landingConfig := web.LandingConfig{
64+
Name: "Clusterpedia kube-state-metrics",
65+
Description: "Metrics for Kubernetes' state managed by Clusterpedia",
66+
Version: version.Get().String(),
67+
Links: []web.LandingLinks{
68+
{
69+
Text: "Metrics",
70+
Address: "/metrics",
71+
},
72+
},
73+
}
74+
landingPage, err := NewLandingPage(landingConfig, getter)
75+
if err != nil {
76+
klog.ErrorS(err, "failed to create landing page")
77+
}
78+
mux.Handle("/", landingPage)
79+
return mux
80+
}
81+
82+
var (
83+
//go:embed landing_page/landing_page.html
84+
landingPagehtmlContent string
85+
//go:embed landing_page/landing_page.css
86+
landingPagecssContent string
87+
)
88+
89+
func NewLandingPage(c web.LandingConfig, getter ClusterMetricsWriterListGetter) (*LandingPageHandler, error) {
90+
length := 0
91+
for _, input := range c.Form.Inputs {
92+
inputLength := len(input.Label)
93+
if inputLength > length {
94+
length = inputLength
95+
}
96+
}
97+
c.Form.Width = (float64(length) + 1) / 2
98+
if c.CSS == "" {
99+
if c.HeaderColor == "" {
100+
// Default to Prometheus orange.
101+
c.HeaderColor = "#e6522c"
102+
}
103+
cssTemplate := template.Must(template.New("landing css").Parse(landingPagecssContent))
104+
var buf bytes.Buffer
105+
if err := cssTemplate.Execute(&buf, c); err != nil {
106+
return nil, err
107+
}
108+
c.CSS = buf.String()
109+
}
110+
return &LandingPageHandler{
111+
config: c,
112+
template: template.Must(template.New("landing page").Parse(landingPagehtmlContent)),
113+
getter: getter,
114+
}, nil
115+
}
116+
117+
type LandingPageHandler struct {
118+
config web.LandingConfig
119+
template *template.Template
120+
121+
getter ClusterMetricsWriterListGetter
122+
}
123+
124+
func (h *LandingPageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
125+
clusters := h.getter.GetMetricsWriterList()
126+
127+
config := h.config
128+
config.Links = append(make([]web.LandingLinks, 0, len(config.Links)+len(clusters)),
129+
h.config.Links...,
130+
)
131+
for cluster := range clusters {
132+
config.Links = append(config.Links, web.LandingLinks{
133+
Text: cluster + " Metrics",
134+
Address: fmt.Sprintf("/clusters/%s/metrics", cluster),
135+
})
136+
}
137+
138+
var buf bytes.Buffer
139+
if err := h.template.Execute(&buf, config); err != nil {
140+
_, _ = w.Write([]byte(err.Error()))
141+
return
142+
}
143+
144+
_, _ = w.Write(buf.Bytes())
145+
w.Header().Add("Content-Type", "text/html; charset=UTF-8")
146+
}

pkg/metrics/server.go

+23-46
Original file line numberDiff line numberDiff line change
@@ -19,67 +19,44 @@ type Config struct {
1919
}
2020

2121
func RunServer(config Config) {
22-
handlers := []Handler{
23-
{
24-
LandingName: "Metrics",
25-
Path: "metrics",
26-
Handler: promhttp.HandlerFor(registry, promhttp.HandlerOpts{
27-
ErrorLog: Logger,
28-
DisableCompression: config.DisableGZIPEncoding,
29-
}),
30-
},
22+
server := &http.Server{
23+
Handler: buildMetricsServer(config),
24+
ReadHeaderTimeout: 6 * time.Second,
25+
}
26+
27+
flags := &web.FlagConfig{
28+
WebListenAddresses: &[]string{config.Endpoint},
29+
WebSystemdSocket: new(bool),
30+
WebConfigFile: &config.TLSConfig,
3131
}
32-
server, flags := BuildMetricsServer(
33-
config.Endpoint,
34-
config.TLSConfig,
35-
"clusterpedia clustersynchro manager",
36-
"Self-metrics for clusterpedia clustersynchro manager",
37-
handlers,
38-
)
3932

4033
klog.Info("Metrics Server is running...")
4134
_ = web.ListenAndServe(server, flags, Logger)
4235
}
4336

44-
type Handler struct {
45-
LandingName string
46-
Path string
47-
Handler http.Handler
48-
}
49-
50-
func BuildMetricsServer(endpoint, tlsConfigFile, name, desc string, handlers []Handler) (*http.Server, *web.FlagConfig) {
51-
var links []web.LandingLinks
37+
func buildMetricsServer(config Config) *http.ServeMux {
5238
mux := http.NewServeMux()
53-
for _, handler := range handlers {
54-
mux.Handle(handler.Path, handler.Handler)
55-
if handler.LandingName != "" {
56-
links = append(links, web.LandingLinks{
57-
Address: handler.Path,
58-
Text: handler.LandingName,
59-
})
60-
}
61-
}
39+
mux.Handle("/metrics", promhttp.HandlerFor(registry, promhttp.HandlerOpts{
40+
ErrorLog: Logger,
41+
DisableCompression: config.DisableGZIPEncoding,
42+
}))
6243

6344
// Add index
6445
landingConfig := web.LandingConfig{
65-
Name: name,
66-
Description: desc,
46+
Name: "clusterpedia clustersynchro manager",
47+
Description: "Self-metrics for clusterpedia clustersynchro manager",
6748
Version: version.Get().String(),
68-
Links: links,
49+
Links: []web.LandingLinks{
50+
{
51+
Text: "Metrics",
52+
Address: "/metrics",
53+
},
54+
},
6955
}
7056
landingPage, err := web.NewLandingPage(landingConfig)
7157
if err != nil {
7258
klog.ErrorS(err, "failed to create landing page")
7359
}
7460
mux.Handle("/", landingPage)
75-
76-
return &http.Server{
77-
Handler: mux,
78-
ReadHeaderTimeout: 5 * time.Second,
79-
},
80-
&web.FlagConfig{
81-
WebListenAddresses: &[]string{endpoint},
82-
WebSystemdSocket: new(bool),
83-
WebConfigFile: &tlsConfigFile,
84-
}
61+
return mux
8562
}

vendor/github.com/gorilla/mux/AUTHORS

+8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/gorilla/mux/LICENSE

+27
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)