Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Accept: application/json on common HTTP endpoints #2673

Merged
merged 9 commits into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,14 @@
* TSDB now does memory-mapping of Head chunks and reduces memory usage.
* [ENHANCEMENT] Experimental TSDB: when `-querier.query-store-after` is configured and running the experimental blocks storage, the time range of the query sent to the store is now manipulated to ensure the query end time is not more recent than 'now - query-store-after'. #2642
* [ENHANCEMENT] Experimental TSDB: small performance improvement in concurrent usage of RefCache, used during samples ingestion. #2651
* [ENHANCEMENT] The following endpoints now respond appropriately to an `Accepts` header with the value `application/json` #2673
* `/distributor/all_user_stats`
* `/distributor/ha_tracker`
* `/ingester/ring`
* `/store-gateway/ring`
* `/compactor/ring`
* `/ruler/ring`
* `/services`
* [ENHANCEMENT] Add `-cassandra.num-connections` to allow increasing the number of TCP connections to each Cassandra server. #2666
* [ENHANCEMENT] Use separate Cassandra clients and connections for reads and writes. #2666
* [BUGFIX] Ruler: Ensure temporary rule files with special characters are properly mapped and cleaned up. #2506
Expand Down
63 changes: 58 additions & 5 deletions pkg/cortex/status.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,71 @@
package cortex

import (
"fmt"
"html/template"
"net/http"
"time"

"github.com/cortexproject/cortex/pkg/util"
)

const tpl = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Cortex Services Status</title>
</head>
<body>
<h1>Cortex Services Status</h1>
<p>Current time: {{ .Now }}</p>
<table border="1">
<thead>
<tr>
<th>Service</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{{ range .Services }}
<tr>
<td>{{ .Name }}</td>
<td>{{ .Status }}</td>
</tr>
{{ end }}
</tbody>
</table>
</body>
</html>`

var tmpl *template.Template

type renderService struct {
Name string `json:"name"`
Status string `json:"status"`
}

func init() {
tmpl = template.Must(template.New("webpage").Parse(tpl))
}

func (t *Cortex) servicesHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
w.Header().Set("Content-Type", "text/plain")

// TODO: this could be extended to also print sub-services, if given service has any
svcs := make([]renderService, 0)
for mod, s := range t.ServiceMap {
if s != nil {
fmt.Fprintf(w, "%v => %v\n", mod, s.State())
}
svcs = append(svcs, renderService{
Name: mod,
Status: s.State().String(),
})
}

// TODO: this could be extended to also print sub-services, if given service has any
util.RenderHTTPResponse(w, struct {
Now time.Time `json:"now"`
Services []renderService `json:"services"`
}{
Now: time.Now(),
Services: svcs,
}, tmpl, r)
}
22 changes: 12 additions & 10 deletions pkg/distributor/ha_tracker_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"time"

"github.com/prometheus/prometheus/pkg/timestamp"

"github.com/cortexproject/cortex/pkg/util"
)

const trackerTpl = `
Expand Down Expand Up @@ -56,9 +58,12 @@ func init() {
func (h *haTracker) ServeHTTP(w http.ResponseWriter, req *http.Request) {
h.electedLock.RLock()
type replica struct {
UserID, Cluster, Replica string
ElectedAt time.Time
UpdateTime, FailoverTime time.Duration
UserID string `json:"userID"`
Cluster string `json:"cluster"`
Replica string `json:"replica"`
ElectedAt time.Time `json:"electedAt"`
UpdateTime time.Duration `json:"updateDuration"`
FailoverTime time.Duration `json:"failoverDuration"`
}

electedReplicas := []replica{}
Expand Down Expand Up @@ -86,14 +91,11 @@ func (h *haTracker) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return first.Cluster < second.Cluster
})

if err := trackerTmpl.Execute(w, struct {
Elected []replica
Now time.Time
util.RenderHTTPResponse(w, struct {
Elected []replica `json:"elected"`
Now time.Time `json:"now"`
}{
Elected: electedReplicas,
Now: time.Now(),
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}, trackerTmpl, req)
}
15 changes: 7 additions & 8 deletions pkg/distributor/http_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (
"sort"
"strings"
"time"

"github.com/cortexproject/cortex/pkg/util"
)

const tpl = `
Expand Down Expand Up @@ -83,16 +85,13 @@ func (d *Distributor) AllUserStatsHandler(w http.ResponseWriter, r *http.Request
return
}

if err := tmpl.Execute(w, struct {
Now time.Time
Stats []UserIDStats
ReplicationFactor int
util.RenderHTTPResponse(w, struct {
Now time.Time `json:"now"`
Stats []UserIDStats `json:"stats"`
ReplicationFactor int `json:"replicationFactor"`
}{
Now: time.Now(),
Stats: stats,
ReplicationFactor: d.ingestersRing.ReplicationFactor(),
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}, tmpl, r)
}
25 changes: 13 additions & 12 deletions pkg/ring/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,14 @@ func (r *Ring) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}

ingesters = append(ingesters, struct {
ID, State, Address, Timestamp, Zone string
Tokens []uint32
NumTokens int
Ownership float64
ID string `json:"id"`
State string `json:"state"`
Address string `json:"address"`
Timestamp string `json:"timestamp"`
Zone string `json:"zone"`
Tokens []uint32 `json:"tokens"`
NumTokens int `json:"-"`
Ownership float64 `json:"-"`
}{
ID: id,
State: state,
Expand All @@ -158,16 +162,13 @@ func (r *Ring) ServeHTTP(w http.ResponseWriter, req *http.Request) {

tokensParam := req.URL.Query().Get("tokens")

if err := pageTemplate.Execute(w, struct {
Ingesters []interface{}
Now time.Time
ShowTokens bool
util.RenderHTTPResponse(w, struct {
Ingesters []interface{} `json:"shards"`
Now time.Time `json:"now"`
ShowTokens bool `json:"-"`
}{
Ingesters: ingesters,
Now: time.Now(),
ShowTokens: tokensParam == "true",
}); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}, pageTemplate, req)
}
17 changes: 17 additions & 0 deletions pkg/util/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import (
"context"
"encoding/json"
"fmt"
"html/template"
"io"
"net/http"
"strings"

"github.com/blang/semver"
"github.com/gogo/protobuf/proto"
Expand All @@ -29,6 +31,21 @@ func WriteJSONResponse(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
}

// RenderHTTPResponse either responds with json or a rendered html page using the passed in template
// by checking the Accepts header
func RenderHTTPResponse(w http.ResponseWriter, v interface{}, t *template.Template, r *http.Request) {
accept := r.Header.Get("Accept")
if strings.Contains(accept, "application/json") {
WriteJSONResponse(w, v)
return
}

err := t.Execute(w, v)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}

// CompressionType for encoding and decoding requests and responses.
type CompressionType int

Expand Down
63 changes: 63 additions & 0 deletions pkg/util/http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package util

import (
"html/template"
"net/http/httptest"
"testing"

"github.com/stretchr/testify/assert"
)

func TestRenderHTTPResponse(t *testing.T) {
type testStruct struct {
Name string `json:"name"`
Value int `json:"value"`
}

tests := []struct {
name string
headers map[string]string
tmpl string
expected string
value testStruct
}{
{
name: "Test Renders json",
headers: map[string]string{
"Accept": "application/json",
},
tmpl: "<html></html>",
expected: `{"name":"testName","value":42}`,
value: testStruct{
Name: "testName",
Value: 42,
},
},
{
name: "Test Renders html",
headers: map[string]string{},
tmpl: "<html>{{ .Name }}</html>",
expected: "<html>testName</html>",
value: testStruct{
Name: "testName",
Value: 42,
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tmpl := template.Must(template.New("webpage").Parse(tt.tmpl))
writer := httptest.NewRecorder()
request := httptest.NewRequest("GET", "/", nil)

for k, v := range tt.headers {
request.Header.Add(k, v)
}

RenderHTTPResponse(writer, tt.value, tmpl, request)

assert.Equal(t, tt.expected, writer.Body.String())
})
}
}