Add status page
This commit is contained in:
parent
1b33388e2a
commit
3587b362db
4 changed files with 486 additions and 6 deletions
20
main.go
20
main.go
|
@ -36,6 +36,7 @@ import (
|
||||||
|
|
||||||
"gitea.drjosh.dev/josh/jrouter/aurp"
|
"gitea.drjosh.dev/josh/jrouter/aurp"
|
||||||
"gitea.drjosh.dev/josh/jrouter/router"
|
"gitea.drjosh.dev/josh/jrouter/router"
|
||||||
|
"gitea.drjosh.dev/josh/jrouter/status"
|
||||||
|
|
||||||
"github.com/google/gopacket/pcap"
|
"github.com/google/gopacket/pcap"
|
||||||
"github.com/sfiera/multitalk/pkg/ddp"
|
"github.com/sfiera/multitalk/pkg/ddp"
|
||||||
|
@ -102,7 +103,13 @@ func main() {
|
||||||
defer cancel()
|
defer cancel()
|
||||||
ctx, _ := signal.NotifyContext(cctx, os.Interrupt)
|
ctx, _ := signal.NotifyContext(cctx, os.Interrupt)
|
||||||
|
|
||||||
// Open PCAP session
|
// --------------------------------- HTTP ---------------------------------
|
||||||
|
http.HandleFunc("/status", status.Handle)
|
||||||
|
go func() {
|
||||||
|
log.Print(http.ListenAndServe(":9459", nil))
|
||||||
|
}()
|
||||||
|
|
||||||
|
// --------------------------------- Pcap ---------------------------------
|
||||||
// First check the interface
|
// First check the interface
|
||||||
iface, err := net.InterfaceByName(cfg.EtherTalk.Device)
|
iface, err := net.InterfaceByName(cfg.EtherTalk.Device)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -129,6 +136,12 @@ func main() {
|
||||||
}
|
}
|
||||||
defer pcapHandle.Close()
|
defer pcapHandle.Close()
|
||||||
|
|
||||||
|
// -------------------------------- Tables --------------------------------
|
||||||
|
routes := router.NewRoutingTable()
|
||||||
|
zones := router.NewZoneTable()
|
||||||
|
zones.Upsert(cfg.EtherTalk.NetStart, cfg.EtherTalk.ZoneName, true)
|
||||||
|
|
||||||
|
// -------------------------------- Peers ---------------------------------
|
||||||
// Wait until all peer handlers have finished before closing the port
|
// Wait until all peer handlers have finished before closing the port
|
||||||
var handlersWG sync.WaitGroup
|
var handlersWG sync.WaitGroup
|
||||||
defer func() {
|
defer func() {
|
||||||
|
@ -144,11 +157,6 @@ func main() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------- Tables --------------------------------
|
|
||||||
routes := router.NewRoutingTable()
|
|
||||||
zones := router.NewZoneTable()
|
|
||||||
zones.Upsert(cfg.EtherTalk.NetStart, cfg.EtherTalk.ZoneName, true)
|
|
||||||
|
|
||||||
// ------------------------- Configured peer setup ------------------------
|
// ------------------------- Configured peer setup ------------------------
|
||||||
if cfg.PeerListURL != "" {
|
if cfg.PeerListURL != "" {
|
||||||
log.Printf("Fetching peer list from %s...", cfg.PeerListURL)
|
log.Printf("Fetching peer list from %s...", cfg.PeerListURL)
|
||||||
|
|
303
status/status.go
Normal file
303
status/status.go
Normal file
|
@ -0,0 +1,303 @@
|
||||||
|
// Package status provides a status page handler, for exposing a summary of what
|
||||||
|
// the various pieces of the agent are doing.
|
||||||
|
//
|
||||||
|
// Inspired heavily by Google "/statsuz" - one public example is at:
|
||||||
|
// https://github.com/youtube/doorman/blob/master/go/status/status.go
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
_ "embed"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const errorTmplSrc = `<div class="error">❌ {{.Operation}}: <code>{{.Error}}</code><br>
|
||||||
|
Raw item data:<br>
|
||||||
|
<pre>{{.Item | printJSON}}</pre>
|
||||||
|
</div>`
|
||||||
|
|
||||||
|
var (
|
||||||
|
//go:embed status.html.tmpl
|
||||||
|
statusTmplSrc string
|
||||||
|
|
||||||
|
// Errors ignored below, as the status page is "best effort".
|
||||||
|
hostname, _ = os.Hostname()
|
||||||
|
username = func() string {
|
||||||
|
user, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("unknown (uid=unknown; error=%v)", err)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s (uid=%s)", user.Username, user.Uid)
|
||||||
|
}()
|
||||||
|
exepath, _ = os.Executable()
|
||||||
|
startTime = time.Now()
|
||||||
|
|
||||||
|
rootItem = &simpleItem{
|
||||||
|
baseItem: baseItem{
|
||||||
|
items: make(map[string]item),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
funcMap = template.FuncMap{
|
||||||
|
"printJSON": printJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The inbuilt templates should always parse. Rather than use template.Must,
|
||||||
|
// successful parsing is enforced by the smoke tests.
|
||||||
|
statusTmpl, _ = template.New("status").Funcs(funcMap).Parse(statusTmplSrc)
|
||||||
|
errorTmpl, _ = template.New("item-error").Funcs(funcMap).Parse(errorTmplSrc)
|
||||||
|
)
|
||||||
|
|
||||||
|
type statusData struct {
|
||||||
|
Items map[string]item
|
||||||
|
Version string
|
||||||
|
Build string
|
||||||
|
Hostname string
|
||||||
|
Username string
|
||||||
|
ExePath string
|
||||||
|
PID int
|
||||||
|
Compiler string
|
||||||
|
RuntimeVer string
|
||||||
|
GOOS string
|
||||||
|
GOARCH string
|
||||||
|
NumCPU int
|
||||||
|
NumGoroutine int
|
||||||
|
StartTime string
|
||||||
|
StartTimeAgo time.Duration
|
||||||
|
CurrentTime string
|
||||||
|
Ctx context.Context // request context for Eval calls inside the template execution only
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorData struct {
|
||||||
|
Operation string
|
||||||
|
Error error
|
||||||
|
Item any
|
||||||
|
}
|
||||||
|
|
||||||
|
type item interface {
|
||||||
|
addSubItem(string, item)
|
||||||
|
delSubItem(string)
|
||||||
|
|
||||||
|
Eval(context.Context) template.HTML
|
||||||
|
Items() map[string]item
|
||||||
|
}
|
||||||
|
|
||||||
|
type itemCtxKey struct{}
|
||||||
|
|
||||||
|
func parentItem(ctx context.Context) item {
|
||||||
|
v := ctx.Value(itemCtxKey{})
|
||||||
|
if v == nil {
|
||||||
|
return rootItem
|
||||||
|
}
|
||||||
|
return v.(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
type baseItem struct {
|
||||||
|
mu sync.RWMutex
|
||||||
|
items map[string]item
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *baseItem) addSubItem(title string, sub item) {
|
||||||
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
i.items[title] = sub
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *baseItem) delSubItem(title string) {
|
||||||
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
delete(i.items, title)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *baseItem) Items() map[string]item {
|
||||||
|
i.mu.RLock()
|
||||||
|
defer i.mu.RUnlock()
|
||||||
|
icopy := make(map[string]item, len(i.items))
|
||||||
|
for k, v := range i.items {
|
||||||
|
icopy[k] = v
|
||||||
|
}
|
||||||
|
return icopy
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimpleItem is for untemplated status items that only report a simple non-HTML string.
|
||||||
|
type simpleItem struct {
|
||||||
|
baseItem
|
||||||
|
stat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStatus sets the status of the item.
|
||||||
|
func (i *simpleItem) setStatus(s string) {
|
||||||
|
i.mu.Lock()
|
||||||
|
defer i.mu.Unlock()
|
||||||
|
i.stat = s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eval escapes the status string, and returns the current item value.
|
||||||
|
func (i *simpleItem) Eval(ctx context.Context) template.HTML {
|
||||||
|
i.mu.RLock()
|
||||||
|
defer i.mu.RUnlock()
|
||||||
|
return template.HTML(template.HTMLEscapeString(i.stat))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ItemCallback funcs are used by templated status items to provide the value to
|
||||||
|
// hydrate the item template.
|
||||||
|
type ItemCallback = func(context.Context) (any, error)
|
||||||
|
|
||||||
|
// templatedItem uses a template to format the item.
|
||||||
|
type templatedItem struct {
|
||||||
|
baseItem
|
||||||
|
tmpl *template.Template
|
||||||
|
cb ItemCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eval calls the item callback, and feeds the result through the item's
|
||||||
|
// own template.
|
||||||
|
func (i *templatedItem) Eval(ctx context.Context) template.HTML {
|
||||||
|
var sb strings.Builder
|
||||||
|
|
||||||
|
data, err := i.cb(ctx)
|
||||||
|
if err != nil {
|
||||||
|
errorTmpl.Execute(&sb, errorData{
|
||||||
|
Operation: "Error from item callback",
|
||||||
|
Error: err,
|
||||||
|
Item: data,
|
||||||
|
})
|
||||||
|
return template.HTML(sb.String())
|
||||||
|
}
|
||||||
|
if err := i.tmpl.Execute(&sb, data); err != nil {
|
||||||
|
errorTmpl.Execute(&sb, errorData{
|
||||||
|
Operation: "Error while executing item template",
|
||||||
|
Error: err,
|
||||||
|
Item: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return template.HTML(sb.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle handles status page requests.
|
||||||
|
func Handle(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := &statusData{
|
||||||
|
Items: rootItem.items,
|
||||||
|
Version: "TODO",
|
||||||
|
Build: "TODO",
|
||||||
|
Hostname: hostname,
|
||||||
|
Username: username,
|
||||||
|
ExePath: exepath,
|
||||||
|
PID: os.Getpid(),
|
||||||
|
Compiler: runtime.Compiler,
|
||||||
|
RuntimeVer: runtime.Version(),
|
||||||
|
GOOS: runtime.GOOS,
|
||||||
|
GOARCH: runtime.GOARCH,
|
||||||
|
NumCPU: runtime.NumCPU(),
|
||||||
|
NumGoroutine: runtime.NumGoroutine(),
|
||||||
|
StartTime: startTime.Format(time.RFC1123),
|
||||||
|
StartTimeAgo: time.Since(startTime),
|
||||||
|
CurrentTime: time.Now().Format(time.RFC1123),
|
||||||
|
Ctx: r.Context(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// The status template ranges over the items.
|
||||||
|
rootItem.mu.RLock()
|
||||||
|
defer rootItem.mu.RUnlock()
|
||||||
|
if err := statusTmpl.Execute(w, data); err != nil {
|
||||||
|
errorTmpl.Execute(w, errorData{
|
||||||
|
Operation: "Error while executing main template",
|
||||||
|
Error: err,
|
||||||
|
Item: data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddItem adds an item to be displayed on the status page. On each page load,
|
||||||
|
// the item's callback is called, and the data returned used to fill the
|
||||||
|
// HTML template in tmpl. The title should be unique (among items under this
|
||||||
|
// parent.
|
||||||
|
func AddItem(parent context.Context, title, tmpl string, cb func(context.Context) (any, error)) (context.Context, func()) {
|
||||||
|
if cb == nil {
|
||||||
|
cb = func(context.Context) (any, error) { return nil, nil }
|
||||||
|
}
|
||||||
|
|
||||||
|
item := &templatedItem{
|
||||||
|
baseItem: baseItem{
|
||||||
|
items: make(map[string]item),
|
||||||
|
},
|
||||||
|
tmpl: template.New(title).Funcs(funcMap),
|
||||||
|
cb: cb,
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := item.tmpl.Parse(tmpl); err != nil {
|
||||||
|
// Insert an item, but swap the template for the error template, and
|
||||||
|
// wrap the callback's return in an errorData.
|
||||||
|
item.tmpl = errorTmpl
|
||||||
|
item.cb = wrapInItemError("Could not parse item template", err, cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
pitem := parentItem(parent)
|
||||||
|
pitem.addSubItem(title, item)
|
||||||
|
|
||||||
|
return context.WithValue(parent, itemCtxKey{}, item), func() { pitem.delSubItem(title) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddSimpleItem adds a simple status item. Set the value shown by the
|
||||||
|
// item by calling setStatus.
|
||||||
|
func AddSimpleItem(parent context.Context, title string) (ctx context.Context, setStatus func(string), done func()) {
|
||||||
|
item := &simpleItem{
|
||||||
|
baseItem: baseItem{
|
||||||
|
items: make(map[string]item),
|
||||||
|
},
|
||||||
|
stat: "Unknown status",
|
||||||
|
}
|
||||||
|
pitem := parentItem(parent)
|
||||||
|
pitem.addSubItem(title, item)
|
||||||
|
|
||||||
|
return context.WithValue(parent, itemCtxKey{}, item), item.setStatus, func() { pitem.delSubItem(title) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelItem removes a status item, specified by the title, from a parent context.
|
||||||
|
func DelItem(parent context.Context, title string) {
|
||||||
|
pitem := item(rootItem)
|
||||||
|
if v := parent.Value(itemCtxKey{}); v != nil {
|
||||||
|
pitem = v.(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
pitem.delSubItem(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapInItemError takes a callback and returns a new callback wrapping the
|
||||||
|
// result in an errorData.
|
||||||
|
func wrapInItemError(op string, err error, cb ItemCallback) ItemCallback {
|
||||||
|
return func(ctx context.Context) (any, error) {
|
||||||
|
if cb == nil {
|
||||||
|
return &errorData{
|
||||||
|
Operation: op,
|
||||||
|
Error: err,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
// Surface item callback errors first.
|
||||||
|
data, err2 := cb(ctx)
|
||||||
|
if err2 != nil {
|
||||||
|
return data, err2
|
||||||
|
}
|
||||||
|
// Surface the item template parse error.
|
||||||
|
return &errorData{
|
||||||
|
Operation: op,
|
||||||
|
Error: err,
|
||||||
|
Item: data,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// printJSON is used as a fallback renderer for item status values.
|
||||||
|
func printJSON(v any) (string, error) {
|
||||||
|
b, err := json.MarshalIndent(v, "", " ")
|
||||||
|
return string(b), err
|
||||||
|
}
|
81
status/status.html.tmpl
Normal file
81
status/status.html.tmpl
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
{{- define "render_items" -}}
|
||||||
|
{{- range $title, $item := . -}}
|
||||||
|
<details open>
|
||||||
|
<summary>{{$title}}</summary>
|
||||||
|
<div class="itemdata">{{$item.Eval $.Ctx}}</div>
|
||||||
|
<div class="subitems">{{template "render_items" $item.Items}}</div>
|
||||||
|
</details>
|
||||||
|
{{- end -}}
|
||||||
|
{{- end -}}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Status for buildkite-agent</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: sans-serif;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
clear: both;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 120%;
|
||||||
|
background: #eef;
|
||||||
|
padding: 4px;
|
||||||
|
}
|
||||||
|
summary {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 120%;
|
||||||
|
background: #eef;
|
||||||
|
padding: 0.2em;
|
||||||
|
border: 1px #eef solid;
|
||||||
|
}
|
||||||
|
details {
|
||||||
|
display: block;
|
||||||
|
padding: 1em 0;
|
||||||
|
}
|
||||||
|
details[open] > summary {
|
||||||
|
background: #fff;
|
||||||
|
border: 1px #000 solid;
|
||||||
|
}
|
||||||
|
.error {
|
||||||
|
background: #fee;
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
padding: 0.2em;
|
||||||
|
}
|
||||||
|
div.warning {
|
||||||
|
background: #ffd;
|
||||||
|
}
|
||||||
|
.itemdata {
|
||||||
|
margin: 0 0 0.5em 0.86em;
|
||||||
|
padding: 0.5em 0em 0.5em 1.5em;
|
||||||
|
border-left: 0.1em #ccc dotted;
|
||||||
|
}
|
||||||
|
.subitems {
|
||||||
|
margin: 0 0 0.5em 0.86em;
|
||||||
|
padding: 0.5em 0em 0.5em 1.5em;
|
||||||
|
border-left: 0.1em #ccc dotted;
|
||||||
|
border-bottom: 0.1em #ccc dotted;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Status for buildkite-agent</h1>
|
||||||
|
<div class="warning">
|
||||||
|
⚠️ This page is intended only for debugging purposes!<br>
|
||||||
|
Do not expect the structure and formatting of this page to be stable across versions.
|
||||||
|
</div>
|
||||||
|
<div class="summary">
|
||||||
|
{{.ExePath}}<br>
|
||||||
|
Started at {{.StartTime}} ({{.StartTimeAgo}} ago)<br>
|
||||||
|
Current time {{.CurrentTime}}<br>
|
||||||
|
Version {{.Version}}, build {{.Build}}<br>
|
||||||
|
Running in PID {{.PID}} as {{.Username}} on {{.Hostname}}<br>
|
||||||
|
{{.RuntimeVer}} on {{.GOOS}}/{{.GOARCH}} compiled by {{.Compiler}}<br>
|
||||||
|
{{.NumGoroutine}} goroutines running/{{.NumCPU}} logical CPUs usable<br>
|
||||||
|
</div>
|
||||||
|
{{template "render_items" .Items}}
|
||||||
|
</body>
|
||||||
|
</html>
|
88
status/status_test.go
Normal file
88
status/status_test.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSmokeErrorTemplate(t *testing.T) {
|
||||||
|
errData := &errorData{
|
||||||
|
Operation: "Couldn't fluff the llamas",
|
||||||
|
Error: errors.New("llama comb unavailable"),
|
||||||
|
Item: map[string]any{
|
||||||
|
"llamas": "yes",
|
||||||
|
"alpacas": 42,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := errorTmpl.Execute(io.Discard, errData); err != nil {
|
||||||
|
t.Errorf("errorTmpl.Execute(io.Discard, errData) = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSmokeStatusTemplate(t *testing.T) {
|
||||||
|
data := &statusData{
|
||||||
|
Items: map[string]item{
|
||||||
|
"Llamas": &simpleItem{
|
||||||
|
stat: "✅ Llamas enabled",
|
||||||
|
},
|
||||||
|
"Alpacas": &templatedItem{
|
||||||
|
tmpl: template.Must(template.New("alpacas").Parse("Alpacas enabled at: {{.AlpacasEnabled}}")),
|
||||||
|
cb: func(context.Context) (any, error) {
|
||||||
|
return struct {
|
||||||
|
AlpacasEnabled time.Time
|
||||||
|
}{time.Now()}, nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Version: "0.0.0",
|
||||||
|
Build: "1234",
|
||||||
|
Hostname: hostname,
|
||||||
|
Username: username,
|
||||||
|
ExePath: exepath,
|
||||||
|
PID: os.Getpid(),
|
||||||
|
Compiler: runtime.Compiler,
|
||||||
|
RuntimeVer: runtime.Version(),
|
||||||
|
GOOS: runtime.GOOS,
|
||||||
|
GOARCH: runtime.GOARCH,
|
||||||
|
NumCPU: runtime.NumCPU(),
|
||||||
|
NumGoroutine: runtime.NumGoroutine(),
|
||||||
|
StartTime: startTime.Format(time.RFC1123),
|
||||||
|
StartTimeAgo: time.Since(startTime),
|
||||||
|
CurrentTime: time.Now().Format(time.RFC1123),
|
||||||
|
Ctx: context.Background(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := statusTmpl.Execute(io.Discard, data); err != nil {
|
||||||
|
t.Errorf("statusData.Execute(io.Discard, data) = %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSmokeHandle(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
cctx, setStat, done := AddSimpleItem(ctx, "Llamas")
|
||||||
|
defer done()
|
||||||
|
setStat("Essence of Llama")
|
||||||
|
|
||||||
|
_, setStat2, done2 := AddSimpleItem(cctx, "Kuzco")
|
||||||
|
defer done2()
|
||||||
|
setStat2("Oh, right. The poison. The poison for Kuzco, the poison chosen especially to kill Kuzco, Kuzco's poison.")
|
||||||
|
|
||||||
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "/status", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("http.NewReqeustWithContext(GET /status) error = %v", err)
|
||||||
|
}
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
Handle(rec, req)
|
||||||
|
if got, want := rec.Result().StatusCode, http.StatusOK; got != want {
|
||||||
|
t.Errorf("Handle(rec, req): rec.Result().StatusCode = %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue