Skip to content

Commit

Permalink
POC: statusz for apiserver
Browse files Browse the repository at this point in the history
  • Loading branch information
richabanker committed Jul 20, 2024
1 parent 14b34fc commit ca731e7
Show file tree
Hide file tree
Showing 4 changed files with 314 additions and 0 deletions.
10 changes: 10 additions & 0 deletions staging/src/k8s.io/apiserver/pkg/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import (
"k8s.io/component-base/logs"
"k8s.io/component-base/metrics/features"
"k8s.io/component-base/metrics/prometheus/slis"
"k8s.io/component-base/statusz"
"k8s.io/component-base/tracing"
"k8s.io/klog/v2"
openapicommon "k8s.io/kube-openapi/pkg/common"
Expand Down Expand Up @@ -1093,6 +1094,8 @@ func installAPI(s *GenericAPIServer, c *Config) {
}

if c.EnableMetrics {
statuzOpts := statuszOptions()
statusz.Statusz{}.Install(s.Handler.NonGoRestfulMux, statuzOpts)
if c.EnableProfiling {
routes.MetricsWithReset{}.Install(s.Handler.NonGoRestfulMux)
slis.SLIMetricsWithReset{}.Install(s.Handler.NonGoRestfulMux)
Expand Down Expand Up @@ -1185,3 +1188,10 @@ func SetHostnameFuncForTests(name string) {
return
}
}

func statuszOptions() statusz.Options {
return statusz.Options{
ComponentName: "apiserver",
StartTime: time.Now(),
}
}
125 changes: 125 additions & 0 deletions staging/src/k8s.io/component-base/statusz/registry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package statusz

import (
"context"
"fmt"
"html/template"
"io"
"sync"
"time"
)

type statuszRegistry struct {
lock sync.Mutex
options Options
sections []section
}

type section struct {
Title string
Func func(context.Context, io.Writer) error
}

func Register(opts Options) *statuszRegistry {
registry := &statuszRegistry{
options: opts,
}
registry.addSection("default", defaultSection)
return registry
}

func (reg *statuszRegistry) addSection(title string, f func(ctx context.Context, wr io.Writer, opts Options) error) error {
reg.lock.Lock()
defer reg.lock.Unlock()
reg.sections = append(reg.sections, section{
Title: title,
Func: func(ctx context.Context, wr io.Writer) error {
err := f(ctx, wr, reg.options)
if err != nil {
failErr := template.Must(template.New("").Parse("<code>invalid HTML: {{.}}</code>")).Execute(wr, err)
if failErr != nil {
return fmt.Errorf("go/server: couldn't execute the error template for %q: %v (couldn't get HTML fragment: %v)", title, failErr, err)
}
return err
}
return nil
},
})
return nil
}

func defaultSection(ctx context.Context, wr io.Writer, opts Options) error {
var data struct {
ServerName string
StartTime string
Uptime string
}

data.ServerName = opts.ComponentName
data.StartTime = opts.StartTime.Format(time.RFC1123)
uptime := int64(time.Since(opts.StartTime).Seconds())
data.Uptime = fmt.Sprintf("%d hr %02d min %02d sec",
uptime/3600, (uptime/60)%60, uptime%60)

if err := defaultTmp.Execute(wr, data); err != nil {
return fmt.Errorf("couldn't execute template: %v", err)
}

return nil
}

var defaultTmp = template.Must(template.New("").Parse(`
<!DOCTYPE html>
<html>
<head>
<title>Status for {{.ServerName}}</title>
<style>
body {
font-family: sans-serif;
}
h1 {
clear: both;
width: 100%;
text-align: center;
font-size: 120%;
background: #eef;
}
.lefthand {
float: left;
width: 80%;
}
.righthand {
text-align: right;
}
td {
background-color: rgba(0, 0, 0, 0.05);
}
</style>
</head>
<body>
<h1>Status for {{.ServerName}}</h1>
<div>
<div class=lefthand>
Started: {{.StartTime}}<br>
Up {{.Uptime}}<br>
</body>
</html>
`))
82 changes: 82 additions & 0 deletions staging/src/k8s.io/component-base/statusz/statusz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package statusz

import (
"bytes"
"context"
"fmt"
"net/http"
"time"

"k8s.io/apiserver/pkg/endpoints/metrics"
"k8s.io/klog/v2"
)

type Statusz struct {
registry *statuszRegistry
}

type Options struct {
ComponentName string
StartTime time.Time
}

type mux interface {
Handle(path string, handler http.Handler)
}

func (f Statusz) Install(m mux, opts Options) {
f.registry = Register(opts)
f.registry.installHandler(m)
}

func (reg *statuszRegistry) installHandler(m mux) {
reg.lock.Lock()
defer reg.lock.Unlock()
m.Handle("/statusz",
metrics.InstrumentHandlerFunc("GET",
/* group = */ "",
/* version = */ "",
/* resource = */ "",
/* subresource = */ "/statusz",
/* scope = */ "",
/* component = */ "",
/* deprecated */ false,
/* removedRelease */ "",
handleSections(reg.sections)))
}

// handleSections returns an http.HandlerFunc that serves the provided sections.
func handleSections(sections []section) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var individualCheckOutput bytes.Buffer
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("X-Content-Type-Options", "nosniff")
for _, section := range sections {
err := section.Func(context.Background(), w)
if err != nil {
fmt.Fprintf(&individualCheckOutput, "[-]%s failed: reason withheld\n", section.Title)
klog.V(2).Infof("%s section failed: %v", section.Title, err)
http.Error(w, fmt.Sprintf("%s%s section failed", individualCheckOutput.String(), section.Title), http.StatusInternalServerError)
return
}
fmt.Fprint(w)
}
individualCheckOutput.WriteTo(w)
}
}
97 changes: 97 additions & 0 deletions staging/src/k8s.io/component-base/statusz/statusz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package statusz

import (
"bytes"
"fmt"
"html/template"
"net/http"
"net/http/httptest"
"testing"
"time"
)

func TestStatusz(t *testing.T) {
tests := []struct {
name string
opts Options
expectedTemplate *template.Template
expectedStatus int
}{
{
name: "default",
opts: Options{
ComponentName: "test-component",
StartTime: time.Now(),
},
expectedTemplate: defaultTmp,
expectedStatus: http.StatusOK,
},
}

for i, test := range tests {
mux := http.NewServeMux()
Statusz{}.Install(mux, test.opts)

path := "/statusz"
req, err := http.NewRequest("GET", fmt.Sprintf("http://example.com%s", path), nil)
if err != nil {
t.Fatalf("case[%d] Unexpected error: %v", i, err)
}

w := httptest.NewRecorder()
mux.ServeHTTP(w, req)
if w.Code != test.expectedStatus {
t.Errorf("case[%d] Expected: %v, got: %v", i, test.expectedStatus, w.Code)
}

c := w.Header().Get("Content-Type")
if c != "text/plain; charset=utf-8" {
t.Errorf("case[%d] Expected: %v, got: %v", i, "text/plain", c)
}

data := prepareData(test.opts)
want := new(bytes.Buffer)
err = test.expectedTemplate.Execute(want, data)
if err != nil {
t.Fatalf("unexpected error while executing expected template: %v", err)
}

if w.Body.String() != want.String() {
t.Errorf("case[%d] Expected:\n%v\ngot:\n%v\n", i, test.expectedTemplate, w.Body.String())
}
}

}

func prepareData(opts Options) struct {
ServerName string
StartTime string
Uptime string
} {
var data struct {
ServerName string
StartTime string
Uptime string
}

data.ServerName = opts.ComponentName
data.StartTime = opts.StartTime.Format(time.RFC1123)
uptime := int64(time.Since(opts.StartTime).Seconds())
data.Uptime = fmt.Sprintf("%d hr %02d min %02d sec",
uptime/3600, (uptime/60)%60, uptime%60)

return data
}

0 comments on commit ca731e7

Please sign in to comment.