├── .gitignore ├── LICENSE ├── README.md ├── broker.go ├── dashing.go ├── example ├── dashboards │ ├── layout.gerb │ └── sample.gerb ├── jobs │ ├── buzzwords.go │ ├── convergence.go │ └── sample.go ├── main.go └── widgets │ ├── graph │ └── graph.html │ ├── list │ └── list.html │ ├── meter │ └── meter.html │ ├── number │ └── number.html │ └── text │ └── text.html ├── server.go └── worker.go /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | example/example 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2016 Chris Heng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | dashing-go 2 | ========== 3 | 4 | A [Go][1] port of [shopify/dashing][2]. Now frameworkless! 5 | 6 | To generate a dashboard using this library, please use the [Yeoman dashing-go generator][3]. 7 | 8 | For a live demo, here's the default [sample dashboard][4]. 9 | 10 | ### Current Status 11 | 12 | * All endpoints have been ported over! Full functionality is available. 13 | * For an example of how to write jobs in dashing-go, please refer to the [demo dashboard source][5]. 14 | 15 | Credits 16 | ------- 17 | 18 | Much of the code is referenced from [golang-sse-todo][6] by @rwynn. 19 | 20 | [1]: http://golang.org 21 | [2]: http://shopify.github.io/dashing 22 | [3]: https://github.com/gigablah/generator-dashing-go 23 | [4]: http://dashing.kuanyen.net 24 | [5]: https://github.com/gigablah/dashing-go-demo 25 | [6]: https://github.com/rwynn/golang-sse-todo 26 | -------------------------------------------------------------------------------- /broker.go: -------------------------------------------------------------------------------- 1 | package dashing 2 | 3 | // An eventCache stores the latest event for each key, so that new clients can 4 | // catch up. 5 | type eventCache map[string]*Event 6 | 7 | // A Broker broadcasts events to multiple clients. 8 | type Broker struct { 9 | // Create a map of clients, the keys of the map are the channels 10 | // over which we can push messages to attached clients. (The values 11 | // are just booleans and are meaningless) 12 | clients map[chan *Event]bool 13 | 14 | // Channel into which new clients can be pushed 15 | newClients chan chan *Event 16 | 17 | // Channel into which disconnected clients should be pushed 18 | defunctClients chan chan *Event 19 | 20 | // Channel into which events are pushed to be broadcast out 21 | // to attached clients 22 | events chan *Event 23 | 24 | // Cache for most recent events with a certain ID 25 | cache eventCache 26 | } 27 | 28 | // Start managing client connections and event broadcasts. 29 | func (b *Broker) Start() { 30 | go func() { 31 | for { 32 | // Block until we receive from one of the 33 | // three following channels. 34 | select { 35 | case s := <-b.newClients: 36 | // There is a new client attached and we 37 | // want to start sending them events. 38 | b.clients[s] = true 39 | // Send all the cached events so that when a new client connects, it 40 | // doesn't miss previous events 41 | for _, e := range b.cache { 42 | s <- e 43 | } 44 | // log.Println("Added new client") 45 | case s := <-b.defunctClients: 46 | // A client has detached and we want to 47 | // stop sending them events. 48 | delete(b.clients, s) 49 | // log.Println("Removed client") 50 | case event := <-b.events: 51 | b.cache[event.ID] = event 52 | // There is a new event to send. For each 53 | // attached client, push the new event 54 | // into the client's channel. 55 | for s := range b.clients { 56 | s <- event 57 | } 58 | // log.Printf("Broadcast event to %d clients", len(b.clients)) 59 | } 60 | } 61 | }() 62 | } 63 | 64 | // NewBroker creates a Broker instance. 65 | func NewBroker() *Broker { 66 | return &Broker{ 67 | make(map[chan *Event]bool), 68 | make(chan (chan *Event)), 69 | make(chan (chan *Event)), 70 | make(chan *Event), 71 | map[string]*Event{}, 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /dashing.go: -------------------------------------------------------------------------------- 1 | package dashing 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | ) 8 | 9 | // An Event contains the widget ID, a body of data, 10 | // and an optional target (only "dashboard" for now). 11 | type Event struct { 12 | ID string 13 | Body map[string]interface{} 14 | Target string 15 | } 16 | 17 | // Dashing struct definition. 18 | type Dashing struct { 19 | started bool 20 | Broker *Broker 21 | Worker *Worker 22 | Server *Server 23 | Router http.Handler 24 | } 25 | 26 | // ServeHTTP implements the HTTP Handler. 27 | func (d *Dashing) ServeHTTP(w http.ResponseWriter, r *http.Request) { 28 | if !d.started { 29 | panic("dashing.Start() has not been called") 30 | } 31 | d.Router.ServeHTTP(w, r) 32 | } 33 | 34 | // Start actives the broker and workers. 35 | func (d *Dashing) Start() *Dashing { 36 | if !d.started { 37 | if d.Router == nil { 38 | d.Router = d.Server.NewRouter() 39 | } 40 | d.Broker.Start() 41 | d.Worker.Start() 42 | d.started = true 43 | } 44 | return d 45 | } 46 | 47 | // NewDashing sets up the event broker, workers and webservice. 48 | func NewDashing() *Dashing { 49 | broker := NewBroker() 50 | worker := NewWorker(broker) 51 | server := NewServer(broker) 52 | 53 | if os.Getenv("WEBROOT") != "" { 54 | server.webroot = filepath.Clean(os.Getenv("WEBROOT")) + "/" 55 | } 56 | if os.Getenv("DEV") != "" { 57 | server.dev = true 58 | } 59 | 60 | return &Dashing{ 61 | started: false, 62 | Broker: broker, 63 | Worker: worker, 64 | Server: server, 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /example/dashboards/layout.gerb: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 |Paste the following at the top of <%= dashboard %>.gerb
25 | 26 |6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /example/widgets/text/text.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package dashing 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "path/filepath" 9 | "time" 10 | 11 | "gopkg.in/husobee/vestigo.v1" 12 | "gopkg.in/karlseguin/gerb.v0" 13 | ) 14 | 15 | // A Server contains webservice parameters and middlewares. 16 | type Server struct { 17 | dev bool 18 | webroot string 19 | broker *Broker 20 | } 21 | 22 | func param(r *http.Request, name string) string { 23 | return r.FormValue(fmt.Sprintf(":%s", name)) 24 | } 25 | 26 | // IndexHandler redirects to the default dashboard. 27 | func (s *Server) IndexHandler(w http.ResponseWriter, r *http.Request) { 28 | files, _ := filepath.Glob("dashboards/*.gerb") 29 | 30 | for _, file := range files { 31 | dashboard := file[11 : len(file)-5] 32 | if dashboard != "layout" { 33 | http.Redirect(w, r, fmt.Sprintf("/%s", dashboard), http.StatusTemporaryRedirect) 34 | return 35 | } 36 | } 37 | 38 | http.NotFound(w, r) 39 | } 40 | 41 | // EventsHandler opens a keepalive connection and pushes events to the client. 42 | func (s *Server) EventsHandler(w http.ResponseWriter, r *http.Request) { 43 | f, ok := w.(http.Flusher) 44 | if !ok { 45 | http.Error(w, "Streaming unsupported!", http.StatusInternalServerError) 46 | return 47 | } 48 | 49 | c, ok := w.(http.CloseNotifier) 50 | if !ok { 51 | http.Error(w, "Close notification unsupported!", http.StatusInternalServerError) 52 | return 53 | } 54 | 55 | // Create a new channel, over which the broker can 56 | // send this client events. 57 | events := make(chan *Event) 58 | 59 | // Add this client to the map of those that should 60 | // receive updates 61 | s.broker.newClients <- events 62 | 63 | // Remove this client from the map of attached clients 64 | // when the handler exits. 65 | defer func() { 66 | s.broker.defunctClients <- events 67 | }() 68 | 69 | w.Header().Set("Content-Type", "text/event-stream") 70 | w.Header().Set("Cache-Control", "no-cache") 71 | w.Header().Set("Connection", "keep-alive") 72 | w.Header().Set("X-Accel-Buffering", "no") 73 | closer := c.CloseNotify() 74 | 75 | for { 76 | select { 77 | case event := <-events: 78 | data := event.Body 79 | data["id"] = event.ID 80 | data["updatedAt"] = int32(time.Now().Unix()) 81 | json, err := json.Marshal(data) 82 | if err != nil { 83 | continue 84 | } 85 | if event.Target != "" { 86 | fmt.Fprintf(w, "event: %s\n", event.Target) 87 | } 88 | fmt.Fprintf(w, "data: %s\n\n", json) 89 | f.Flush() 90 | case <-closer: 91 | log.Println("Closing connection") 92 | return 93 | } 94 | } 95 | } 96 | 97 | // DashboardHandler serves the dashboard layout template. 98 | func (s *Server) DashboardHandler(w http.ResponseWriter, r *http.Request) { 99 | dashboard := param(r, "dashboard") 100 | if dashboard == "" { 101 | dashboard = fmt.Sprintf("events%s", param(r, "suffix")) 102 | } 103 | template, err := gerb.ParseFile(true, fmt.Sprintf("dashboards/%s.gerb", dashboard), "dashboards/layout.gerb") 104 | 105 | if err != nil { 106 | http.NotFound(w, r) 107 | return 108 | } 109 | 110 | w.Header().Set("Content-Type", "text/html; charset=UTF-8") 111 | 112 | template.Render(w, map[string]interface{}{ 113 | "dashboard": dashboard, 114 | "development": s.dev, 115 | "request": r, 116 | }) 117 | } 118 | 119 | // DashboardEventHandler accepts dashboard events. 120 | func (s *Server) DashboardEventHandler(w http.ResponseWriter, r *http.Request) { 121 | if r.Body != nil { 122 | defer r.Body.Close() 123 | } 124 | 125 | var data map[string]interface{} 126 | 127 | if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 128 | http.Error(w, "", http.StatusBadRequest) 129 | return 130 | } 131 | 132 | s.broker.events <- &Event{param(r, "id"), data, "dashboards"} 133 | 134 | w.WriteHeader(http.StatusNoContent) 135 | } 136 | 137 | // WidgetHandler serves widget templates. 138 | func (s *Server) WidgetHandler(w http.ResponseWriter, r *http.Request) { 139 | widget := param(r, "widget") 140 | widget = widget[0 : len(widget)-5] 141 | template, err := gerb.ParseFile(true, fmt.Sprintf("widgets/%s/%s.html", widget, widget)) 142 | 143 | if err != nil { 144 | log.Printf("%v", err) 145 | http.NotFound(w, r) 146 | return 147 | } 148 | 149 | w.Header().Set("Content-Type", "text/html; charset=UTF-8") 150 | 151 | template.Render(w, nil) 152 | } 153 | 154 | // WidgetEventHandler accepts widget data. 155 | func (s *Server) WidgetEventHandler(w http.ResponseWriter, r *http.Request) { 156 | if r.Body != nil { 157 | defer r.Body.Close() 158 | } 159 | 160 | var data map[string]interface{} 161 | 162 | if err := json.NewDecoder(r.Body).Decode(&data); err != nil { 163 | log.Printf("%v", err) 164 | http.Error(w, "", http.StatusBadRequest) 165 | return 166 | } 167 | 168 | s.broker.events <- &Event{param(r, "id"), data, ""} 169 | 170 | w.WriteHeader(http.StatusNoContent) 171 | } 172 | 173 | // NewRouter creates a router with defaults. 174 | func (s *Server) NewRouter() http.Handler { 175 | r := vestigo.NewRouter() 176 | r.Get("/", s.IndexHandler) 177 | r.Get("/events", s.EventsHandler) 178 | r.Get("/events:suffix", s.DashboardHandler) // workaround for router edge case 179 | r.Get("/:dashboard", s.DashboardHandler) 180 | r.Post("/dashboards/:id", s.DashboardEventHandler) 181 | r.Get("/views/:widget", s.WidgetHandler) 182 | r.Post("/widgets/:id", s.WidgetEventHandler) 183 | return r 184 | } 185 | 186 | // NewServer creates a Server instance. 187 | func NewServer(b *Broker) *Server { 188 | return &Server{ 189 | dev: false, 190 | webroot: "", 191 | broker: b, 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /worker.go: -------------------------------------------------------------------------------- 1 | package dashing 2 | 3 | // A Job does periodic work and sends events to a channel. 4 | type Job interface { 5 | Work(send chan *Event) 6 | } 7 | 8 | // A Worker contains a collection of jobs. 9 | type Worker struct { 10 | broker *Broker 11 | registry []Job 12 | } 13 | 14 | // Register a job for a particular worker. 15 | func (w *Worker) Register(j Job) { 16 | if j == nil { 17 | panic("Can't register nil job") 18 | } 19 | w.registry = append(w.registry, j) 20 | } 21 | 22 | // Start all jobs. 23 | func (w *Worker) Start() { 24 | for _, j := range w.registry { 25 | go j.Work(w.broker.events) 26 | } 27 | } 28 | 29 | // NewWorker returns a Worker instance. 30 | func NewWorker(b *Broker) *Worker { 31 | return &Worker{ 32 | broker: b, 33 | registry: append([]Job(nil), jobs...), 34 | } 35 | } 36 | 37 | // Global registry for background jobs. 38 | var jobs []Job 39 | 40 | // Register a job to be kicked off upon starting a worker. 41 | func Register(j Job) { 42 | if j == nil { 43 | panic("Can't register nil job") 44 | } 45 | jobs = append(jobs, j) 46 | } 47 | --------------------------------------------------------------------------------