├── .gitignore ├── .travis.yml ├── Dockerfile ├── GLOCKFILE ├── Makefile ├── README.md ├── docs └── graphs.png ├── grafana └── grafana.go ├── main.go ├── store.go ├── templates.go └── templates ├── config.js ├── datasource.gofana.js └── signin.html /.gitignore: -------------------------------------------------------------------------------- 1 | gofana 2 | dist 3 | *.gz 4 | grafana-* 5 | *.pem 6 | dashboards 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.3 4 | - 1.4 5 | - tip 6 | install: 7 | - make deps 8 | script: 9 | - make all 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:wheezy 2 | MAINTAINER Jason Wilder 3 | 4 | RUN mkdir /app 5 | WORKDIR /app 6 | 7 | ENV GOFANA_VERSION v0.0.6 8 | ADD https://github.com/jwilder/gofana/releases/download/$GOFANA_VERSION/gofana-linux-amd64-$GOFANA_VERSION.tar.gz gofana-linux-amd64-$GOFANA_VERSION.tar.gz 9 | RUN gunzip -c /gofana-linux-amd64-$GOFANA_VERSION.tar.gz | tar -C /app -xvf - > /app/gofana 10 | 11 | VOLUME /app/dashboards 12 | 13 | EXPOSE 8080 14 | EXPOSE 8443 15 | 16 | ENTRYPOINT ["/app/gofana"] 17 | -------------------------------------------------------------------------------- /GLOCKFILE: -------------------------------------------------------------------------------- 1 | github.com/boj/redistore 9d1554b400d726c7aa54b6e4a836f273b72b97f7 2 | github.com/clbanning/x2j 0e45c2228aab9921ef50cc7d0bf7186082cc51a4 3 | github.com/codegangsta/inject 9aea7a2fa5b79ef7fc00f63a575e72df33b4e886 4 | github.com/garyburd/redigo 61e2910408efd40dafb3c7d856a4cf8aeb5fb1c6 5 | github.com/go-martini/martini bbd489b35fd7eac9aaf442e5671ab21cdd972926 6 | github.com/gorilla/context a08edd30ad9e104612741163dc087a613829a23c 7 | github.com/gorilla/securecookie f0559d009e6943011cd1ba6eb16195cf5e0ed326 8 | github.com/gorilla/sessions 13c86220d944600e7a6e3de6c87418896ccfbe77 9 | github.com/martini-contrib/auth a7b827c87c2d3e571c974f1fec378def3f2ea1ed 10 | github.com/martini-contrib/render b65063311fac6cbf84d3538f5715f7847e648322 11 | github.com/martini-contrib/secure 56b905eb8ab2eb124d2f4acfa3ea893df45b7d79 12 | github.com/martini-contrib/sessions fa13114fbcf036dff8185d728651d85fcef772db 13 | github.com/martini-contrib/staticbin b9631fb8c188bec7aefed202f05fb3b76abadced 14 | github.com/oxtoacart/bpool 923fab21d0624a469b877a05bbfc562c1c26c663 15 | github.com/stretchr/codecs 8e29b38960a541ed1264b001dcb21701283c590d 16 | github.com/stretchr/gomniauth 64e995fba5a513975f4ef888284d195073fb6762 17 | github.com/stretchr/objx cbeaeb16a013161a98496fad62933b1d21786672 18 | github.com/stretchr/signature 78854fe0e7853ac2d799afb0301e41fa48d7fca1 19 | github.com/stretchr/stew 80ef0842b48b329a6120607c817a85aff5a2ec71 20 | github.com/stretchr/tracer 66d3696bba973e1697f7d68413b3f19c6d6e2a7c 21 | github.com/ugorji/go 71c2886f5a673a35f909803f38ece5810165097b 22 | labix.org/v2/mgo 287 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | TAG:=`git describe --abbrev=0 --tags` 2 | LDFLAGS:=-X main.buildVersion $(TAG) 3 | GRAFANA_VERSION=1.9.1 4 | 5 | .SILENT : grafana-$(GRAFANA_VERSION).tar.gz 6 | .PHONY : gofana clean run 7 | 8 | all: gofana 9 | 10 | deps: 11 | go get github.com/jteeuwen/go-bindata/... 12 | go get github.com/robfig/glock 13 | glock sync github.com/jwilder/gofana 14 | 15 | grafana-$(GRAFANA_VERSION).tar.gz: 16 | wget http://grafanarel.s3.amazonaws.com/grafana-$(GRAFANA_VERSION).tar.gz 17 | 18 | grafana-$(GRAFANA_VERSION): grafana-$(GRAFANA_VERSION).tar.gz 19 | tar xvzf grafana-$(GRAFANA_VERSION).tar.gz 20 | 21 | download-grafana: grafana-$(GRAFANA_VERSION) 22 | 23 | gofana: download-grafana 24 | echo "Building gofana" 25 | go-bindata -o templates.go templates/ 26 | go-bindata -o grafana/grafana.go -pkg grafana grafana-$(GRAFANA_VERSION)/... 27 | go build -ldflags "$(LDFLAGS)" 28 | 29 | dist-clean: 30 | rm -rf dist 31 | rm -f gofana-linux-*.tar.gz 32 | rm -f gofana-darwin-*.tar.gz 33 | 34 | dist: dist-clean 35 | mkdir -p dist/linux/amd64 && GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/linux/amd64/gofana 36 | mkdir -p dist/darwin/amd64 && GOOS=darwin GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o dist/darwin/amd64/gofana 37 | 38 | release: deps dist 39 | tar -cvzf gofana-linux-amd64-$(TAG).tar.gz -C dist/linux/amd64 gofana 40 | tar -cvzf gofana-darwin-amd64-$(TAG).tar.gz -C dist/darwin/amd64 gofana 41 | 42 | run: gofana 43 | ./gofana -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gofana 2 | 3 | ![latest v0.0.6](https://img.shields.io/badge/latest-v0.0.6-brightgreen.svg) 4 | ![grafana 1.9.1](https://img.shields.io/badge/grafana-1.9.1-orange.svg) ![License MIT](https://img.shields.io/badge/license-MIT-blue.svg) 5 | [![Build Status](https://travis-ci.org/jwilder/gofana.svg?branch=master)](https://travis-ci.org/jwilder/gofana) 6 | 7 | Gofana is a self-contained [Grafana](http://grafana.org/) web server written in Go with no extra dependencies. It's designed to make it easy to setup a secure, Grafana-based dashboard system with your existing Graphite, InfluxDB or OpenTSDB servers. There is no additional nginx/apache, ElasticSearch installation or configuration required. 8 | 9 | It handles dashboard storage so that saved dashboards do not need to be saved in Elasticsearch or within InfluxDB making it easier to maintain and back them up. 10 | 11 | It will also proxy Graphite, InfluxDB and OpenTSDB queries to simplifly serving Grafana over HTTPS as well as removing the need for having Grafana query your metrics store directly. 12 | 13 | ![Grafana Graphs](docs/graphs.png "Grafana Graphs") 14 | 15 | ## Features 16 | 17 | * Self-contained grafana 1.9.1 release 18 | * Local file based dashboard storage 19 | * HTTP and HTTPS server support 20 | * Basic Authentication Support 21 | * Graphite, InfluxDB and OpenTSDB proxying 22 | * Single Linux/OSX binaries 23 | * _OAuth Authentication (coming soon)_ 24 | * _S3 and other Dashboard Storage (coming soon)_ 25 | 26 | ## Getting Started 27 | 28 | Gofana requires an existing [Graphite](http://graphite.wikidot.com/), [InfluxDB](http://influxdb.com/) 29 | or [OpenTSDB](http://opentsdb.net/) installation. 30 | 31 | Gofana handles dashboard storage using the local filesystem (in a `dashboards` directory by default). 32 | When using Graphite or OpenTSDB, you do not need a separate [elasticsearch](http://www.elasticsearch.org/) 33 | server for dashboard storage. Similarly, when using InfluxDB, dashboards will not be stored in InfluxDB. 34 | 35 | ### Linux 36 | ``` 37 | $ curl -sfL https://github.com/jwilder/gofana/releases/download/v0.0.6/gofana-linux-amd64-v0.0.6.tar.gz | tar xvzf - > gofana 38 | $ gofana -graphite-url http://127.0.0.1:8000 39 | ``` 40 | 41 | ### OSX 42 | ``` 43 | $ curl -sfL https://github.com/jwilder/gofana/releases/download/v0.0.6/gofana-darwin-amd64-v0.0.6.tar.gz | tar xvzf - > gofana 44 | $ gofana -graphite-url http://127.0.0.1:8000 45 | ``` 46 | 47 | ### Docker 48 | ``` 49 | $ docker run --name gofana -d -v /mnt/my/dashboard:/app/dashboards -p 80:8080 -p 443:8443 jwilder/gofana -graphite-url http://host:port 50 | ``` 51 | 52 | ## Basic Authentication 53 | 54 | To password protect your grafana server with HTTP Basic authentication, you can 55 | start gofana with the `-auth user:pw` option. 56 | 57 | ``` 58 | $ gofana -graphite-url http://127.0.0.1:8000 -auth user:pw 59 | ``` 60 | 61 | If you are using Basic authentication, it's a good idea to also use HTTPS. 62 | 63 | ## HTTPS Server 64 | 65 | To run gofana over HTTPS, you need a SSL cert and key. To create a self-signed key and certificate: 66 | 67 | ``` 68 | $ openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3650 -nodes 69 | ``` 70 | 71 | You should have a private `key.pem` and a public `cert.pem` in you current directory. 72 | 73 | Then start gofana with `-ssl-cert` and `-ssl-key`. 74 | 75 | ``` 76 | $ gofana -graphite-url http://127.0.0.1:8000 -ssl-cert cert.pem -ssl-key key.pem 77 | ``` 78 | 79 | ## Bind Address 80 | 81 | By default, gofana will listen on port `8080` for HTTP and `8443` for HTTPS. You can use a different port by passing `-http-addr` and `https-addr` respectively. 82 | 83 | ## Dashboard Storage 84 | 85 | By default, gofana will create a `dashboards` directory in the directory that gofana was started. You can change this to somewhere else with the `-db-dir` 86 | option. 87 | 88 | ``` 89 | $ gofana -db-dir /mnt/gofana/mydashboards 90 | ``` 91 | 92 | ## Updating Grafana 93 | 94 | Gofana embeds the latest release of Grafana. If you want to run an older or customized version of Grafana you can use the `-app-dir` option. _Note: this is feature is experimental._ 95 | 96 | ``` 97 | $ gofana -app-dir /mnt/grafana-1.8.1 98 | ``` 99 | 100 | If you want to bundle a new version of Grafana within gofana, see the [Development](#development) section. 101 | 102 | 103 | ## Why Gofana 104 | 105 | The [installation process](http://grafana.org/docs/) for getting a working grafana server is more complicated than expected for a client-side application that runs in the browser. I needed to expose our internal graphite metrics using Grafana dashboards over HTTPS with authentication, but ran into several issues with CORS headers, complicated configuration for nginx and graphite when proxying over SSL, extra dependencies such as running Elasticsearch for storing dashboards, etc... 106 | 107 | It really needed to be simpler and so gofana was created make it simpler. 108 | 109 | ## Known Issues 110 | 111 | * Dashboard filtering w/ tags and query language is not implemented yet. 112 | 113 | ## Development 114 | 115 | This project uses [glock](https://github.com/robfig/glock) for managing 3rd party dependencies. 116 | You'll need to install glock into your workspace before hacking on gofana. 117 | 118 | ``` 119 | $ git clone 120 | $ glock sync github.com/jwilder/gofana 121 | $ make 122 | ``` 123 | 124 | ## License 125 | 126 | MIT 127 | -------------------------------------------------------------------------------- /docs/graphs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jwilder/gofana/16d2db86c519e50f9f82100471f356981bbdd386/docs/graphs.png -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "encoding/json" 7 | "flag" 8 | "fmt" 9 | "github.com/go-martini/martini" 10 | "github.com/jwilder/gofana/grafana" 11 | "github.com/martini-contrib/auth" 12 | "github.com/martini-contrib/render" 13 | "github.com/martini-contrib/secure" 14 | "github.com/martini-contrib/sessions" 15 | "github.com/martini-contrib/staticbin" 16 | "github.com/stretchr/gomniauth" 17 | "github.com/stretchr/gomniauth/providers/google" 18 | "github.com/stretchr/objx" 19 | "github.com/stretchr/signature" 20 | 21 | "io/ioutil" 22 | "log" 23 | "net/http" 24 | "os" 25 | "strconv" 26 | "strings" 27 | "sync" 28 | "text/template" 29 | ) 30 | 31 | const ( 32 | defaultSessionSecret = "e40f5e3a62f25ef48eeb03440735831a" 33 | ) 34 | 35 | var ( 36 | db *DashboardRepository 37 | wg sync.WaitGroup 38 | basicAuth, authDomain string 39 | httpAddr, httpsAddr string 40 | sslCert, sslKey string 41 | appDir, dbDir string 42 | graphiteURL string 43 | influxDBURL string 44 | influxDBUser string 45 | influxDBPass string 46 | openTSDBUrl string 47 | buildVersion string 48 | hostAddr string 49 | googleClientID, googleClientSecret string 50 | sessionSecret string 51 | version bool 52 | ) 53 | 54 | func addCorsHeaders(w http.ResponseWriter) { 55 | w.Header().Add("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Content-Length") 56 | w.Header().Add("Access-Control-Allow-Methods", "OPTIONS, HEAD, GET, POST, PUT, DELETE") 57 | w.Header().Add("Access-Control-Allow-Origin", "*") 58 | } 59 | 60 | func loginRequired(s sessions.Session, c martini.Context, w http.ResponseWriter, r *http.Request) { 61 | if strings.HasSuffix(r.URL.Path, "/oauth2callback") || 62 | strings.HasSuffix(r.URL.Path, "/signin") || 63 | strings.HasSuffix(r.URL.Path, "/auth") { 64 | return 65 | } 66 | 67 | if s.Get("username") == nil { 68 | http.Redirect(w, r, "/signin", http.StatusFound) 69 | } 70 | } 71 | 72 | func authRedirect(params martini.Params, s sessions.Session, c martini.Context, w http.ResponseWriter, r *http.Request) { 73 | providerName := params["provider"] 74 | if providerName == "" { 75 | http.Error(w, "Unknown provider", http.StatusBadRequest) 76 | return 77 | } 78 | 79 | provider, err := gomniauth.Provider(providerName) 80 | if err != nil { 81 | w.WriteHeader(http.StatusInternalServerError) 82 | log.Printf("ERROR: %s", err) 83 | return 84 | } 85 | 86 | authUrl, err := provider.GetBeginAuthURL(nil, nil) 87 | if err != nil { 88 | w.WriteHeader(http.StatusInternalServerError) 89 | log.Printf("ERROR: %s", err) 90 | return 91 | } 92 | http.Redirect(w, r, authUrl, http.StatusFound) 93 | } 94 | 95 | func getSignin(s sessions.Session, w http.ResponseWriter, r *http.Request) { 96 | s.Delete("username") 97 | if !oauthEnabled() { 98 | http.Redirect(w, r, "/", http.StatusFound) 99 | return 100 | } 101 | 102 | body, err := Asset("templates/signin.html") 103 | if err != nil { 104 | w.WriteHeader(http.StatusInternalServerError) 105 | log.Printf("ERROR: %s", err) 106 | return 107 | } 108 | 109 | tmpl, err := template.New("config.js").Parse(string(body)) 110 | if err != nil { 111 | w.WriteHeader(http.StatusInternalServerError) 112 | log.Printf("ERROR: %s", err) 113 | return 114 | } 115 | 116 | err = tmpl.Execute(w, struct { 117 | Error string 118 | Google bool 119 | }{ 120 | Error: r.FormValue("error"), 121 | Google: googleOauthEnabled(), 122 | }) 123 | if err != nil { 124 | w.WriteHeader(http.StatusInternalServerError) 125 | log.Printf("ERROR: %s", err) 126 | return 127 | } 128 | } 129 | 130 | func oauth2callback(params martini.Params, s sessions.Session, c martini.Context, w http.ResponseWriter, r *http.Request) { 131 | providerName := params["provider"] 132 | if providerName == "" { 133 | http.Error(w, "Unknown provider", http.StatusBadRequest) 134 | return 135 | } 136 | 137 | provider, err := gomniauth.Provider(providerName) 138 | if err != nil { 139 | w.WriteHeader(http.StatusInternalServerError) 140 | log.Printf("ERROR: %s", err) 141 | return 142 | } 143 | 144 | omap, err := objx.FromURLQuery(r.URL.RawQuery) 145 | if err != nil { 146 | http.Error(w, err.Error(), http.StatusInternalServerError) 147 | return 148 | } 149 | 150 | creds, err := provider.CompleteAuth(omap) 151 | if err != nil { 152 | s.Delete("username") 153 | http.Redirect(w, r, "/signin?error=Access+Denied", http.StatusFound) 154 | log.Printf("ERROR: %s", err) 155 | return 156 | } 157 | 158 | user, err := provider.GetUser(creds) 159 | if err != nil { 160 | w.WriteHeader(http.StatusInternalServerError) 161 | log.Printf("ERROR: %s", err) 162 | return 163 | } 164 | 165 | if strings.HasSuffix(user.Email(), authDomain) { 166 | log.Printf("%s authenticated via %s", user.Email(), providerName) 167 | s.Set("username", user.Email()) 168 | } else { 169 | http.Redirect(w, r, "/signin", http.StatusFound) 170 | return 171 | } 172 | 173 | http.Redirect(w, r, "/", http.StatusFound) 174 | } 175 | 176 | func saveDashboard(w http.ResponseWriter, r *http.Request, params martini.Params) { 177 | body, err := ioutil.ReadAll(r.Body) 178 | if err != nil { 179 | w.WriteHeader(http.StatusInternalServerError) 180 | log.Printf("ERROR: %s", err) 181 | return 182 | } 183 | 184 | err = db.Save(params["id"], body) 185 | if err != nil { 186 | w.WriteHeader(http.StatusInternalServerError) 187 | log.Printf("ERROR: %s", err) 188 | return 189 | } 190 | 191 | w.Write([]byte("{}")) 192 | } 193 | 194 | func getDashboard(w http.ResponseWriter, r *http.Request, params martini.Params) { 195 | id := params["id"] 196 | if !db.Exists(id) { 197 | w.WriteHeader(http.StatusNotFound) 198 | return 199 | } 200 | 201 | w.Header().Add("Content-Type", "application/json") 202 | 203 | data, err := db.Get(id) 204 | if err != nil { 205 | w.WriteHeader(http.StatusInternalServerError) 206 | log.Printf("ERROR: %s", err) 207 | return 208 | } 209 | 210 | w.Header().Add("Content-Length", strconv.FormatInt(int64(len(string(data))), 10)) 211 | w.Write(data) 212 | } 213 | 214 | func deleteDashboard(w http.ResponseWriter, r *http.Request, params martini.Params) { 215 | id := params["id"] 216 | err := db.Delete(id) 217 | if err != nil { 218 | w.WriteHeader(http.StatusInternalServerError) 219 | log.Printf("ERROR: %s", err) 220 | return 221 | } 222 | } 223 | 224 | func searchDashboards(w http.ResponseWriter, r *http.Request) { 225 | err := r.ParseForm() 226 | if err != nil { 227 | w.WriteHeader(http.StatusBadRequest) 228 | log.Printf("ERROR: %s", err) 229 | return 230 | } 231 | 232 | dashboards, err := db.Search(r.Form.Get("query")) 233 | if err != nil { 234 | w.WriteHeader(http.StatusBadRequest) 235 | log.Printf("ERROR: %s", err) 236 | return 237 | } 238 | 239 | w.Header().Add("Content-Type", "application/json") 240 | 241 | body, err := json.Marshal(struct { 242 | Dashboards []*Dashboard `json:"dashboards"` 243 | }{Dashboards: dashboards}) 244 | if err != nil { 245 | w.WriteHeader(http.StatusInternalServerError) 246 | log.Printf("ERROR: %s", err) 247 | return 248 | 249 | } 250 | w.Write(body) 251 | } 252 | 253 | func gofanaDatasource(w http.ResponseWriter) { 254 | w.Header().Add("Content-Type", "application/json") 255 | body, err := Asset("templates/datasource.gofana.js") 256 | if err != nil { 257 | w.WriteHeader(http.StatusInternalServerError) 258 | log.Printf("ERROR: %s", err) 259 | return 260 | } 261 | 262 | w.Write(body) 263 | } 264 | 265 | func gofanaConfig(w http.ResponseWriter) { 266 | w.Header().Add("Content-Type", "application/json") 267 | body, err := Asset("templates/config.js") 268 | if err != nil { 269 | w.WriteHeader(http.StatusInternalServerError) 270 | log.Printf("ERROR: %s", err) 271 | return 272 | } 273 | 274 | tmpl, err := template.New("config.js").Parse(string(body)) 275 | if err != nil { 276 | w.WriteHeader(http.StatusInternalServerError) 277 | log.Printf("ERROR: %s", err) 278 | return 279 | } 280 | 281 | err = tmpl.Execute(w, struct { 282 | GraphiteURL string 283 | InfluxDBURL string 284 | InfluxDBUser string 285 | InfluxDBPass string 286 | OpenTSDBUrl string 287 | }{ 288 | GraphiteURL: graphiteURL, 289 | InfluxDBURL: influxDBURL, 290 | InfluxDBUser: influxDBUser, 291 | InfluxDBPass: influxDBPass, 292 | OpenTSDBUrl: openTSDBUrl, 293 | }) 294 | if err != nil { 295 | w.WriteHeader(http.StatusInternalServerError) 296 | log.Printf("ERROR: %s", err) 297 | return 298 | } 299 | } 300 | 301 | func copyHeader(source http.Header, dest *http.Header) { 302 | for n, v := range source { 303 | for _, vv := range v { 304 | dest.Add(n, vv) 305 | } 306 | } 307 | } 308 | 309 | func proxyOpenTSDB(w http.ResponseWriter, r *http.Request) { 310 | proxy(openTSDBUrl, w, r) 311 | } 312 | 313 | func proxyGraphite(w http.ResponseWriter, r *http.Request) { 314 | proxy(graphiteURL, w, r) 315 | } 316 | func proxyInfluxDB(w http.ResponseWriter, r *http.Request) { 317 | proxy(influxDBURL, w, r) 318 | } 319 | func proxy(target string, w http.ResponseWriter, r *http.Request) { 320 | 321 | stripped := r.RequestURI[strings.Index(r.RequestURI[1:], "/")+1:] 322 | uri := target + stripped 323 | 324 | body, err := ioutil.ReadAll(r.Body) 325 | if err != nil { 326 | w.WriteHeader(http.StatusInternalServerError) 327 | log.Printf("ERROR: %s", err) 328 | return 329 | } 330 | 331 | rr, err := http.NewRequest(r.Method, uri, bytes.NewBuffer(body)) 332 | if err != nil { 333 | w.WriteHeader(http.StatusInternalServerError) 334 | log.Printf("ERROR: %s", err) 335 | return 336 | } 337 | copyHeader(r.Header, &rr.Header) 338 | 339 | // Create a client and query the target 340 | var transport http.Transport 341 | resp, err := transport.RoundTrip(rr) 342 | if err != nil { 343 | w.WriteHeader(http.StatusInternalServerError) 344 | log.Printf("ERROR: %s", err) 345 | return 346 | } 347 | 348 | defer resp.Body.Close() 349 | body, err = ioutil.ReadAll(resp.Body) 350 | if err != nil { 351 | w.WriteHeader(http.StatusInternalServerError) 352 | log.Printf("ERROR: %s", err) 353 | return 354 | } 355 | 356 | dH := w.Header() 357 | copyHeader(resp.Header, &dH) 358 | 359 | w.Write(body) 360 | } 361 | 362 | func googleOauthEnabled() bool { 363 | return googleClientID != "" && googleClientSecret != "" 364 | } 365 | func oauthEnabled() bool { 366 | return googleOauthEnabled() 367 | } 368 | 369 | func main() { 370 | 371 | flag.StringVar(&appDir, "app-dir", "", "Path to grafana installation") 372 | flag.StringVar(&dbDir, "db-dir", "dashboards", "Path to dashboard storage dir") 373 | flag.StringVar(&authDomain, "auth-domain", "", "OAuth2 domain users must authenticated from (mydomain.com)") 374 | flag.StringVar(&basicAuth, "auth", "", "Basic auth username (user:pw)") 375 | flag.StringVar(&sessionSecret, "session-secret", defaultSessionSecret, "Session secret key") 376 | flag.StringVar(&httpAddr, "http-addr", ":8080", "HTTP Server bind address") 377 | flag.StringVar(&httpsAddr, "https-addr", ":8443", "HTTPS Server bind address") 378 | flag.StringVar(&graphiteURL, "graphite-url", "", "Graphite URL (http://host:port)") 379 | flag.StringVar(&influxDBURL, "influxdb-url", "", "InfluxDB URL (http://host:8086/db/mydb)") 380 | flag.StringVar(&influxDBUser, "influxdb-user", "", "InfluxDB username") 381 | flag.StringVar(&influxDBPass, "influxdb-pass", "", "InfluxDB password") 382 | flag.StringVar(&openTSDBUrl, "opentsdb-url", "", "OpenTSDB URL (http://host:4242)") 383 | flag.StringVar(&sslCert, "ssl-cert", "", "SSL cert (PEM formatted)") 384 | flag.StringVar(&sslKey, "ssl-key", "", "SSL key (PEM formatted)") 385 | flag.StringVar(&hostAddr, "host-addr", "http://localhost:8080", "Public server address (http://mydomain.com)") 386 | flag.StringVar(&googleClientID, "google-client-id", "", "Google Oauth2 Client ID") 387 | flag.StringVar(&googleClientSecret, "google-client-secret", "", "Google Oauth2 Client Sercret") 388 | 389 | flag.BoolVar(&version, "version", false, "show version") 390 | flag.Parse() 391 | 392 | if version { 393 | println(buildVersion) 394 | return 395 | } 396 | 397 | if sessionSecret == defaultSessionSecret { 398 | log.Printf("WARN: Session secret key is using the hard-coded default. Use -session-secret for a live deployment.\n") 399 | } 400 | 401 | if graphiteURL == "" && influxDBURL == "" && openTSDBUrl == "" { 402 | fmt.Printf("No graphite-url, influxdb-url or opentsdb-url specified.\nUse -graphite-url http://host:port or -influxdb-url http://host:8086/db/mydb or -opentsdb-url http://host:4242\n") 403 | return 404 | } 405 | 406 | log.Printf("Starting gofana %s", buildVersion) 407 | if _, err := os.Stat(dbDir); os.IsNotExist(err) { 408 | fmt.Printf("%s does not exist. Creating.\n", dbDir) 409 | err := os.Mkdir(dbDir, 0766) 410 | if err != nil { 411 | fmt.Printf("ERROR: %s\n", err) 412 | return 413 | } 414 | } 415 | 416 | db = &DashboardRepository{Dir: dbDir} 417 | err := db.Load() 418 | if err != nil { 419 | fmt.Printf("ERROR: %s\n", err) 420 | return 421 | } 422 | 423 | logger := log.New(os.Stderr, "", log.LstdFlags) 424 | r := martini.NewRouter() 425 | m := martini.New() 426 | m.Map(logger) 427 | m.Use(martini.Recovery()) 428 | m.MapTo(r, (*martini.Routes)(nil)) 429 | m.Action(r.Handle) 430 | 431 | if sslCert != "" && sslKey != "" { 432 | m.Use(secure.Secure(secure.Options{})) 433 | } 434 | 435 | b := make([]byte, 32) 436 | _, err = rand.Read(b) 437 | if err != nil { 438 | fmt.Printf("ERROR: %s\n", err) 439 | return 440 | } 441 | 442 | m.Use(sessions.Sessions("session", sessions.NewCookieStore([]byte(sessionSecret)))) 443 | if oauthEnabled() { 444 | 445 | if authDomain == "" { 446 | fmt.Println("ERROR: No -auth-domain specified. Cannot authenticate with OAuth2.\n") 447 | return 448 | } 449 | 450 | gomniauth.SetSecurityKey(signature.RandomKey(64)) 451 | providers := gomniauth.WithProviders() 452 | 453 | if googleOauthEnabled() { 454 | providers.Add(google.New(googleClientID, googleClientSecret, fmt.Sprintf("%s/google/oauth2callback", hostAddr))) 455 | } 456 | m.Use(loginRequired) 457 | } 458 | 459 | m.Use(addCorsHeaders) 460 | m.Use(render.Renderer()) 461 | 462 | if basicAuth != "" && strings.Contains(basicAuth, ":") { 463 | parts := strings.Split(basicAuth, ":") 464 | m.Use(auth.Basic(parts[0], parts[1])) 465 | } 466 | 467 | var static martini.Handler 468 | if appDir == "" { 469 | static = staticbin.Static("grafana-1.9.1", grafana.Asset) 470 | } else { 471 | static = martini.Static(appDir, martini.StaticOptions{Fallback: "/index.html", Exclude: "/api/v"}) 472 | } 473 | 474 | r.NotFound(static, http.NotFound) 475 | 476 | r.Get("/search", searchDashboards) 477 | r.Get("/dashboard/:id", getDashboard) 478 | r.Post("/dashboard/:id", saveDashboard) 479 | r.Delete("/dashboard/:id", deleteDashboard) 480 | r.Get("/plugins/datasource.gofana.js", gofanaDatasource) 481 | r.Get("/config.js", gofanaConfig) 482 | r.Get("/graphite/**", proxyGraphite) 483 | r.Post("/graphite/**", proxyGraphite) 484 | r.Get("/influxdb/**", proxyInfluxDB) 485 | r.Post("/influxdb/**", proxyInfluxDB) 486 | r.Get("/opentsdb/**", proxyOpenTSDB) 487 | r.Post("/opentsdb/**", proxyOpenTSDB) 488 | r.Get("/:provider/auth", authRedirect) 489 | r.Get("/:provider/oauth2callback", oauth2callback) 490 | r.Get("/signin", getSignin) 491 | 492 | // HTTP Listener 493 | wg.Add(1) 494 | go func() { 495 | defer wg.Done() 496 | log.Printf("HTTP listening on %s\n", httpAddr) 497 | if err := http.ListenAndServe(httpAddr, m); err != nil { 498 | log.Fatal(err) 499 | } 500 | }() 501 | 502 | // HTTPS Listener 503 | if sslCert != "" && sslKey != "" { 504 | wg.Add(1) 505 | go func() { 506 | defer wg.Done() 507 | log.Printf("HTTPS listening on %s", httpsAddr) 508 | if err := http.ListenAndServeTLS(httpsAddr, sslCert, sslKey, m); err != nil { 509 | log.Fatal(err) 510 | 511 | } 512 | }() 513 | } 514 | wg.Wait() 515 | } 516 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | type Dashboard struct { 16 | Id string `json:"id"` 17 | Title string `json:"title"` 18 | Tags []string `json:"tags"` 19 | Path string `json:"-"` 20 | } 21 | 22 | type DashboardRepository struct { 23 | Dir string 24 | dashboards []*Dashboard 25 | } 26 | 27 | func (d *Dashboard) String() string { 28 | return fmt.Sprintf("id=%s title=%s tags=%s path=%s", d.Id, d.Title, d.Tags, d.Path) 29 | } 30 | 31 | func (d *DashboardRepository) unmarshalDashboard(data io.Reader) (*Dashboard, error) { 32 | body, err := ioutil.ReadAll(data) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | var dash Dashboard 38 | err = json.Unmarshal(body, &dash) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return &dash, nil 43 | } 44 | 45 | func (d *DashboardRepository) Load() error { 46 | d.dashboards = []*Dashboard{} 47 | 48 | err := filepath.Walk(d.Dir, func(path string, info os.FileInfo, err error) error { 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if info.IsDir() || !strings.HasSuffix(path, ".json") { 54 | return nil 55 | } 56 | 57 | f, err := os.OpenFile(path, os.O_RDONLY, 0666) 58 | if err != nil { 59 | return err 60 | } 61 | defer f.Close() 62 | 63 | dash, err := d.unmarshalDashboard(f) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | dash.Path = path 69 | d.dashboards = append(d.dashboards, dash) 70 | return nil 71 | }) 72 | if err != nil { 73 | return err 74 | } 75 | log.Printf("Loaded %d dashboards\n", len(d.dashboards)) 76 | return nil 77 | } 78 | 79 | func (d *DashboardRepository) Search(query string) ([]*Dashboard, error) { 80 | return d.dashboards, nil 81 | } 82 | 83 | func (d *DashboardRepository) Exists(id string) bool { 84 | path := filepath.Join(d.Dir, id+".json") 85 | if _, err := os.Stat(path); os.IsNotExist(err) { 86 | return false 87 | } 88 | return true 89 | } 90 | 91 | func (d *DashboardRepository) Delete(id string) error { 92 | dashboards := []*Dashboard{} 93 | 94 | for _, d := range d.dashboards { 95 | if d.Id == id { 96 | err := os.Remove(d.Path) 97 | if err != nil { 98 | return err 99 | } 100 | log.Printf("Deleted dashboard %s", id) 101 | } else { 102 | dashboards = append(dashboards, d) 103 | } 104 | } 105 | d.dashboards = dashboards 106 | return nil 107 | } 108 | 109 | func (d *DashboardRepository) Get(id string) ([]byte, error) { 110 | path := filepath.Join(d.Dir, id+".json") 111 | 112 | f, err := os.OpenFile(path, os.O_RDONLY, 0666) 113 | if err != nil { 114 | return nil, err 115 | } 116 | defer f.Close() 117 | 118 | return ioutil.ReadAll(f) 119 | } 120 | 121 | func (d *DashboardRepository) Save(id string, data []byte) error { 122 | path := filepath.Join(d.Dir, id+".json.tmp") 123 | f, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, 0666) 124 | if err != nil { 125 | return err 126 | } 127 | defer f.Close() 128 | 129 | n, err := f.Write(data) 130 | if err != nil { 131 | return err 132 | } 133 | 134 | if n != len(data) { 135 | return fmt.Errorf("wrote %d, expected %d", n, len(data)) 136 | } 137 | 138 | err = f.Sync() 139 | if err != nil { 140 | return err 141 | } 142 | 143 | newPath := filepath.Join(d.Dir, id+".json") 144 | err = os.Rename(path, newPath) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | dash, err := d.unmarshalDashboard(bytes.NewBuffer(data)) 150 | if err != nil { 151 | return err 152 | } 153 | 154 | dash.Path = newPath 155 | d.update(dash) 156 | log.Printf("Saved dashboard %s", id) 157 | 158 | return nil 159 | } 160 | 161 | func (d *DashboardRepository) update(dash *Dashboard) { 162 | for _, d := range d.dashboards { 163 | if d.Id == dash.Id { 164 | d.Title = dash.Title 165 | d.Tags = dash.Tags 166 | return 167 | } 168 | } 169 | d.dashboards = append(d.dashboards, dash) 170 | } 171 | -------------------------------------------------------------------------------- /templates.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "fmt" 7 | "io" 8 | "strings" 9 | "os" 10 | "time" 11 | "io/ioutil" 12 | "path" 13 | "path/filepath" 14 | ) 15 | 16 | func bindata_read(data []byte, name string) ([]byte, error) { 17 | gz, err := gzip.NewReader(bytes.NewBuffer(data)) 18 | if err != nil { 19 | return nil, fmt.Errorf("Read %q: %v", name, err) 20 | } 21 | 22 | var buf bytes.Buffer 23 | _, err = io.Copy(&buf, gz) 24 | gz.Close() 25 | 26 | if err != nil { 27 | return nil, fmt.Errorf("Read %q: %v", name, err) 28 | } 29 | 30 | return buf.Bytes(), nil 31 | } 32 | 33 | type asset struct { 34 | bytes []byte 35 | info os.FileInfo 36 | } 37 | 38 | type bindata_file_info struct { 39 | name string 40 | size int64 41 | mode os.FileMode 42 | modTime time.Time 43 | } 44 | 45 | func (fi bindata_file_info) Name() string { 46 | return fi.name 47 | } 48 | func (fi bindata_file_info) Size() int64 { 49 | return fi.size 50 | } 51 | func (fi bindata_file_info) Mode() os.FileMode { 52 | return fi.mode 53 | } 54 | func (fi bindata_file_info) ModTime() time.Time { 55 | return fi.modTime 56 | } 57 | func (fi bindata_file_info) IsDir() bool { 58 | return false 59 | } 60 | func (fi bindata_file_info) Sys() interface{} { 61 | return nil 62 | } 63 | 64 | var _templates_config_js = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xac\x56\xdb\x8e\xdb\x36\x10\x7d\xf7\x57\x0c\xfc\xe2\x4d\xe0\xac\x92\x57\xa5\x29\xda\xec\x16\x41\x80\x02\x2d\x9a\xed\x43\xb1\x08\x0c\x5a\xa4\x2c\xb6\x14\xa9\xf2\x52\xdb\x58\xf8\xdf\x7b\x48\x51\x17\x5f\xd0\x02\x45\x0d\xec\xca\x9e\xcb\x99\xe1\x99\xe1\x8c\x8a\x82\x3e\x7c\xa0\x07\xa3\x6b\xb9\x0b\x96\x79\x69\xf4\x02\xb2\x2a\x09\xee\x7f\x77\x24\x1d\xed\x1b\x61\x05\x1d\x4d\xa0\xbd\x54\x8a\x6a\xa9\x39\xf9\x46\xc0\x08\xe2\x4f\x96\xd5\x4c\xb3\xec\x91\x21\xee\xe9\xa9\x81\x63\x2d\x55\xb4\xd2\x9e\x49\xed\xa8\x63\x96\xb5\xc2\x0b\x0b\x67\xe6\x63\x94\x36\x38\x4f\x5b\x41\x4e\xc4\x47\x3d\x87\x83\xb7\x0d\x9a\x20\x4b\xa1\x6a\x69\x61\xea\x65\x2b\xee\x17\x0b\x2e\x90\x82\xb8\x7b\x5e\xc1\xcf\x4b\xbd\x73\xab\xaf\x6b\xaa\x83\xae\x62\xe8\xbb\x2f\x59\xf8\x8a\x5e\x16\x44\xcb\xe0\x80\xef\xad\xac\xfc\xf2\xfd\x02\x02\x2b\x7c\xb0\x9a\xb4\xd8\xd3\x60\x79\xf7\x12\x15\xf1\x53\xbc\xa6\x47\xe6\x19\x39\x13\x6c\x25\x5c\x96\xbe\x06\x45\xff\xed\x33\x02\x44\xd4\x0c\x4a\x0c\xc7\x44\x56\xe0\xd0\x50\x2d\x7c\xd5\x10\x58\x41\x82\x6e\x4d\x4c\x6b\xe3\x13\x83\xe9\x07\x07\x33\xf6\x2f\x41\xcc\x11\x67\xae\xd9\x1a\x66\x21\xf2\xc6\xb2\x9d\x18\xa1\xe9\x0d\xfd\x86\xd2\x54\x4c\x53\xc3\x60\xdc\x06\xe5\x65\x07\xe2\x4d\x9d\xa8\x73\x20\x9d\xfc\xb1\x03\x73\x33\x97\x5d\xcf\xf3\xe3\xc7\x92\xbc\x0d\x22\xca\x5b\x66\xff\x40\xbd\x7d\x22\x3d\xd2\x16\x9f\xff\x18\x17\x85\x60\x88\x36\x41\x0c\x20\x31\x2e\x1f\x8f\x1c\xf3\x4f\x92\xde\x3c\x1f\x37\x93\x4c\x77\xb2\x4e\xbd\x75\x96\xfc\xab\x79\x98\x2d\x73\x30\x67\x01\x18\xda\xcb\x2a\xf1\x53\xa6\x0c\x83\x55\xe4\x8e\xe8\xaf\x03\x35\xde\x77\x65\x51\x40\x6a\x35\x4e\x5c\x76\xcc\xb9\xbd\xb1\xfc\x3b\x6e\x5a\xb4\x5f\xd9\x19\xeb\x07\xd0\xa2\xaf\xf7\x94\xa1\x2b\x53\xb3\xc4\xcf\xcb\x0b\x21\xa1\x7b\xf4\x61\xd7\x48\x2f\x7e\xfd\xe5\x47\x3a\x9d\xb2\x6e\x97\x85\x93\x35\x25\x66\x4b\x5a\x0d\xaa\xd5\x7a\xd4\x20\x39\x28\x8a\x2b\xcd\x69\x3d\x85\x12\x9a\x8f\xe8\x39\xf2\x67\x5d\xab\x70\x78\xfc\x78\x16\x59\x26\x21\xdf\xde\x88\x3c\xa8\xae\x23\xdf\xd0\x5c\xc6\x00\x5b\x53\x10\xa2\x91\x3d\x5a\xc1\xf2\xd2\xec\x1c\x67\x9e\xfa\x15\xf0\xcf\x60\x7f\x0e\x3c\x54\xe3\x02\x38\x9b\x5d\x01\x4f\xae\xff\x42\xd6\x4f\x9d\xd0\x4f\x5f\x90\x21\x1a\x61\xd4\x19\x08\xbd\xbb\x49\xd6\xa0\xba\x26\xeb\x4a\x73\x11\x79\x82\xaf\x30\xb5\x4c\x7b\x03\xfc\x21\x29\xa6\xab\x3e\x0b\x92\x49\xdd\x99\x78\xe9\x66\xf2\x8b\x6b\x78\x11\x1b\x8f\x69\x30\x7d\x52\x66\xcb\xd4\xf9\x9c\xc5\x51\xd3\xb0\xf8\xdf\x06\x55\x31\x06\x2c\xc8\x75\xa2\x92\xf5\x31\x5d\x5d\x25\xdb\x3c\x18\x66\x03\x41\x30\x8b\xe1\x65\x85\xc3\xa5\x1d\x52\xe8\x85\x73\x72\x5a\x76\xd8\x64\x9b\x92\xde\xbd\x7d\x3b\x1d\x71\x0a\x35\x4c\x86\xc6\xb4\x62\x8a\x90\xf5\x59\xb9\xb1\x26\xc4\xab\xb7\x2a\x46\x83\x22\x2e\x98\x22\xeb\xb1\xac\x8c\x5e\xcd\x51\xe3\x5a\x89\x33\x96\x29\x8c\x0a\x7c\xe1\xd2\xb1\x2d\x06\x63\xd0\x0e\xb3\x86\x53\xd5\x30\xbd\xc3\x40\xde\x33\xab\xb1\x07\xb2\x63\xd6\x6e\xb2\x76\x93\xb5\x43\x81\x2e\xe0\x67\x63\x2d\x6e\x27\xd7\xb1\x69\x69\x75\x8a\x1d\x95\xc4\xde\xaa\x05\xc3\xd2\x11\x93\xeb\x0f\x07\xd6\x62\xca\x95\xb4\x7c\xd7\x2e\xd7\xf8\xdf\x2c\xb3\x72\xf0\xd9\x0c\x68\xd9\x66\x16\xf7\x73\x3f\x2f\xf7\x4c\xa7\xe3\x0d\x65\x1a\xae\xd8\xb0\x49\x71\x0a\xa4\xbd\x06\xa2\x60\x71\x07\x66\x33\x19\x57\xad\x32\xfb\x09\xef\x29\xa6\x1a\x6c\x67\x5c\xde\x19\xd2\x4d\x60\xf8\x8e\x95\x84\xb3\x56\xc1\x4a\x7f\x5c\xd3\x36\xf4\x51\xbd\xe9\x30\xc1\xdb\xb4\xca\x2c\x56\xbd\x35\x2d\xb1\xaa\x92\x1c\xd7\x88\x29\x75\xec\xd9\x45\x06\x53\x3d\x87\x1e\x61\xbc\xc5\x40\x9e\xb5\xc8\x6c\x3a\xac\x6e\xb5\xc7\x43\x2a\x05\xde\x3e\x34\x37\x7b\xf0\xec\x51\xc4\xce\xe2\x2d\xe0\xd0\xc7\x5d\x0d\x6f\x0d\x6f\xe8\x9b\xa9\x3f\x93\xdd\xb7\x03\x60\xef\xbc\x49\xc2\x4d\xef\x5c\xce\x1d\xcf\x3a\xe7\x7b\xce\x23\xc7\x96\xcc\x5e\xe7\xfb\x8e\x24\xb5\x50\x6e\x2c\x53\xc0\xd9\xdc\xfc\x10\x70\x4b\xc5\x06\x85\xbd\xf6\xdc\x83\xf2\xcf\x92\x9e\xbf\xae\xe7\x4e\x56\xfc\x19\xa4\x15\x78\xd5\x6a\x0d\x0f\x0a\xfd\x18\x5d\x7b\x7c\xb4\x92\xe2\xf9\x85\x89\x5c\x63\x82\x8a\xe5\x25\x65\x18\x17\x7c\x0e\x12\x5b\x4e\xf4\x4d\x35\xe4\x3b\xdb\x6e\xa3\x25\x17\x98\x72\xa8\x50\x25\xe3\xca\x7b\x5e\x4e\x36\xf7\xfd\x68\x5a\x8e\xb9\x9d\x7a\x3a\x4e\xaf\xde\x2f\xe2\xdf\xdf\x01\x00\x00\xff\xff\x19\x51\xc4\xaa\x28\x0a\x00\x00") 65 | 66 | func templates_config_js_bytes() ([]byte, error) { 67 | return bindata_read( 68 | _templates_config_js, 69 | "templates/config.js", 70 | ) 71 | } 72 | 73 | func templates_config_js() (*asset, error) { 74 | bytes, err := templates_config_js_bytes() 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | info := bindata_file_info{name: "templates/config.js", size: 2600, mode: os.FileMode(420), modTime: time.Unix(1422915677, 0)} 80 | a := &asset{bytes: bytes, info: info} 81 | return a, nil 82 | } 83 | 84 | var _templates_datasource_gofana_js = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\xc4\x56\x4d\x6b\xdb\x4c\x10\xbe\xfb\x57\x0c\x22\x20\x89\x08\xf9\x1e\x13\x5e\xde\x26\x14\x7a\xe8\xa1\x1f\x3e\x85\x50\xd6\xda\x91\xbd\xa9\xb4\xab\xec\x87\xc1\x84\xfc\xf7\xce\xae\xac\xcf\x24\xe0\x36\x4d\xaa\x83\x91\x76\x9f\x99\x79\x66\x9e\x99\xf5\x72\x2c\x85\xc4\xe4\x66\x01\x10\x33\xb9\x75\x15\xd3\x71\xe6\x3f\x2a\xc5\x99\xd9\xb5\xef\x3f\x37\xb2\x7d\xa9\x55\x8d\xd2\xc6\x8b\xdb\x6c\x51\x3a\x59\x58\xa1\x24\x24\x47\xb3\x0c\x7e\x64\x40\xc8\x14\x1e\x3c\xd4\x19\x04\x63\xb5\x28\x6c\xbc\x5a\xd0\xc2\x9e\x69\xa8\x15\x77\x15\xc2\x25\x1c\x4d\xf2\x76\x21\x89\xb7\x9a\x95\x4c\xb2\xdc\xa0\xde\x8b\x02\x4d\x9c\x06\x9b\x76\x3b\x2f\x59\x61\x95\x3e\x24\xf1\x95\x33\x56\xd5\xd7\xcc\x32\xa3\x9c\x2e\x30\xce\xa0\xa3\x91\x9c\xdd\x67\x70\xb6\xb3\xb6\xf1\xf1\xc9\x16\x60\xb9\x04\xbb\x43\xe0\x3d\x1c\xd4\xe6\x0e\x0b\x0b\x0d\x33\x06\x39\x58\x05\x85\x92\xc4\xd1\x79\xf7\x9d\x89\x30\xc1\xca\xb0\x9a\x4c\x43\x71\x38\x08\xe9\x91\xa5\xd8\xe6\x77\x26\xe0\xfa\xe4\xe7\x8c\x92\x21\x5a\x5b\x07\xff\xd8\x9d\x30\xb9\xf4\x0e\x2f\x47\x6c\xc2\xca\x6a\x0c\x31\xae\x69\x94\xb6\x9f\xd1\x97\xcd\x10\xb8\x64\x95\x79\x16\xf2\xbf\x94\xca\x32\xcf\xc0\xc3\x28\x83\x29\xca\xe9\x6a\x1a\x8a\x16\x26\x80\x63\xbd\xaf\x3f\x4c\x61\xfd\x72\x07\xf6\xf5\x51\xa4\x40\xa5\xb6\x89\x37\x4c\xdb\x8d\xc7\x55\x5b\xe1\x79\xf6\x79\xa3\x95\x55\xf6\xd0\x60\xce\x7a\x86\x5f\x1c\xea\x83\x4f\xa6\x53\x6a\xd8\xca\x40\x53\x27\xe0\x5a\x36\x4c\x93\x22\x43\xc5\xa6\x91\xa3\x99\xb3\x28\xed\x61\x1a\xad\xd3\x12\x6e\x6e\x4f\x26\x76\x3f\xa7\xa3\x9a\x50\xc7\x21\xf8\x38\x74\xb7\xd9\x15\xe4\xf7\xe3\x71\xac\xd0\xe2\x35\xcd\xd2\x46\x31\xcd\xc7\x91\x05\x7f\x3e\x68\x34\xb7\x89\xce\x09\x7a\x04\xfa\x39\xa2\xbe\x44\xad\xd1\x3b\x3b\xbb\xcf\xc3\x57\x92\x2e\xfa\xa2\x84\x31\x38\x06\x4e\xe2\x25\xef\xfc\x2c\x63\xef\x27\xef\x71\x00\xc6\x15\x34\x6d\x26\xe9\x29\xf9\x66\xc8\x68\x6e\x99\x75\x26\x83\x1d\x32\x8e\x9a\x5e\xda\xee\x1f\xeb\xe3\x9f\x8e\x46\xae\x91\xb8\xef\xd1\x27\xb4\x1a\x21\x1e\x27\xb1\x08\xaa\xf4\xeb\x23\xf9\x09\x4e\xa2\xb5\x64\x1b\x3a\x49\x68\x84\xdb\x34\xdb\x12\x4d\x83\xaf\x16\x53\xd1\x7a\x27\x24\x4e\x2d\xba\xc1\x3a\x41\x42\x83\x4c\x17\xbb\x5e\x0e\x33\xd6\x30\xb4\xd3\x37\x1a\x58\x39\x21\xfd\xb2\x4a\x53\x91\xb6\x68\x49\xa1\x36\xc0\x7f\xc1\xd7\x65\x7c\x3e\xf6\xf9\x86\x6a\x79\xeb\xf7\xd7\xab\xcd\x35\x7a\xa2\xd5\xf0\xf5\x5a\xb9\xa8\xa6\x2f\x8c\x5b\x46\xa7\xfb\x77\xac\x9b\x3f\x57\xea\xfd\x66\xe9\xdf\xa8\x43\x59\x02\x9f\x9e\x3b\x2f\x0c\xd5\xdf\x50\xca\xb0\xfd\xf3\x27\x63\x4f\x61\xae\x94\xf0\x30\x94\x85\xe2\xb8\xfe\xfa\xe9\x4a\xd5\x8d\x92\x74\x23\x49\xe8\xde\x91\x9b\xca\x6d\x45\x79\xf8\xa8\xf4\x5a\x57\x83\x8b\xdc\x0a\x5b\x61\x3a\xca\x63\xd8\x0a\xee\x04\x1f\x25\x75\x6a\x3b\x34\xca\x3c\xed\x87\x6c\x70\xfd\x96\xad\xf1\x00\x21\xa5\x0b\x98\xe5\x98\x01\xfd\xcf\x5f\x40\x34\x62\xc5\x37\xcb\x08\xce\x7d\xd9\x1e\xd3\xf7\xec\xa5\x70\xbb\xf2\xf2\x9e\xde\x4d\x27\x35\xd3\x11\x34\xef\xa9\xb0\x1b\xdc\xd1\xcf\xaf\x00\x00\x00\xff\xff\x3f\x98\xe7\x2b\xd3\x0a\x00\x00") 85 | 86 | func templates_datasource_gofana_js_bytes() ([]byte, error) { 87 | return bindata_read( 88 | _templates_datasource_gofana_js, 89 | "templates/datasource.gofana.js", 90 | ) 91 | } 92 | 93 | func templates_datasource_gofana_js() (*asset, error) { 94 | bytes, err := templates_datasource_gofana_js_bytes() 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | info := bindata_file_info{name: "templates/datasource.gofana.js", size: 2771, mode: os.FileMode(420), modTime: time.Unix(1421957735, 0)} 100 | a := &asset{bytes: bytes, info: info} 101 | return a, nil 102 | } 103 | 104 | var _templates_signin_html = []byte("\x1f\x8b\x08\x00\x00\x09\x6e\x88\x00\xff\x5c\x92\xcb\xce\xdb\x20\x10\x85\xf7\x79\x8a\x11\x52\x97\x36\xf9\xb3\x74\x6c\xef\xaa\xaa\xeb\x3c\x01\x81\x31\x46\xe2\x62\xc1\x24\x69\x6a\xe5\xdd\x0b\xd8\x6a\xad\xb2\xf1\xe5\x7c\x33\x67\xe6\x88\x7e\x26\x67\xc7\x53\x3f\xa3\x50\xf9\x91\xe8\x6d\x71\x3c\x41\x3e\x45\x80\xb5\xbe\x96\x33\x05\x4f\xcd\x24\x9c\xb1\xef\x0e\x92\xf0\xa9\x49\x18\xcd\x74\xad\xc0\x67\xab\xf8\xfa\x9f\x4f\xe6\x37\x76\xe0\x50\x99\x87\x3b\x92\xf7\xa0\xde\x07\x96\xf0\x17\x35\xc2\x1a\xed\x3b\x90\xe8\x09\xe3\xf5\xaf\xe6\x44\xd4\xc6\x37\x14\x96\x0e\x2e\xe7\x6f\xc7\x2e\x2d\xc6\x18\x22\xac\x20\x83\x0d\xb1\x83\x88\xea\x7a\xf4\xbd\x1b\x9d\xc9\x9e\xef\x4b\xf5\x7c\x5f\xb2\xb8\x8f\xa7\xbc\xf3\xd7\xb6\xe9\x0d\x2d\x4a\x02\x01\x4b\x0c\x4f\xa3\x30\x76\x85\xcd\xe2\xba\x82\x99\xa0\xfd\x5e\x6d\x3e\xb9\xd5\x02\xd2\x8a\x94\x06\x56\x9d\x59\x25\x0e\x32\x5f\xea\x1f\xf4\xaa\x7c\xee\xd5\x3f\x42\xd0\x16\xab\xae\xcc\x73\x73\xec\x05\xcc\x11\xa7\x81\x71\x5d\x55\x2e\x1e\x34\xb3\xb1\x37\x4e\xc3\xcb\x28\x9a\x07\x76\x39\x9f\x19\xa4\x28\x07\x36\x13\x2d\xa9\xe3\x5c\xe1\x13\x6d\x58\x30\xa6\x76\xab\x6a\x65\x70\x5c\x48\x19\x1e\x9e\x12\x37\x4e\x68\x4c\x3c\xe5\x14\x9b\x1c\xd8\xcb\xd0\xdc\xec\xdc\xe2\x35\x03\x61\x69\x60\xb7\xa2\xfe\xf4\x50\x54\xd8\x26\xcb\xb6\x5c\x94\x74\xea\x74\xff\xc6\xef\xf9\x96\x53\x8e\xa2\x5c\x91\x3f\x01\x00\x00\xff\xff\x1c\x7d\x03\x12\x29\x02\x00\x00") 105 | 106 | func templates_signin_html_bytes() ([]byte, error) { 107 | return bindata_read( 108 | _templates_signin_html, 109 | "templates/signin.html", 110 | ) 111 | } 112 | 113 | func templates_signin_html() (*asset, error) { 114 | bytes, err := templates_signin_html_bytes() 115 | if err != nil { 116 | return nil, err 117 | } 118 | 119 | info := bindata_file_info{name: "templates/signin.html", size: 553, mode: os.FileMode(420), modTime: time.Unix(1422661748, 0)} 120 | a := &asset{bytes: bytes, info: info} 121 | return a, nil 122 | } 123 | 124 | // Asset loads and returns the asset for the given name. 125 | // It returns an error if the asset could not be found or 126 | // could not be loaded. 127 | func Asset(name string) ([]byte, error) { 128 | cannonicalName := strings.Replace(name, "\\", "/", -1) 129 | if f, ok := _bindata[cannonicalName]; ok { 130 | a, err := f() 131 | if err != nil { 132 | return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) 133 | } 134 | return a.bytes, nil 135 | } 136 | return nil, fmt.Errorf("Asset %s not found", name) 137 | } 138 | 139 | // AssetInfo loads and returns the asset info for the given name. 140 | // It returns an error if the asset could not be found or 141 | // could not be loaded. 142 | func AssetInfo(name string) (os.FileInfo, error) { 143 | cannonicalName := strings.Replace(name, "\\", "/", -1) 144 | if f, ok := _bindata[cannonicalName]; ok { 145 | a, err := f() 146 | if err != nil { 147 | return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) 148 | } 149 | return a.info, nil 150 | } 151 | return nil, fmt.Errorf("AssetInfo %s not found", name) 152 | } 153 | 154 | // AssetNames returns the names of the assets. 155 | func AssetNames() []string { 156 | names := make([]string, 0, len(_bindata)) 157 | for name := range _bindata { 158 | names = append(names, name) 159 | } 160 | return names 161 | } 162 | 163 | // _bindata is a table, holding each asset generator, mapped to its name. 164 | var _bindata = map[string]func() (*asset, error){ 165 | "templates/config.js": templates_config_js, 166 | "templates/datasource.gofana.js": templates_datasource_gofana_js, 167 | "templates/signin.html": templates_signin_html, 168 | } 169 | 170 | // AssetDir returns the file names below a certain 171 | // directory embedded in the file by go-bindata. 172 | // For example if you run go-bindata on data/... and data contains the 173 | // following hierarchy: 174 | // data/ 175 | // foo.txt 176 | // img/ 177 | // a.png 178 | // b.png 179 | // then AssetDir("data") would return []string{"foo.txt", "img"} 180 | // AssetDir("data/img") would return []string{"a.png", "b.png"} 181 | // AssetDir("foo.txt") and AssetDir("notexist") would return an error 182 | // AssetDir("") will return []string{"data"}. 183 | func AssetDir(name string) ([]string, error) { 184 | node := _bintree 185 | if len(name) != 0 { 186 | cannonicalName := strings.Replace(name, "\\", "/", -1) 187 | pathList := strings.Split(cannonicalName, "/") 188 | for _, p := range pathList { 189 | node = node.Children[p] 190 | if node == nil { 191 | return nil, fmt.Errorf("Asset %s not found", name) 192 | } 193 | } 194 | } 195 | if node.Func != nil { 196 | return nil, fmt.Errorf("Asset %s not found", name) 197 | } 198 | rv := make([]string, 0, len(node.Children)) 199 | for name := range node.Children { 200 | rv = append(rv, name) 201 | } 202 | return rv, nil 203 | } 204 | 205 | type _bintree_t struct { 206 | Func func() (*asset, error) 207 | Children map[string]*_bintree_t 208 | } 209 | var _bintree = &_bintree_t{nil, map[string]*_bintree_t{ 210 | "templates": &_bintree_t{nil, map[string]*_bintree_t{ 211 | "config.js": &_bintree_t{templates_config_js, map[string]*_bintree_t{ 212 | }}, 213 | "datasource.gofana.js": &_bintree_t{templates_datasource_gofana_js, map[string]*_bintree_t{ 214 | }}, 215 | "signin.html": &_bintree_t{templates_signin_html, map[string]*_bintree_t{ 216 | }}, 217 | }}, 218 | }} 219 | 220 | // Restore an asset under the given directory 221 | func RestoreAsset(dir, name string) error { 222 | data, err := Asset(name) 223 | if err != nil { 224 | return err 225 | } 226 | info, err := AssetInfo(name) 227 | if err != nil { 228 | return err 229 | } 230 | err = os.MkdirAll(_filePath(dir, path.Dir(name)), os.FileMode(0755)) 231 | if err != nil { 232 | return err 233 | } 234 | err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) 235 | if err != nil { 236 | return err 237 | } 238 | err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) 239 | if err != nil { 240 | return err 241 | } 242 | return nil 243 | } 244 | 245 | // Restore assets under the given directory recursively 246 | func RestoreAssets(dir, name string) error { 247 | children, err := AssetDir(name) 248 | if err != nil { // File 249 | return RestoreAsset(dir, name) 250 | } else { // Dir 251 | for _, child := range children { 252 | err = RestoreAssets(dir, path.Join(name, child)) 253 | if err != nil { 254 | return err 255 | } 256 | } 257 | } 258 | return nil 259 | } 260 | 261 | func _filePath(dir, name string) string { 262 | cannonicalName := strings.Replace(name, "\\", "/", -1) 263 | return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) 264 | } 265 | 266 | -------------------------------------------------------------------------------- /templates/config.js: -------------------------------------------------------------------------------- 1 | // == Configuration 2 | // config.js is where you will find the core Grafana configuration. This file contains parameter that 3 | // must be set before Grafana is run for the first time. 4 | 5 | define(['settings'], function(Settings) { 6 | "use strict"; 7 | 8 | return new Settings({ 9 | 10 | /* Data sources 11 | * ======================================================== 12 | * Datasources are used to fetch metrics, annotations, and serve as dashboard storage 13 | * - You can have multiple of the same type. 14 | * - grafanaDB: true marks it for use for dashboard storage 15 | * - default: true marks the datasource as the default metric source (if you have multiple) 16 | * - basic authentication: use url syntax http://username:password@domain:port 17 | */ 18 | 19 | datasources: { 20 | {{ if .GraphiteURL }} 21 | graphite: { 22 | type: 'graphite', 23 | url: '/graphite', 24 | }, 25 | {{ end}} 26 | {{ if .InfluxDBURL }} 27 | influxdb: { 28 | type: 'influxdb', 29 | url: '/influxdb', 30 | {{ if .InfluxDBUser }} 31 | username: '{{ .InfluxDBUser }}', 32 | {{ end}} 33 | {{ if .InfluxDBPass }} 34 | password: '{{ .InfluxDBPass }}', 35 | {{ end }} 36 | }, 37 | {{ end}} 38 | {{ if .OpenTSDBUrl }} 39 | opentsdb: { 40 | type: 'opentsdb', 41 | url: '/opentsdb', 42 | }, 43 | {{ end }} 44 | custom: { 45 | type: 'CustomDatasource', 46 | name: 'gofana', 47 | grafanaDB: true, 48 | }, 49 | }, 50 | 51 | /* Global configuration options 52 | * ======================================================== 53 | */ 54 | 55 | // specify the limit for dashboard search results 56 | search: { 57 | max_results: 100 58 | }, 59 | 60 | // default home dashboard 61 | default_route: '/dashboard/file/default.json', 62 | 63 | // set to false to disable unsaved changes warning 64 | unsaved_changes_warning: true, 65 | 66 | // set the default timespan for the playlist feature 67 | // Example: "1m", "1h" 68 | playlist_timespan: "1m", 69 | 70 | // If you want to specify password before saving, please specify it below 71 | // The purpose of this password is not security, but to stop some users from accidentally changing dashboards 72 | admin: { 73 | password: '' 74 | }, 75 | 76 | // Change window title prefix from 'Grafana - ' 77 | window_title_prefix: 'Grafana - ', 78 | 79 | // Add your own custom panels 80 | plugins: { 81 | // list of plugin panels 82 | panels: [], 83 | // requirejs modules in plugins folder that should be loaded 84 | // for example custom datasources 85 | dependencies: ["datasource.gofana"], 86 | } 87 | 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /templates/datasource.gofana.js: -------------------------------------------------------------------------------- 1 | define([ 2 | 'angular', 3 | 'lodash', 4 | 'kbn', 5 | 'moment' 6 | ], 7 | function (angular, _, kbn) { 8 | 'use strict'; 9 | 10 | var module = angular.module('grafana.services'); 11 | 12 | module.factory('CustomDatasource', function($q, $http) { 13 | 14 | // the datasource object passed to constructor 15 | // is the same defined in config.js 16 | function CustomDatasource(datasource) { 17 | this.name = datasource.name; 18 | this.supportMetrics = false; 19 | this.supportAnnotations = true; 20 | this.url = datasource.url; 21 | this.grafanaDB = datasource.grafanaDB; 22 | console.log(this); 23 | }; 24 | 25 | CustomDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) { 26 | console.log("annotationQuery") 27 | return []; 28 | }; 29 | 30 | CustomDatasource.prototype.query = function(options) { 31 | console.log(options); 32 | return []; 33 | }; 34 | 35 | CustomDatasource.prototype.deleteDashboard = function(id) { 36 | console.log("deleteDashboard "+id) 37 | var deferred = $q.defer() 38 | 39 | $http.delete('/dashboard/'+id). 40 | success(function(data, status, headers, config) { 41 | deferred.resolve(id); 42 | }). 43 | error(function(data, status, headers, config) { 44 | deferred.reject("Unable to delete "+id); 45 | }); 46 | 47 | return deferred.promise; 48 | }; 49 | 50 | CustomDatasource.prototype.searchDashboards = function(queryString) { 51 | var deferred = $q.defer() 52 | $http.get('/search?query='+queryString). 53 | success(function(data, status, headers, config) { 54 | deferred.resolve(data); 55 | }). 56 | error(function(data, status, headers, config) { 57 | deferred.reject("Unable to search"); 58 | }); 59 | return deferred.promise; 60 | }; 61 | 62 | CustomDatasource.prototype.getDashboard = function(id, isTemp) { 63 | var deferred = $q.defer() 64 | $http.get('/dashboard/'+id). 65 | success(function(data, status, headers, config) { 66 | deferred.resolve(data); 67 | }). 68 | error(function(data, status, headers, config) { 69 | deferred.reject("Unable to get dashboard "+id); 70 | }); 71 | 72 | return deferred.promise; 73 | }; 74 | 75 | CustomDatasource.prototype.saveDashboard = function(dashboard) { 76 | var id = encodeURIComponent(kbn.slugifyForUrl(dashboard.title)); 77 | dashboard.id = id; 78 | 79 | var deferred = $q.defer() 80 | $http.post('/dashboard/'+id, dashboard). 81 | success(function(data, status, headers, config) { 82 | deferred.resolve({ title: dashboard.title, url: "/dashboard/db/" + id }) 83 | }). 84 | error(function(data, status, headers, config) { 85 | deferred.reject("Unabled to save dashboard "+id); 86 | }); 87 | 88 | return deferred.promise; 89 | }; 90 | 91 | return CustomDatasource; 92 | 93 | }); 94 | 95 | }); -------------------------------------------------------------------------------- /templates/signin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | 17 | 18 | 19 |

20 | Select a provider: 21 |

22 | {{ if .Error }} 23 |

24 | {{ .Error }} 25 |

26 | {{ end }} 27 | {{ if .Google }} 28 |
29 | Sign-In with Google 30 |
31 | {{ end }} 32 | 33 | --------------------------------------------------------------------------------