Skip to content

Commit 80b1261

Browse files
authored
Support Accept: application/json on common HTTP endpoints (#2673)
* Added json support to rings, user stats, ha tracker Signed-off-by: Joe Elliott <number101010@gmail.com> * Added services template and accepts json support Signed-off-by: Joe Elliott <number101010@gmail.com> * lint + changelog Signed-off-by: Joe Elliott <number101010@gmail.com> * Improved json field names Signed-off-by: Joe Elliott <number101010@gmail.com> * Fixed accept header name and removed jpe notes Signed-off-by: Joe Elliott <number101010@gmail.com> * Added test Signed-off-by: Joe Elliott <number101010@gmail.com> * lint Signed-off-by: Joe Elliott <number101010@gmail.com> * Removed unused CSRF Token Placeholder Signed-off-by: Joe Elliott <number101010@gmail.com>
1 parent 58790db commit 80b1261

File tree

7 files changed

+178
-35
lines changed

7 files changed

+178
-35
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,14 @@
9494
* TSDB now does memory-mapping of Head chunks and reduces memory usage.
9595
* [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
9696
* [ENHANCEMENT] Experimental TSDB: small performance improvement in concurrent usage of RefCache, used during samples ingestion. #2651
97+
* [ENHANCEMENT] The following endpoints now respond appropriately to an `Accepts` header with the value `application/json` #2673
98+
* `/distributor/all_user_stats`
99+
* `/distributor/ha_tracker`
100+
* `/ingester/ring`
101+
* `/store-gateway/ring`
102+
* `/compactor/ring`
103+
* `/ruler/ring`
104+
* `/services`
97105
* [ENHANCEMENT] Add `-cassandra.num-connections` to allow increasing the number of TCP connections to each Cassandra server. #2666
98106
* [ENHANCEMENT] Use separate Cassandra clients and connections for reads and writes. #2666
99107
* [BUGFIX] Ruler: Ensure temporary rule files with special characters are properly mapped and cleaned up. #2506

pkg/cortex/status.go

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,71 @@
11
package cortex
22

33
import (
4-
"fmt"
4+
"html/template"
55
"net/http"
6+
"time"
7+
8+
"github.com/cortexproject/cortex/pkg/util"
69
)
710

11+
const tpl = `
12+
<!DOCTYPE html>
13+
<html>
14+
<head>
15+
<meta charset="UTF-8">
16+
<title>Cortex Services Status</title>
17+
</head>
18+
<body>
19+
<h1>Cortex Services Status</h1>
20+
<p>Current time: {{ .Now }}</p>
21+
<table border="1">
22+
<thead>
23+
<tr>
24+
<th>Service</th>
25+
<th>Status</th>
26+
</tr>
27+
</thead>
28+
<tbody>
29+
{{ range .Services }}
30+
<tr>
31+
<td>{{ .Name }}</td>
32+
<td>{{ .Status }}</td>
33+
</tr>
34+
{{ end }}
35+
</tbody>
36+
</table>
37+
</body>
38+
</html>`
39+
40+
var tmpl *template.Template
41+
42+
type renderService struct {
43+
Name string `json:"name"`
44+
Status string `json:"status"`
45+
}
46+
47+
func init() {
48+
tmpl = template.Must(template.New("webpage").Parse(tpl))
49+
}
50+
851
func (t *Cortex) servicesHandler(w http.ResponseWriter, r *http.Request) {
952
w.WriteHeader(200)
1053
w.Header().Set("Content-Type", "text/plain")
1154

12-
// TODO: this could be extended to also print sub-services, if given service has any
55+
svcs := make([]renderService, 0)
1356
for mod, s := range t.ServiceMap {
14-
if s != nil {
15-
fmt.Fprintf(w, "%v => %v\n", mod, s.State())
16-
}
57+
svcs = append(svcs, renderService{
58+
Name: mod,
59+
Status: s.State().String(),
60+
})
1761
}
62+
63+
// TODO: this could be extended to also print sub-services, if given service has any
64+
util.RenderHTTPResponse(w, struct {
65+
Now time.Time `json:"now"`
66+
Services []renderService `json:"services"`
67+
}{
68+
Now: time.Now(),
69+
Services: svcs,
70+
}, tmpl, r)
1871
}

pkg/distributor/ha_tracker_http.go

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"time"
99

1010
"github.com/prometheus/prometheus/pkg/timestamp"
11+
12+
"github.com/cortexproject/cortex/pkg/util"
1113
)
1214

1315
const trackerTpl = `
@@ -56,9 +58,12 @@ func init() {
5658
func (h *haTracker) ServeHTTP(w http.ResponseWriter, req *http.Request) {
5759
h.electedLock.RLock()
5860
type replica struct {
59-
UserID, Cluster, Replica string
60-
ElectedAt time.Time
61-
UpdateTime, FailoverTime time.Duration
61+
UserID string `json:"userID"`
62+
Cluster string `json:"cluster"`
63+
Replica string `json:"replica"`
64+
ElectedAt time.Time `json:"electedAt"`
65+
UpdateTime time.Duration `json:"updateDuration"`
66+
FailoverTime time.Duration `json:"failoverDuration"`
6267
}
6368

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

89-
if err := trackerTmpl.Execute(w, struct {
90-
Elected []replica
91-
Now time.Time
94+
util.RenderHTTPResponse(w, struct {
95+
Elected []replica `json:"elected"`
96+
Now time.Time `json:"now"`
9297
}{
9398
Elected: electedReplicas,
9499
Now: time.Now(),
95-
}); err != nil {
96-
http.Error(w, err.Error(), http.StatusInternalServerError)
97-
return
98-
}
100+
}, trackerTmpl, req)
99101
}

pkg/distributor/http_admin.go

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"sort"
99
"strings"
1010
"time"
11+
12+
"github.com/cortexproject/cortex/pkg/util"
1113
)
1214

1315
const tpl = `
@@ -83,16 +85,13 @@ func (d *Distributor) AllUserStatsHandler(w http.ResponseWriter, r *http.Request
8385
return
8486
}
8587

86-
if err := tmpl.Execute(w, struct {
87-
Now time.Time
88-
Stats []UserIDStats
89-
ReplicationFactor int
88+
util.RenderHTTPResponse(w, struct {
89+
Now time.Time `json:"now"`
90+
Stats []UserIDStats `json:"stats"`
91+
ReplicationFactor int `json:"replicationFactor"`
9092
}{
9193
Now: time.Now(),
9294
Stats: stats,
9395
ReplicationFactor: d.ingestersRing.ReplicationFactor(),
94-
}); err != nil {
95-
http.Error(w, err.Error(), http.StatusInternalServerError)
96-
return
97-
}
96+
}, tmpl, r)
9897
}

pkg/ring/http.go

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,14 @@ func (r *Ring) ServeHTTP(w http.ResponseWriter, req *http.Request) {
140140
}
141141

142142
ingesters = append(ingesters, struct {
143-
ID, State, Address, Timestamp, Zone string
144-
Tokens []uint32
145-
NumTokens int
146-
Ownership float64
143+
ID string `json:"id"`
144+
State string `json:"state"`
145+
Address string `json:"address"`
146+
Timestamp string `json:"timestamp"`
147+
Zone string `json:"zone"`
148+
Tokens []uint32 `json:"tokens"`
149+
NumTokens int `json:"-"`
150+
Ownership float64 `json:"-"`
147151
}{
148152
ID: id,
149153
State: state,
@@ -158,16 +162,13 @@ func (r *Ring) ServeHTTP(w http.ResponseWriter, req *http.Request) {
158162

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

161-
if err := pageTemplate.Execute(w, struct {
162-
Ingesters []interface{}
163-
Now time.Time
164-
ShowTokens bool
165+
util.RenderHTTPResponse(w, struct {
166+
Ingesters []interface{} `json:"shards"`
167+
Now time.Time `json:"now"`
168+
ShowTokens bool `json:"-"`
165169
}{
166170
Ingesters: ingesters,
167171
Now: time.Now(),
168172
ShowTokens: tokensParam == "true",
169-
}); err != nil {
170-
http.Error(w, err.Error(), http.StatusInternalServerError)
171-
return
172-
}
173+
}, pageTemplate, req)
173174
}

pkg/util/http.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import (
55
"context"
66
"encoding/json"
77
"fmt"
8+
"html/template"
89
"io"
910
"net/http"
11+
"strings"
1012

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

34+
// RenderHTTPResponse either responds with json or a rendered html page using the passed in template
35+
// by checking the Accepts header
36+
func RenderHTTPResponse(w http.ResponseWriter, v interface{}, t *template.Template, r *http.Request) {
37+
accept := r.Header.Get("Accept")
38+
if strings.Contains(accept, "application/json") {
39+
WriteJSONResponse(w, v)
40+
return
41+
}
42+
43+
err := t.Execute(w, v)
44+
if err != nil {
45+
http.Error(w, err.Error(), http.StatusInternalServerError)
46+
}
47+
}
48+
3249
// CompressionType for encoding and decoding requests and responses.
3350
type CompressionType int
3451

pkg/util/http_test.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package util
2+
3+
import (
4+
"html/template"
5+
"net/http/httptest"
6+
"testing"
7+
8+
"github.com/stretchr/testify/assert"
9+
)
10+
11+
func TestRenderHTTPResponse(t *testing.T) {
12+
type testStruct struct {
13+
Name string `json:"name"`
14+
Value int `json:"value"`
15+
}
16+
17+
tests := []struct {
18+
name string
19+
headers map[string]string
20+
tmpl string
21+
expected string
22+
value testStruct
23+
}{
24+
{
25+
name: "Test Renders json",
26+
headers: map[string]string{
27+
"Accept": "application/json",
28+
},
29+
tmpl: "<html></html>",
30+
expected: `{"name":"testName","value":42}`,
31+
value: testStruct{
32+
Name: "testName",
33+
Value: 42,
34+
},
35+
},
36+
{
37+
name: "Test Renders html",
38+
headers: map[string]string{},
39+
tmpl: "<html>{{ .Name }}</html>",
40+
expected: "<html>testName</html>",
41+
value: testStruct{
42+
Name: "testName",
43+
Value: 42,
44+
},
45+
},
46+
}
47+
48+
for _, tt := range tests {
49+
t.Run(tt.name, func(t *testing.T) {
50+
tmpl := template.Must(template.New("webpage").Parse(tt.tmpl))
51+
writer := httptest.NewRecorder()
52+
request := httptest.NewRequest("GET", "/", nil)
53+
54+
for k, v := range tt.headers {
55+
request.Header.Add(k, v)
56+
}
57+
58+
RenderHTTPResponse(writer, tt.value, tmpl, request)
59+
60+
assert.Equal(t, tt.expected, writer.Body.String())
61+
})
62+
}
63+
}

0 commit comments

Comments
 (0)