Skip to content

Commit

Permalink
Add the golang example code
Browse files Browse the repository at this point in the history
  • Loading branch information
robsears committed Oct 23, 2019
1 parent 3a49e0a commit 0b279a2
Show file tree
Hide file tree
Showing 16 changed files with 598 additions and 1 deletion.
1 change: 0 additions & 1 deletion golang
Submodule golang deleted from c03143
4 changes: 4 additions & 0 deletions golang/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.DS_Store
.idea
go-bonsai
*.iml
28 changes: 28 additions & 0 deletions golang/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## Go Bonsai! A lightweight Elasticsearch cluster exploration tool written in golang.

To get started, clone the repo:

```
git clone [email protected]:omc/dogfood-go.git
```

Make sure to export your Bonsai cluster URL to the dev environment:

```
export BONSAI_URL="https://user:[email protected]"
```

Build the tool:

```
go build bonsai-example-go.go
```

Run it:

```
$ chmod +x go-bonsai
$ ./go-bonsai
```

Check it out by opening up a browser and navigating to http://localhost:8080
20 changes: 20 additions & 0 deletions golang/go-bonsai.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import(
"./lib/controllers"
"net/http"
)

func main() {
http.HandleFunc("/", rootHandler)
http.HandleFunc("/index/", indexHandler)
http.ListenAndServe(":8080", nil)
}

func indexHandler(w http.ResponseWriter, r *http.Request) {
controllers.IndexBuilder(w, r)
}

func rootHandler(w http.ResponseWriter, r *http.Request) {
controllers.RootBuilder(w, r)
}
73 changes: 73 additions & 0 deletions golang/lib/controllers/http_handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package controllers

import(
"../elasticsearch"
"../views"
"../errors"
"gopkg.in/olivere/elastic.v2"
"net/http"
"sort"
)

func IndexBuilder(w http.ResponseWriter, r *http.Request) {
es,index := elasticsearch.ConfirmIndex(w, r)
if index != "" {

// Get a list of sorted index names
indices, _ := es.IndexNames()
sort.Strings(indices)

// Query for some docs
// Use default settings for FROM and SIZE
query := elastic.NewQueryStringQuery("*")
docs,_ := es.Search().From(0).Size(10).Index(index).Query(query).Pretty(true).Do()

// Get a mapping for the index
mappings := elasticsearch.GetIndexMappings(es, index)
var mappingTypes []string
typeCounts := make(map[string]int64)
if docs.TotalHits() > 0 {
// No docs? Then there ain't no mappin' to get
types := mappings[index].(map[string]interface{})["mappings"].(map[string]interface{})
mappingTypes = elasticsearch.GetTypesFromMapping(types)
sort.Strings(mappingTypes)

for _,i:=range mappingTypes {
typeDocs,_ := es.Search().From(0).Size(10).Index(index).Type(i).Query(query).Pretty(true).Do()
typeCounts[i] = typeDocs.TotalHits()
}
}

//Create a view with the data and render it for the user
p := &views.View{
Title: "Overview of " + index,
Hits: docs.TotalHits(),
Docs: docs.Hits.Hits,
Name: index,
IndexCount: len(indices),
Indices: indices,
Mappings: mappings,
MappingTypes: mappingTypes,
TypeCounts: typeCounts}
views.Render(p, w, r, "index")
}
}

func RootBuilder(w http.ResponseWriter, r *http.Request) {
es := elasticsearch.GetClient()
indices,err := es.IndexNames()

if err != nil {
errors.ExitOnError(err, errors.INDEX_NAME_ERROR)
}
sort.Strings(indices)
health := elasticsearch.GetClusterHealth(es)
stats := elasticsearch.GetClusterStats(es)
p := &views.View{
Title: "Cluster Overview",
Health: health,
Stats: stats,
IndexCount: len(indices),
Indices: indices}
views.Render(p, w, r, "root")
}
97 changes: 97 additions & 0 deletions golang/lib/elasticsearch/elasticsearch.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package elasticsearch

import(
"gopkg.in/olivere/elastic.v2"
"os"
"regexp"
"strings"
"net/http"
"fmt"
"../errors"
"../views"
)

// Instantiate an Elasticsearch client
func GetClient() *elastic.Client {
user,pass,host := parseBonsaiURL()
client, err := elastic.NewSimpleClient(
elastic.SetURL(host),
elastic.SetMaxRetries(10),
elastic.SetBasicAuth(user,pass),
elastic.SetSniff(false))
if err != nil {
errors.ExitOnError(err, errors.CLIENT_FAILURE)
}
return client
}

//
func GetClusterHealth(es *elastic.Client) *elastic.ClusterHealthResponse {
health,_ := es.ClusterHealth().
Do()
return health
}

//
func GetClusterStats(es *elastic.Client) *elastic.IndicesStatsResponse {
stats,_ := es.IndexStats().
Index("_all").
Human(true).
Pretty(true).
Do()
return stats
}

func SearchDocuments(es *elastic.Client, index string) *elastic.SearchResult {
docs,_ := es.Search().
Index(index).
Do()
return docs
}

func GetIndexMappings(es *elastic.Client, index string) map[string]interface{} {
mappings,err := es.GetMapping().Index(index).Do()
if err != nil {
errors.ExitOnError(err, errors.INDEX_MAPPING_ERROR)
}
return mappings
}

// Make sure that an index exists before we do anything with it.
// If it exists, return the client for reuse
func ConfirmIndex(w http.ResponseWriter, r *http.Request) (*elastic.Client, string) {
rex, err := regexp.Compile("/index/(.*?)")
index := rex.ReplaceAllString(r.URL.Path, "$1")
if err != nil {
errors.ExitOnError(err, errors.REGEX_COMPILE)
}
es := GetClient()
indexCheck, err := es.IndexExists(index).Do()
if indexCheck == false {
indices,_ := es.IndexNames()
fmt.Printf("Aw, brah, I couldn't find an index called '%s'!\n", index)
p := &views.View{IndexCount: len(indices), Indices: indices, ErrorMessage: "That index doesn't exist!"}
views.Render(p, w, r, "root")
return es,""
} else if err != nil {
errors.ExitOnError(err, errors.INDEX_EXISTS_ERROR)
}
return es, index
}

func parseBonsaiURL() (string, string, string){
url := os.Getenv("BONSAI_URL")
rex, _ := regexp.Compile(".*?://([a-z0-9]{1,}):([a-z0-9]{1,})@.*$")
user := rex.ReplaceAllString(url, "$1")
pass := rex.ReplaceAllString(url, "$2")
host := strings.Replace(url, user+":"+pass+"@", "", -1)
return user,pass,host
}

func GetTypesFromMapping(m map[string]interface{}) (types []string) {
for k := range m {
types = append(types, k)
}
return types
}

23 changes: 23 additions & 0 deletions golang/lib/errors/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package errors

import (
"fmt"
"os"
)

const (
CLIENT_FAILURE = "Could not instantiate a client to the Elasticsearch cluster"
JSON_DECODE_FAILURE = "Could not decode JSON"
INDEX_EXISTS_ERROR = "The Elasticsearch client failed to determine whether the index exists"
INDEX_NAME_ERROR = "An error occurred while retrieving the cluster's index names"
REGEX_COMPILE = "Could not compile the regular expression"
TEMPLATE_PARSE_FAILURE = "The template engine failed to parse the requested view"
TEMPLATE_EXECUTE_FAILURE = "The template engine failed to execute the requested view"
INDEX_MAPPING_ERROR = "The requested index mapping could not be found"
)

// Quit the program if there is an error
func ExitOnError(err error, errMsg string) {
fmt.Printf("The program exited with error category '%s'. Specific failure: %s\n", errMsg, err)
os.Exit(1)
}
20 changes: 20 additions & 0 deletions golang/lib/views/templates/container.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{{define "container"}}
<div class="container-fluid">
<div class="row">
<div class="col-sm-3 col-md-2 sidebar">
{{ template "sidebar" . }}
</div>
<div class="col-sm-9 col-sm-offset-3 col-md-10 col-md-offset-2 main">

{{if .ErrorMessage}}
<div class="alert alert-danger">
{{ .ErrorMessage }}
</div>
{{end}}

{{ template "content" . }}

</div>
</div>
</div>
{{end}}
4 changes: 4 additions & 0 deletions golang/lib/views/templates/footer.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{{define "footer"}}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="http://getbootstrap.com/dist/js/bootstrap.min.js"></script>
{{end}}
13 changes: 13 additions & 0 deletions golang/lib/views/templates/header.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{{define "header"}}
<meta charset=utf-8>
<meta http-equiv=X-UA-Compatible content="IE=edge">
<meta name=viewport content="width=device-width,initial-scale=1">
<meta name=author content="Rob Sears">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="http://getbootstrap.com/examples/dashboard/dashboard.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<title>{{ .Title }}</title>
{{end}}
99 changes: 99 additions & 0 deletions golang/lib/views/templates/index.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{{ define "content"}}
<h1 class="page-header">Overview of {{ .Name }}</h1>

{{if gt .Hits 0}}

<div>

<!-- Nav tabs -->
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" class="active"><a href="#home" aria-controls="home" role="tab" data-toggle="tab">Stats</a></li>
<li role="presentation"><a href="#profile" aria-controls="profile" role="tab" data-toggle="tab">Fields</a></li>
<li role="presentation"><a href="#messages" aria-controls="messages" role="tab" data-toggle="tab">Documents</a></li>
</ul>

<!-- Tab panes -->
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="home">
<style>

.chart div {
font: 10px sans-serif;
background-color: steelblue;
text-align: right;
padding: 3px;
margin: 1px;
color: white;
min-width:100px;
}

</style>
<div class="chart"></div>
<script language="javascript" type="text/javascript">
var data = {{ d3data .TypeCounts }};
var keys = Object.keys(data)
var vals = Object.keys(data).map(function(v) { return (((data[v]) * 100.0 / {{ .Hits }}).toFixed(0)); })
var x = d3.scale.linear()
.domain([0, d3.max(vals)])
.range([0, 100]);

d3.select(".chart")
.selectAll("div")
.data(vals)
.enter().append("div")
.style("width", function(d) { return x(d) + "%"; })
.text(function(d) { return keys.shift() + ": " + vals.shift() + "%"; });
</script>
</div>
<div role="tabpanel" class="tab-pane" id="profile">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<th>Type</th>
<th>Fields</th>
</thead>
<tbody>
{{range $type := .MappingTypes}}
<tr>
<td>{{ $type }}</td>
<td>
<ul>
{{ range $field := mappingFields $.Mappings $.Name $type}}
<li>{{ $field }}</li>
{{ end }}
</ul>
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="messages">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<th>ID</th>
<th>Type</th>
<th>Source</th>
</thead>
<tbody>
{{range $doc := .Docs}}
<tr>
<td>{{ $doc.Id }}</td>
<td>{{ $doc.Type }}</td>
<td>
{{ jsonDecode $doc }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
</div>
</div>

</div>
{{end}}

{{ end }}
Loading

0 comments on commit 0b279a2

Please sign in to comment.