diff --git a/README.md b/README.md index c3b537b2675..11c2a5618ba 100644 --- a/README.md +++ b/README.md @@ -236,6 +236,19 @@ You can also report detailed subscription information on a per connection basis } ``` +Monitoring endpoints support JSONP for CORS so you can easily create single page +web applications for monitoring. Simply pass `callback` query parameter to any +endpoint. For example; `http://localhost:8222/connz?callback=cb` + +```javascript +// JQuery example + +$.getJSON('http://localhost:8222/connz?callback=?', function(data) { + console.log(data); +}); + +``` + ## Building This code currently requires at _least_ version 1.1 of Go, but we encourage diff --git a/server/monitor.go b/server/monitor.go index d0be11ec3e3..ed58994009c 100644 --- a/server/monitor.go +++ b/server/monitor.go @@ -140,8 +140,9 @@ func (s *Server) HandleConnz(w http.ResponseWriter, r *http.Request) { if err != nil { Errorf("Error marshalling response to /connz request: %v", err) } - w.Header().Set("Content-Type", "application/json") - w.Write(b) + + // Handle response + ResponseHandler(w, r, b) } func castToSliceString(input []interface{}) []string { @@ -220,8 +221,9 @@ func (s *Server) HandleRoutez(w http.ResponseWriter, r *http.Request) { if err != nil { Errorf("Error marshalling response to /routez request: %v", err) } - w.Header().Set("Content-Type", "application/json") - w.Write(b) + + // Handle response + ResponseHandler(w, r, b) } // HandleStats process HTTP requests for subjects stats. @@ -232,8 +234,9 @@ func (s *Server) HandleSubsz(w http.ResponseWriter, r *http.Request) { if err != nil { Errorf("Error marshalling response to /subscriptionsz request: %v", err) } - w.Header().Set("Content-Type", "application/json") - w.Write(b) + + // Handle response + ResponseHandler(w, r, b) } // Varz will output server information on the monitoring port at /varz. @@ -287,16 +290,16 @@ func myUptime(d time.Duration) string { // HandleRoot will show basic info and links to others handlers. func (s *Server) HandleRoot(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, "gnatsd monitoring

") - vlink := fmt.Sprintf("http://%s/varz", r.Host) - fmt.Fprintf(w, "%s
", vlink, vlink) - clink := fmt.Sprintf("http://%s/connz", r.Host) - fmt.Fprintf(w, "%s
", clink, clink) - rlink := fmt.Sprintf("http://%s/routez", r.Host) - fmt.Fprintf(w, "%s
", rlink, rlink) - slink := fmt.Sprintf("http://%s/subscriptionsz", r.Host) - fmt.Fprintf(w, "%s
", slink, slink) - fmt.Fprint(w, "") + fmt.Fprintf(w, ` + + gnatsd monitoring +

+ http://%s/varz
+ http://%s/connz
+ http://%s/routez
+ http://%s/subscriptionsz
+ + `, r.Host, r.Host, r.Host, r.Host, r.Host, r.Host, r.Host, r.Host) } // HandleVarz will process HTTP requests for server information. @@ -322,8 +325,9 @@ func (s *Server) HandleVarz(w http.ResponseWriter, r *http.Request) { if err != nil { Errorf("Error marshalling response to /varz request: %v", err) } - w.Header().Set("Content-Type", "application/json") - w.Write(b) + + // Handle response + ResponseHandler(w, r, b) } // Grab RSS and PCPU @@ -337,3 +341,19 @@ func updateUsage(v *Varz) { v.CPU = pcpu v.Cores = numCores } + +// ResponseHandler handles responses for monitoring routes +func ResponseHandler(w http.ResponseWriter, r *http.Request, data []byte) { + // Get callback from request + callback := r.URL.Query().Get("callback") + // If callback is not empty then + if callback != "" { + // Response for JSONP + w.Header().Set("Content-Type", "application/javascript") + fmt.Fprintf(w, "%s(%s)", callback, data) + } else { + // Otherwise JSON + w.Header().Set("Content-Type", "application/json") + w.Write(data) + } +} diff --git a/server/monitor_test.go b/server/monitor_test.go index 8909a6f4c16..bb04118731b 100644 --- a/server/monitor_test.go +++ b/server/monitor_test.go @@ -8,6 +8,7 @@ import ( "io/ioutil" "net/http" "net/url" + "strings" "testing" "time" @@ -99,6 +100,10 @@ func TestVarz(t *testing.T) { if resp.StatusCode != 200 { t.Fatalf("Expected a 200 response, got %d\n", resp.StatusCode) } + ct := resp.Header.Get("Content-Type") + if ct != "application/json" { + t.Fatalf("Expected application/json content-type, got %s\n", ct) + } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -151,6 +156,17 @@ func TestVarz(t *testing.T) { if v.OutBytes != 5 { t.Fatalf("Expected OutBytes of 5, got %v\n", v.OutBytes) } + + // Test JSONP + respj, errj := http.Get(fmt.Sprintf("http://localhost:%d/", DEFAULT_HTTP_PORT) + "varz?callback=callback") + if errj != nil { + t.Fatalf("Expected no error: Got %v\n", err) + } + ct = respj.Header.Get("Content-Type") + if ct != "application/javascript" { + t.Fatalf("Expected application/javascript content-type, got %s\n", ct) + } + defer respj.Body.Close() } func TestConnz(t *testing.T) { @@ -165,6 +181,10 @@ func TestConnz(t *testing.T) { if resp.StatusCode != 200 { t.Fatalf("Expected a 200 response, got %d\n", resp.StatusCode) } + ct := resp.Header.Get("Content-Type") + if ct != "application/json" { + t.Fatalf("Expected application/json content-type, got %s\n", ct) + } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -249,6 +269,17 @@ func TestConnz(t *testing.T) { if ci.OutBytes != 5 { t.Fatalf("Expected OutBytes of 1, got %v\n", ci.OutBytes) } + + // Test JSONP + respj, errj := http.Get(fmt.Sprintf("http://localhost:%d/", DEFAULT_HTTP_PORT) + "connz?callback=callback") + if errj != nil { + t.Fatalf("Expected no error: Got %v\n", err) + } + ct = respj.Header.Get("Content-Type") + if ct != "application/javascript" { + t.Fatalf("Expected application/javascript content-type, got %s\n", ct) + } + defer respj.Body.Close() } func TestConnzWithSubs(t *testing.T) { @@ -629,6 +660,10 @@ func TestConnzWithRoutes(t *testing.T) { if resp.StatusCode != 200 { t.Fatalf("Expected a 200 response, got %d\n", resp.StatusCode) } + ct := resp.Header.Get("Content-Type") + if ct != "application/json" { + t.Fatalf("Expected application/json content-type, got %s\n", ct) + } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -682,6 +717,17 @@ func TestConnzWithRoutes(t *testing.T) { if route.DidSolicit != false { t.Fatalf("Expected unsolicited route, got %v\n", route.DidSolicit) } + + // Test JSONP + respj, errj := http.Get(fmt.Sprintf("http://localhost:%d/", DEFAULT_HTTP_PORT) + "routez?callback=callback") + if errj != nil { + t.Fatalf("Expected no error: Got %v\n", err) + } + ct = respj.Header.Get("Content-Type") + if ct != "application/javascript" { + t.Fatalf("Expected application/javascript content-type, got %s\n", ct) + } + defer respj.Body.Close() } func TestSubsz(t *testing.T) { @@ -699,6 +745,10 @@ func TestSubsz(t *testing.T) { if resp.StatusCode != 200 { t.Fatalf("Expected a 200 response, got %d\n", resp.StatusCode) } + ct := resp.Header.Get("Content-Type") + if ct != "application/json" { + t.Fatalf("Expected application/json content-type, got %s\n", ct) + } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { @@ -719,6 +769,38 @@ func TestSubsz(t *testing.T) { t.Fatalf("Expected NumMatches of 1, got %d\n", sl.NumMatches) } + // Test JSONP + respj, errj := http.Get(fmt.Sprintf("http://localhost:%d/", DEFAULT_HTTP_PORT) + "subscriptionsz?callback=callback") + ct = respj.Header.Get("Content-Type") + if errj != nil { + t.Fatalf("Expected no error: Got %v\n", err) + } + if ct != "application/javascript" { + t.Fatalf("Expected application/javascript content-type, got %s\n", ct) + } + defer respj.Body.Close() +} + +// Tests handle root +func TestHandleRoot(t *testing.T) { + s := runMonitorServer(DEFAULT_HTTP_PORT) + defer s.Shutdown() + + nc := createClientConnSubscribeAndPublish(t) + defer nc.Close() + + resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", DEFAULT_HTTP_PORT)) + if err != nil { + t.Fatalf("Expected no error: Got %v\n", err) + } + if resp.StatusCode != 200 { + t.Fatalf("Expected a 200 response, got %d\n", resp.StatusCode) + } + ct := resp.Header.Get("Content-Type") + if !strings.Contains(ct, "text/html") { + t.Fatalf("Expected text/html response, got %s\n", ct) + } + defer resp.Body.Close() } // Create a connection to test ConnInfo