Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
mmcgahan committed Aug 16, 2014
0 parents commit d715075
Show file tree
Hide file tree
Showing 24 changed files with 9,715 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
test-results/
tmp/
routes/
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Gourd

Analytics dashboard framework written in golang and AngularJS

# Features

- pluggable, concurrent data loggers
- custom data processors

- admin panel for visualizations

# Stack

- Nginx reverse proxy to static and go server
- Revel framework
- AngularJS
- some kinda realtime charts
13 changes: 13 additions & 0 deletions app/controllers/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package controllers

import (
"github.com/revel/revel"
)

type App struct {
*revel.Controller
}

func (c App) Index() revel.Result {
return c.Render()
}
58 changes: 58 additions & 0 deletions app/controllers/monitor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package controllers

import (
"code.google.com/p/go.net/websocket"
"github.com/revel/revel"
"gourd/app/stream"
"strconv"
)

type WebSocket struct {
*revel.Controller
}

func (c WebSocket) StreamSocket(ws *websocket.Conn) revel.Result {
// activate new Stream, with NewPoints channel
watcher := stream.Watch()
defer watcher.Unwatch()

// In order to select between websocket messages and newPoint data, we
// need to stuff websocket events into a channel.
newMessages := make(chan string)
go func() {
// listener on socket
var msg string
for {
err := websocket.Message.Receive(ws, &msg)
if err != nil {
close(newMessages)
return
}
newMessages <- msg
}
}()

// Now listen for new data
// on the websocket (data) and newMessages (msg, ok).
for {
select {
case point := <-watcher.NewPoints:
// broadcast NewPoint to socket as JSON, e.g.
// {"XVal":1,"YVal":2,"Timestamp":1407452575,"Label":"C"}
if websocket.JSON.Send(ws, &point) != nil {
// They disconnected.
return nil
}
case rate, ok := <-newMessages:
// process received message on the socket
// If the channel is closed, they disconnected.
if !ok {
return nil
}
// Otherwise, say something.
i, _ := strconv.Atoi(rate)
stream.Throttle(i)
}
}
return nil
}
38 changes: 38 additions & 0 deletions app/init.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package app

import "github.com/revel/revel"

func init() {
// Filters is the default set of global filters.
revel.Filters = []revel.Filter{
revel.PanicFilter, // Recover from panics and display an error page instead.
revel.RouterFilter, // Use the routing table to select the right Action
revel.FilterConfiguringFilter, // A hook for adding or removing per-Action filters.
revel.ParamsFilter, // Parse parameters into Controller.Params.
revel.SessionFilter, // Restore and write the session cookie.
revel.FlashFilter, // Restore and write the flash cookie.
revel.ValidationFilter, // Restore kept validation errors and save new ones from cookie.
revel.I18nFilter, // Resolve the requested language
HeaderFilter, // Add some security based headers
revel.InterceptorFilter, // Run interceptors around the action.
revel.CompressFilter, // Compress the result.
revel.ActionInvoker, // Invoke the action.
}

// register startup functions with OnAppStart
// ( order dependent )
// revel.OnAppStart(InitDB())
// revel.OnAppStart(FillCache())
}

// TODO turn this into revel.HeaderFilter
// should probably also have a filter for CSRF
// not sure if it can go in the same filter or not
var HeaderFilter = func(c *revel.Controller, fc []revel.Filter) {
// Add some common security headers
c.Response.Out.Header().Add("X-Frame-Options", "SAMEORIGIN")
c.Response.Out.Header().Add("X-XSS-Protection", "1; mode=block")
c.Response.Out.Header().Add("X-Content-Type-Options", "nosniff")

fc[0](c, fc[1:]) // Execute the next filter stage.
}
122 changes: 122 additions & 0 deletions app/stream/stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package stream

import (
"container/list"
"math/rand"
"time"
)

type Point struct {
XVal int
YVal int
Label string
}

type Stream struct {
NewPoints chan Point // New points coming in
}

func (s Stream) Unwatch() {
// Owner of a Stream must Unwatch it when they stop listening to events.
// This gets called automatically with .defer after Watch()
unwatch <- s // unwatch the channel - send the channel to the unwatch channel
drain(s.NewPoints) // Drain it, just in case there was a pending publish.
}

func Watch() Stream {
// resp := make(chan Stream) // make new channel of Streams
// watch <- resp // put the new Stream into the watch channel
// return <-resp // return the sending-only channel
stream := Stream{make(chan Point)}
watch <- stream
return stream
}

func Publish(point *Point) {
publish <- *point
}

func Throttle(rate int) {
throttle <- rate
}

var (
// Send a stream here to connect to data feed.
watch = make(chan Stream) // receiving channel

// Send a stream here to to disconnect from data feed.
unwatch = make(chan Stream)

// Send points here to publish them to all streams.
publish = make(chan Point)

// send rate integers here to throttle rate of new points
throttle = make(chan int)
rate = 1
)

// This function loops forever, handling the data stream pubsub
func stream() {
// private list of Streams, one per websocket
watchers := list.New()

for {
select {
// listen to all 'global' channels
case stream := <-watch:
// put stream into list of watcher streams
watchers.PushBack(stream)

case stream := <-unwatch:
// watcher disconnected - find the channel and remove it
for watcher := watchers.Front(); watcher != nil; watcher = watcher.Next() {
// watcher.Value.(Stream) gets the underlying element, with type Stream
if watcher.Value.(Stream) == stream {
watchers.Remove(watcher)
break
}
}

case point := <-publish:
// new point, send it to the NewPoints channel of all watcher streams
for watcher := watchers.Front(); watcher != nil; watcher = watcher.Next() {
watcher.Value.(Stream).NewPoints <- point
}

case newRate := <-throttle:
rate = newRate
}
}
}

// go routine for generating random points
func generate() {
rand.Seed(42)
labels := []string{"A", "B", "C"} // all possible Point Labels
for {
idx := rand.Intn(len(labels))
Publish(&Point{rand.Intn(10), rand.Intn(10), labels[idx]})
time.Sleep(time.Second * time.Duration(rate))
}
}

func init() {
go stream()
go generate()
}

// Helpers

// Drains a given channel of any points.
func drain(ch <-chan Point) { // <-chan can only release data
for {
select {
case _, ok := <-ch:
if !ok {
return
}
default:
return
}
}
}
40 changes: 40 additions & 0 deletions app/views/App/Index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<html ng-app="app">
<head>
<title>[[.title]]</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<link rel="stylesheet" type="text/css" href="/public/css/bootstrap.css">
<link rel="shortcut icon" type="image/png" href="/public/img/favicon.png">
<script src="/public/js/jquery-1.9.1.min.js" type="text/javascript" charset="utf-8"></script>
<script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.21/angular.min.js"></script>
<script src="/public/js/lib/Chart.js"></script>
<script src="/public/js/controllers.js"></script>
</head>
<body>
<header class="hero-unit">
<div class="container">
<div class="row">
<div class="hero-text">
<div id="newMessage">
<input type="button" value="1" class="send">
<input type="button" value="2" class="send">
<input type="button" value="3" class="send">
</div>
<div ng-controller="displays">
<!-- ng-repeat or use go template generation? (performance might degrade quickly for ng-repeat) -->
<p ng-repeat="(label, loc) in displays"><strong>{{ label }}</strong> {{ loc.x }} {{ loc.y }} </p>
</div>
</div>
</div>
</div>
</header>

<div class="container">
<div class="row">
<div class="span6">
[[template "flash.html" .]]
</div>
</div>
</div>

</body>
</html>
64 changes: 64 additions & 0 deletions app/views/debug.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<style type="text/css">
#sidebar {
position: absolute;
right: 0px;
top:69px;
max-width: 75%;
z-index: 1000;
background-color: #fee;
border: thin solid grey;
padding: 10px;
}
#toggleSidebar {
position: absolute;
right: 0px;
top: 50px;
background-color: #fee;
}

</style>
<div id="sidebar" style="display:none;">
<h4>Available pipelines</h4>
<dl>
{{ range $index, $value := .}}
<dt>{{$index}}</dt>
<dd>{{$value}}</dd>
{{end}}
</dl>
<h4>Flash</h4>
<dl>
{{ range $index, $value := .flash}}
<dt>{{$index}}</dt>
<dd>{{$value}}</dd>
{{end}}
</dl>

<h4>Errors</h4>
<dl>
{{ range $index, $value := .errors}}
<dt>{{$index}}</dt>
<dd>{{$value}}</dd>
{{end}}
</dl>
</div>
<a id="toggleSidebar" href="#" class="toggles"><i class="icon-chevron-left"></i></a>

<script>
$sidebar = 0;
$('#toggleSidebar').click(function() {
if ($sidebar === 1) {
$('#sidebar').hide();
$('#toggleSidebar i').addClass('icon-chevron-left');
$('#toggleSidebar i').removeClass('icon-chevron-right');
$sidebar = 0;
}
else {
$('#sidebar').show();
$('#toggleSidebar i').addClass('icon-chevron-right');
$('#toggleSidebar i').removeClass('icon-chevron-left');
$sidebar = 1;
}

return false;
});
</script>
20 changes: 20 additions & 0 deletions app/views/errors/404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Not found</title>
</head>
<body>
{{if eq .RunMode "dev"}}
{{template "errors/404-dev.html" .}}
{{else}}
{{with .Error}}
<h1>
{{.Title}}
</h1>
<p>
{{.Description}}
</p>
{{end}}
{{end}}
</body>
</html>
Loading

0 comments on commit d715075

Please sign in to comment.