Code

Added support for simple SVG graphs for metrics.
[sysdb/webui.git] / server / server.go
index 995f2558c7bd118854ba6db28e0f9c5fb63db2f3..59c1e4215a468bea1a3a36813eafc97014bfac99 100644 (file)
@@ -44,8 +44,11 @@ import (
 
 // A Config specifies configuration values for a SysDB web server.
 type Config struct {
-       // Conn specifies a connection to a SysDB server instance.
-       Conn *client.Conn
+       // Conns is a slice of connections to a SysDB server instance. The number of
+       // elements specifies the maximum number of parallel queries to the backend.
+       // Note that a client connection is not thread-safe but multiple idle
+       // connections don't impose any load on the server.
+       Conns []*client.Conn
 
        // TemplatePath specifies the relative or absolute location of template files.
        TemplatePath string
@@ -56,19 +59,32 @@ type Config struct {
 
 // A Server implements an http.Handler that serves the SysDB user interface.
 type Server struct {
-       c *client.Conn
+       conns chan *client.Conn
+
+       // Request multiplexer
+       mux map[string]handler
 
        // Templates:
        main    *template.Template
        results map[string]*template.Template
 
-       // Static content:
-       static http.Handler
+       // Base directory of static files.
+       basedir string
 }
 
 // New constructs a new SysDB web server using the specified configuration.
 func New(cfg Config) (*Server, error) {
-       s := &Server{c: cfg.Conn, results: make(map[string]*template.Template)}
+       if len(cfg.Conns) == 0 {
+               return nil, errors.New("need at least one client connection")
+       }
+
+       s := &Server{
+               conns:   make(chan *client.Conn, len(cfg.Conns)),
+               results: make(map[string]*template.Template),
+       }
+       for _, c := range cfg.Conns {
+               s.conns <- c
+       }
 
        var err error
        s.main, err = cfg.parse("main.tmpl")
@@ -84,7 +100,12 @@ func New(cfg Config) (*Server, error) {
                }
        }
 
-       s.static = http.FileServer(http.Dir(cfg.StaticPath))
+       s.basedir = cfg.StaticPath
+       s.mux = map[string]handler{
+               "images": s.static,
+               "style":  s.static,
+               "graph":  s.graph,
+       }
        return s, nil
 }
 
@@ -99,7 +120,10 @@ type request struct {
        args []string
 }
 
-var handlers = map[string]func(request, *Server) (template.HTML, error){
+type handler func(http.ResponseWriter, request)
+
+// Content generators for HTML pages.
+var content = map[string]func(request, *Server) (template.HTML, error){
        "": index,
 
        // Queries
@@ -121,11 +145,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        }
        fields := strings.Split(path, "/")
 
-       if fields[0] == "style" || fields[0] == "images" {
-               s.static.ServeHTTP(w, r)
-               return
-       }
-
        req := request{
                r:   r,
                cmd: fields[0],
@@ -140,7 +159,12 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
                }
        }
 
-       f, ok := handlers[req.cmd]
+       if h := s.mux[fields[0]]; h != nil {
+               h(w, req)
+               return
+       }
+
+       f, ok := content[req.cmd]
        if !ok {
                s.notfound(w, r)
                return
@@ -173,6 +197,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
        io.Copy(w, &buf)
 }
 
+// static serves static content.
+func (s *Server) static(w http.ResponseWriter, req request) {
+       http.ServeFile(w, req.r, filepath.Clean(filepath.Join(s.basedir, req.r.URL.Path)))
+}
+
 // Content handlers.
 
 func index(_ request, s *Server) (template.HTML, error) {
@@ -251,16 +280,19 @@ func html(s string) template.HTML {
 }
 
 func (s *Server) query(cmd string) (interface{}, error) {
+       c := <-s.conns
+       defer func() { s.conns <- c }()
+
        m := &proto.Message{
                Type: proto.ConnectionQuery,
                Raw:  []byte(cmd),
        }
-       if err := s.c.Send(m); err != nil {
+       if err := c.Send(m); err != nil {
                return nil, fmt.Errorf("Query %q: %v", cmd, err)
        }
 
        for {
-               m, err := s.c.Receive()
+               m, err := c.Receive()
                if err != nil {
                        return nil, fmt.Errorf("Failed to receive server response: %v", err)
                }
@@ -286,6 +318,10 @@ func (s *Server) query(cmd string) (interface{}, error) {
                        var host sysdb.Host
                        err = proto.Unmarshal(m, &host)
                        res = host
+               case proto.Timeseries:
+                       var ts sysdb.Timeseries
+                       err = proto.Unmarshal(m, &ts)
+                       res = ts
                default:
                        return nil, fmt.Errorf("Unsupported data type %d", t)
                }