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 pretty-print of JSON via HTTP API #643

Closed
wants to merge 1 commit into from
Closed
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
55 changes: 39 additions & 16 deletions src/api/http/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@ func NewHttpServer(httpPort string, readTimeout time.Duration, adminAssetsDir st

const (
INVALID_CREDENTIALS_MSG = "Invalid database/username/password"
JSON_PRETTY_PRINT_INDENT = " "
)

func isPretty(r *libhttp.Request) bool {
return r.URL.Query().Get("pretty") == "true"
}

func (self *HttpServer) EnableSsl(addr, certPath string) {
if addr == "" || certPath == "" {
// don't enable ssl unless both the address and the certificate
Expand Down Expand Up @@ -211,6 +216,7 @@ type AllPointsWriter struct {
memSeries map[string]*protocol.Series
w libhttp.ResponseWriter
precision TimePrecision
pretty bool
}

func (self *AllPointsWriter) yield(series *protocol.Series) error {
Expand All @@ -225,7 +231,7 @@ func (self *AllPointsWriter) yield(series *protocol.Series) error {
}

func (self *AllPointsWriter) done() {
data, err := serializeMultipleSeries(self.memSeries, self.precision)
data, err := serializeMultipleSeries(self.memSeries, self.precision, self.pretty)
if err != nil {
self.w.WriteHeader(libhttp.StatusInternalServerError)
self.w.Write([]byte(err.Error()))
Expand All @@ -240,10 +246,11 @@ type ChunkWriter struct {
w libhttp.ResponseWriter
precision TimePrecision
wroteContentType bool
pretty bool
}

func (self *ChunkWriter) yield(series *protocol.Series) error {
data, err := serializeSingleSeries(series, self.precision)
data, err := serializeSingleSeries(series, self.precision, self.pretty)
if err != nil {
return err
}
Expand Down Expand Up @@ -292,6 +299,7 @@ func (self *HttpServer) sendCrossOriginHeader(w libhttp.ResponseWriter, r *libht
func (self *HttpServer) query(w libhttp.ResponseWriter, r *libhttp.Request) {
query := r.URL.Query().Get("q")
db := r.URL.Query().Get(":db")
pretty := isPretty(r)

self.tryAsDbUserAndClusterAdmin(w, r, func(user User) (int, interface{}) {

Expand All @@ -302,9 +310,9 @@ func (self *HttpServer) query(w libhttp.ResponseWriter, r *libhttp.Request) {

var writer Writer
if r.URL.Query().Get("chunked") == "true" {
writer = &ChunkWriter{w, precision, false}
writer = &ChunkWriter{w, precision, false, pretty}
} else {
writer = &AllPointsWriter{map[string]*protocol.Series{}, w, precision}
writer = &AllPointsWriter{map[string]*protocol.Series{}, w, precision, pretty}
}
seriesWriter := NewSeriesWriter(writer.yield)
err = self.coordinator.RunQuery(user, db, query, seriesWriter)
Expand Down Expand Up @@ -447,18 +455,26 @@ type Point struct {
Values []interface{} `json:"values"`
}

func serializeSingleSeries(series *protocol.Series, precision TimePrecision) ([]byte, error) {
func serializeSingleSeries(series *protocol.Series, precision TimePrecision, pretty bool) ([]byte, error) {
arg := map[string]*protocol.Series{"": series}
return json.Marshal(SerializeSeries(arg, precision)[0])
if pretty {
return json.MarshalIndent(SerializeSeries(arg, precision)[0], "", JSON_PRETTY_PRINT_INDENT)
} else {
return json.Marshal(SerializeSeries(arg, precision)[0])
}
}

func serializeMultipleSeries(series map[string]*protocol.Series, precision TimePrecision) ([]byte, error) {
return json.Marshal(SerializeSeries(series, precision))
func serializeMultipleSeries(series map[string]*protocol.Series, precision TimePrecision, pretty bool) ([]byte, error) {
if pretty {
return json.MarshalIndent(SerializeSeries(series, precision), "", JSON_PRETTY_PRINT_INDENT)
} else {
return json.Marshal(SerializeSeries(series, precision))
}
}

// // cluster admins management interface

func toBytes(body interface{}) ([]byte, string, error) {
func toBytes(body interface{}, pretty bool) ([]byte, string, error) {
if body == nil {
return nil, "text/plain", nil
}
Expand All @@ -468,14 +484,21 @@ func toBytes(body interface{}) ([]byte, string, error) {
case []byte:
return x, "text/plain", nil
default:
body, err := json.Marshal(body)
return body, "application/json", err
// only JSON output is prettied up.
var b []byte
var e error
if pretty {
b, e = json.MarshalIndent(body, "", JSON_PRETTY_PRINT_INDENT)
} else {
b, e = json.Marshal(body)
}
return b, "application/json", e
}
}

func yieldUser(user User, yield func(User) (int, interface{})) (int, string, []byte) {
func yieldUser(user User, yield func(User) (int, interface{}), pretty bool) (int, string, []byte) {
statusCode, body := yield(user)
bodyContent, contentType, err := toBytes(body)
bodyContent, contentType, err := toBytes(body, pretty)
if err != nil {
return libhttp.StatusInternalServerError, "text/plain", []byte(err.Error())
}
Expand Down Expand Up @@ -536,7 +559,7 @@ func (self *HttpServer) tryAsClusterAdmin(w libhttp.ResponseWriter, r *libhttp.R
w.Write([]byte(err.Error()))
return
}
statusCode, contentType, body := yieldUser(user, yield)
statusCode, contentType, body := yieldUser(user, yield, isPretty(r))
if statusCode < 0 {
return
}
Expand Down Expand Up @@ -689,7 +712,7 @@ func (self *HttpServer) tryAsDbUser(w libhttp.ResponseWriter, r *libhttp.Request
return libhttp.StatusUnauthorized, []byte(err.Error())
}

statusCode, contentType, v := yieldUser(user, yield)
statusCode, contentType, v := yieldUser(user, yield, isPretty(r))
if statusCode == libhttp.StatusUnauthorized {
w.Header().Add("WWW-Authenticate", "Basic realm=\"influxdb\"")
}
Expand Down Expand Up @@ -887,7 +910,7 @@ func (self *HttpServer) listInterfaces(w libhttp.ResponseWriter, r *libhttp.Requ
}
}
return libhttp.StatusOK, directories
})
}, isPretty(r))

w.Header().Add("content-type", contentType)
w.WriteHeader(statusCode)
Expand Down
100 changes: 100 additions & 0 deletions src/api/http/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,53 @@ func (self *ApiSuite) TestNotChunkedQuery(c *C) {
c.Assert(int64(series[0].Points[0][0].(float64)), Equals, int64(1381346631000))
}


func (self *ApiSuite) TestNotChunkedPrettyQuery(c *C) {
query := "select * from foo where column_one == 'some_value';"
query = url.QueryEscape(query)
addr := self.formatUrl("/db/foo/series?q=%s&u=dbuser&p=password&pretty=true", query)
resp, err := libhttp.Get(addr)
c.Assert(err, IsNil)
defer resp.Body.Close()
c.Assert(resp.StatusCode, Equals, libhttp.StatusOK)
c.Assert(resp.Header.Get("content-type"), Equals, "application/json")
data, err := ioutil.ReadAll(resp.Body)
c.Assert(err, IsNil)
series := []SerializedSeries{}
err = json.Unmarshal(data, &series)
c.Assert(err, IsNil)
c.Assert(series, HasLen, 1)
c.Assert(series[0].Name, Equals, "foo")
// time, seq, column_one, column_two
c.Assert(series[0].Columns, HasLen, 4)
c.Assert(series[0].Points, HasLen, 4)
// timestamp precision is milliseconds by default
c.Assert(int64(series[0].Points[0][0].(float64)), Equals, int64(1381346631000))
}

func (self *ApiSuite) TestNotChunkedNotPrettyQuery(c *C) {
query := "select * from foo where column_one == 'some_value';"
query = url.QueryEscape(query)
addr := self.formatUrl("/db/foo/series?q=%s&u=dbuser&p=password&pretty=false", query)
resp, err := libhttp.Get(addr)
c.Assert(err, IsNil)
defer resp.Body.Close()
c.Assert(resp.StatusCode, Equals, libhttp.StatusOK)
c.Assert(resp.Header.Get("content-type"), Equals, "application/json")
data, err := ioutil.ReadAll(resp.Body)
c.Assert(err, IsNil)
series := []SerializedSeries{}
err = json.Unmarshal(data, &series)
c.Assert(err, IsNil)
c.Assert(series, HasLen, 1)
c.Assert(series[0].Name, Equals, "foo")
// time, seq, column_one, column_two
c.Assert(series[0].Columns, HasLen, 4)
c.Assert(series[0].Points, HasLen, 4)
// timestamp precision is milliseconds by default
c.Assert(int64(series[0].Points[0][0].(float64)), Equals, int64(1381346631000))
}

func (self *ApiSuite) TestChunkedQuery(c *C) {
query := "select * from foo where column_one == 'some_value';"
query = url.QueryEscape(query)
Expand All @@ -382,6 +429,30 @@ func (self *ApiSuite) TestChunkedQuery(c *C) {
}
}

func (self *ApiSuite) TestPrettyChunkedQuery(c *C) {
query := "select * from foo where column_one == 'some_value';"
query = url.QueryEscape(query)
addr := self.formatUrl("/db/foo/series?q=%s&chunked=true&u=dbuser&p=password&pretty=true", query)
resp, err := libhttp.Get(addr)
c.Assert(err, IsNil)
defer resp.Body.Close()
c.Assert(resp.Header.Get("content-type"), Equals, "application/json")

for i := 0; i < 2; i++ {
chunk := make([]byte, 2048, 2048)
n, err := resp.Body.Read(chunk)

series := SerializedSeries{}
err = json.Unmarshal(chunk[0:n], &series)
c.Assert(err, IsNil)
c.Assert(series.Name, Equals, "foo")
// time, seq, column_one, column_two
c.Assert(series.Columns, HasLen, 4)
// each chunk should have 2 points
c.Assert(series.Points, HasLen, 2)
}
}

func (self *ApiSuite) TestWriteDataWithTimeInSeconds(c *C) {
data := `
[
Expand Down Expand Up @@ -706,6 +777,20 @@ func (self *ApiSuite) TestClusterAdminsIndex(c *C) {
c.Assert(users, DeepEquals, []*ApiUser{&ApiUser{"root"}})
}

func (self *ApiSuite) TestPrettyClusterAdminsIndex(c *C) {
url := self.formatUrl("/cluster_admins?u=root&p=root&pretty=true")
resp, err := libhttp.Get(url)
c.Assert(err, IsNil)
c.Assert(resp.Header.Get("content-type"), Equals, "application/json")
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
c.Assert(err, IsNil)
users := []*ApiUser{}
err = json.Unmarshal(body, &users)
c.Assert(err, IsNil)
c.Assert(users, DeepEquals, []*ApiUser{&ApiUser{"root"}})
}

func (self *ApiSuite) TestDbUsersIndex(c *C) {
url := self.formatUrl("/db/db1/users?u=root&p=root")
resp, err := libhttp.Get(url)
Expand All @@ -721,6 +806,21 @@ func (self *ApiSuite) TestDbUsersIndex(c *C) {
c.Assert(users[0], DeepEquals, &UserDetail{"db_user1", false})
}

func (self *ApiSuite) TestPrettyDbUsersIndex(c *C) {
url := self.formatUrl("/db/db1/users?u=root&p=root&pretty=true")
resp, err := libhttp.Get(url)
c.Assert(err, IsNil)
c.Assert(resp.Header.Get("content-type"), Equals, "application/json")
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
c.Assert(err, IsNil)
users := []*UserDetail{}
err = json.Unmarshal(body, &users)
c.Assert(err, IsNil)
c.Assert(users, HasLen, 1)
c.Assert(users[0], DeepEquals, &UserDetail{"db_user1", false})
}

func (self *ApiSuite) TestDbUserShow(c *C) {
url := self.formatUrl("/db/db1/users/db_user1?u=root&p=root")
resp, err := libhttp.Get(url)
Expand Down