Commit 5e4091f1 authored by Geoff Simmons's avatar Geoff Simmons

govarnishstat runs a websockets app to imitate varnishstat curses mode.

parent c6389ce6
......@@ -274,4 +274,6 @@ func main() {
if *xmlf {
doXML(vstats)
}
doWebSocks(vstats)
}
/*-
* Copyright (c) 2018 UPLEX Nils Goroll Systemoptimierung
* All rights reserved
*
* Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package main
const table = `<!DOCTYPE html>
<html>
<head>
<title>govarnishstat</title>
<style>
table {
border-collapse: collapse;
font-family: monospace,monospace;
}
table, td, th {
border: 2px solid black;
}
td.name, th.name {
width: 24em;
text-align: left;
}
td, th {
width: 14em;
text-align: right;
padding: 0.25rem;
}
th {
background-color: black;
color: white;
}
tr:hover {background-color: WhiteSmoke;}
tr.hidden { display: none; }
table.uptime {
float: left;
text-align: left;
width: 18em;
}
table.hitrate {
float: right;
width: 23em;
}
.top {
border: none;
}
#stats {
clear: both;
}
#wrapper {
display: inline-block;
}
.controls {
font-family: monospace,monospace;
margin: 0 10px;
float: right;
}
</style>
<script type="text/javascript">
var addr = "ws://" + location.hostname + ":" + location.port
var tbl
var tbody
var show0 = false
var formatting = true
var verbosity = "INFO"
var verbose_select
var diag
var debug
function format_num(cell, val) {
if (cell.classList.contains("float")) {
cell.innerHTML = val.toFixed(2) + "&nbsp;"
return
}
cell.innerHTML = val + "&nbsp;"
}
function format_duration(cell, val) {
if (cell.classList.contains("float")) {
return
}
var days = Math.floor(val/86400).toFixed(0)
var hours = (Math.floor((val % 86400)/3600)).toFixed(0)
if (hours < 10) {
hours = "0" + hours
}
var mins = (Math.floor((val % 3600)/60)).toFixed(0)
if (mins < 10) {
mins = "0" + mins
}
secs = (val % 60).toFixed(0)
if (secs < 10) {
secs = "0" + secs
}
cell.innerHTML = days + "+" + hours + ":" + mins + ":" + secs + "&nbsp;"
}
function format_bytes(cell, val) {
var suffix = ["&nbsp;", "K", "M", "G", "T", "P", "E", "Z", "Y"]
var i
if (cell.classList.contains("int") && val < 1024) {
cell.innerHTML = val.toFixed(0) + "&nbsp;"
return
}
for (i = 0; val >= 1024; i++)
val /= 1024
cell.innerHTML = val.toFixed(2) + suffix[i]
}
function format_bitmap(cell, val, bitmap) {
if (!cell.classList.contains("int")) {
return
}
if (!formatting) {
format_num(cell, val)
return
}
cell.innerHTML = bitmap + "&nbsp;"
}
function format(cell, val) {
if (!formatting) {
format_num(cell, val)
return
}
if (cell.classList.contains("d")) {
format_duration(cell, val)
return
}
if (cell.classList.contains("B")) {
format_bytes(cell, val)
return
}
if (cell.classList.contains("b")) {
return
}
format_num(cell, val)
}
function getD9ns() {
var ws = new WebSocket(addr + "/d9ns")
ws.onmessage = function (evt) {
var d9ns = JSON.parse(evt.data)
for (i = 0; i < d9ns.length; i++) {
var row = tbody.insertRow(i)
var nameCell = row.insertCell(0)
var curCell = row.insertCell(1)
var changeCell = row.insertCell(2)
var avgCell = row.insertCell(3)
var avg10Cell = row.insertCell(4)
var avg100Cell = row.insertCell(5)
var avg1000Cell = row.insertCell(6)
curCell.classList.add("int", d9ns[i].format,
d9ns[i].semantics)
changeCell.classList.add("float", d9ns[i].format,
d9ns[i].semantics)
avgCell.classList.add("float", d9ns[i].format,
d9ns[i].semantics)
avg10Cell.classList.add("float", d9ns[i].format,
d9ns[i].semantics)
avg100Cell.classList.add("float", d9ns[i].format,
d9ns[i].semantics)
avg1000Cell.classList.add("float", d9ns[i].format,
d9ns[i].semantics)
if (d9ns[i].semantics == "g") {
avgCell.innerHTML = ".&nbsp;&nbsp;&nbsp;"
}
row.id = d9ns[i].name
nameCell.innerHTML = d9ns[i].name
nameCell.className = "name"
row.id = d9ns[i].name
row.classList.add(d9ns[i].level)
if (d9ns[i].level != "INFO") {
row.classList.add("hidden")
}
}
};
}
function getStats() {
var ws = new WebSocket(addr + "/stats")
ws.onmessage = function (evt) {
var stats = JSON.parse(evt.data)
var row = document.getElementById(stats.name)
var cells = row.cells
if (cells[1].classList.contains("b")) {
format_bitmap(cells[1], stats.value, stats.bitmap)
}
else {
format(cells[1], stats.value)
}
format(cells[2], stats.change)
if (!cells[3].classList.contains("g")) {
format(cells[3], stats.avg)
}
format(cells[4], stats.avg10)
format(cells[5], stats.avg100)
format(cells[6], stats.avg1000)
if (!show0 && stats.value == 0) {
row.classList.add("hidden")
}
if (stats.name == "MGT.uptime") {
var cell = document.getElementById("uptime_mgt")
format_duration(cell, stats.value)
}
if (stats.name == "MAIN.uptime") {
var cell = document.getElementById("uptime_chld")
format_duration(cell, stats.value)
}
ws.send("ACK");
};
}
function format_hitrate(id, val) {
var cell = document.getElementById(id)
cell.innerHTML = val.toFixed(4)
}
function getHitrate() {
var ws = new WebSocket(addr + "/hitrate")
ws.onmessage = function (evt) {
var stats = JSON.parse(evt.data)
format_hitrate("hitrate10", stats.avg10)
format_hitrate("hitrate100", stats.avg100)
format_hitrate("hitrate1000", stats.avg1000)
ws.send("ACK");
};
}
function setHidden(rows) {
for (i = 0; i < rows.length; i++) {
rows[i].classList.add("hidden")
}
}
function setVisible(rows) {
for (i = 0; i < rows.length; i++) {
rows[i].classList.remove("hidden")
}
}
function setVerbosity() {
var verbosity = verbose_select.value
if (verbosity == "INFO") {
setHidden(diag)
setHidden(debug)
return
}
if (verbosity == "DIAG") {
setVisible(diag)
setHidden(debug)
return
}
setVisible(diag)
setVisible(debug)
}
window.onload = function() {
tbl = document.getElementById("statsTbl")
tbody = tbl.getElementsByTagName('tbody')[0];
getD9ns()
getStats()
getHitrate()
verbose_select = document.getElementById("verbosity")
verbose_select.addEventListener("change", setVerbosity);
diag = document.getElementsByClassName("DIAG")
debug = document.getElementsByClassName("DEBUG")
}
</script>
</head>
<body>
<div id="wrapper">
<div id="header">
<table class="uptime top">
<tr class="top">
<td class="top">Uptime mgt:</td>
<td class="top" id="uptime_mgt"></td>
</tr>
<tr class="top">
<td class="top">Uptime child:</td>
<td class="top" id="uptime_chld"></td>
</tr>
</table>
<table class="hitrate top">
<tr>
<td class="top">Hitrate n:</td>
<td class="top">10</td>
<td class="top">100</td>
<td class="top">1000</td>
</tr>
<tr class="top">
<td class="top">avg(n):</td>
<td class="top" id="hitrate10"></td>
<td class="top" id="hitrate100"></td>
<td class="top" id="hitrate1000"></td>
</tr>
</table>
</div>
<div id="stats">
<br />
<table id="statsTbl">
<thead>
<tr>
<th class="name">NAME</th>
<th>CURRENT&nbsp;</th>
<th>CHANGE&nbsp;</th>
<th>AVERAGE&nbsp;</th>
<th>AVG_10&nbsp;</th>
<th>AVG_100&nbsp;</th>
<th>AVG_1000&nbsp;</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div id="controls">
<div class="controls">
<p>Verbosity</p>
<select id="verbosity">
<option value="INFO">INFO</option>
<option value="DIAG">DIAG</option>
<option value="DEBUG">DEBUG</option>
</select>
</div>
<div class="controls">
<p>Current=0</p>
<select id="zero">
<option value="false">Hide</option>
<option value="true">Show</option>
</select>
</div>
</div>
</div>
</body>
</html>`
/*-
* Copyright (c) 2018 UPLEX Nils Goroll Systemoptimierung
* All rights reserved
*
* Author: Geoffrey Simmons <geoffrey.simmons@uplex.de>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
package main
import (
"golang.org/x/net/websocket"
"uplex.de/varnishapi/pkg/stats"
"errors"
"fmt"
"net"
"net/http"
"os/exec"
"runtime"
"time"
)
var vstats *stats.Stats
func tableHandler(w http.ResponseWriter, req *http.Request) {
w.Write([]byte(table))
}
type jsonD9n struct {
Name string `json:"name"`
Short string `json:"short"`
Long string `json:"long"`
Level string `json:"level"`
Format string `json:"format"`
S7s string `json:"semantics"`
}
func d9nsHandler(ws *websocket.Conn) {
names, err := vstats.Names()
if err != nil {
errExit(err)
}
var data []jsonD9n
for _, name := range names {
d9n, err := vstats.D9n(name)
if err != nil {
errExit(err)
}
jd9n := jsonD9n{}
jd9n.Name = name
jd9n.Short = d9n.ShortD9n
jd9n.Long = d9n.LongD9n
jd9n.Level = d9n.Level.Label
jd9n.Format = d9n.Format.String()
jd9n.S7s = d9n.Semantics.String()
data = append(data, jd9n)
}
websocket.JSON.Send(ws, data)
}
type movAvg struct {
n uint
max uint
acc float64
}
func (avg *movAvg) update(val float64) {
if avg.n < avg.max {
avg.n++
}
avg.acc += (val - avg.acc) / float64(avg.n)
}
type state struct {
last uint64
tlast time.Time
avg10 movAvg
avg100 movAvg
avg1000 movAvg
s7s stats.Semantics
}
var statState map[string]*state
type jStats struct {
Name string `json:"name"`
Value uint64 `json:"value"`
Change float64 `json:"change"`
Avg float64 `json:"avg"`
Avg10 float64 `json:"avg10"`
Avg100 float64 `json:"avg100"`
Avg1000 float64 `json:"avg1000"`
Bitmap string `json:"bitmap"`
}
func initCB(name string, val uint64) bool {
newState := state{}
newState.tlast = time.Now()
newState.last = val
d9n, err := vstats.D9n(name)
if err != nil {
errExit(err)
}
s7s := d9n.Semantics
newState.s7s = s7s
if s7s == stats.Counter || s7s == stats.Gauge {
avg10 := movAvg{0, 10, 0}
newState.avg10 = avg10
avg100 := movAvg{0, 100, 0}
newState.avg100 = avg100
avg1000 := movAvg{0, 1000, 0}
newState.avg1000 = avg1000
}
statState[name] = &newState
return true
}
func statsHandler(ws *websocket.Conn) {
var uptime uint64
uptimeCB := func(name string, val uint64) bool {
if name == "MAIN.uptime" {
uptime = val
return false
}
return true
}
readCB := func(name string, val uint64) bool {
now := time.Now()
var jstats jStats
state, ok := statState[name]
if !ok {
errExit(errors.New("uninitialized stat: " + name))
}
jstats.Name = name
jstats.Value = val
dsecs := now.Sub(state.tlast).Seconds()
change := float64(val-state.last) / dsecs
state.last = val
state.tlast = now
jstats.Change = change
if state.s7s == stats.Gauge {
state.avg10.update(float64(val))
state.avg100.update(float64(val))
state.avg1000.update(float64(val))
jstats.Avg10 = state.avg10.acc
jstats.Avg100 = state.avg100.acc
jstats.Avg1000 = state.avg1000.acc
} else if state.s7s == stats.Counter {
jstats.Avg = float64(val) / float64(uptime)
state.avg10.update(change)
state.avg100.update(change)
state.avg1000.update(change)
jstats.Avg10 = state.avg10.acc
jstats.Avg100 = state.avg100.acc
jstats.Avg1000 = state.avg1000.acc
} else if state.s7s == stats.S7sBitmap {
jstats.Bitmap = fmt.Sprintf("%10.10x",
((val >> 24) & 0xffffffffff))
}
websocket.JSON.Send(ws, jstats)
return true
}
for {
err := vstats.Read(uptimeCB)
if err != nil {
errExit(err)
}
err = vstats.Read(readCB)
if err != nil {
errExit(err)
}
time.Sleep(time.Second)
}
}
var lsnr net.Listener
func ackListener(ws *websocket.Conn) {
for {
var msg []byte
websocket.Message.Receive(ws, &msg)
if len(msg) == 0 {
fmt.Println("Client closed the connection")
lsnr.Close()
return
}
}
}
type hrStats struct {
Avg10 float64 `json:"avg10"`
Avg100 float64 `json:"avg100"`
Avg1000 float64 `json:"avg1000"`
}
func hitrateHandler(ws *websocket.Conn) {
var hits uint64
var misses uint64
var lastHits uint64
var lastMisses uint64
var gotHits bool
var gotMisses bool
hr10 := movAvg{0, 10, 0}
hr100 := movAvg{0, 100, 0}
hr1000 := movAvg{0, 1000, 0}
hrCB := func(name string, val uint64) bool {
if name == "MAIN.cache_hit" {
hits = val
gotHits = true
} else if name == "MAIN.cache_miss" {
misses = val
gotMisses = true
}
return !(gotHits && gotMisses)
}
go ackListener(ws)
for {
var hr float64
var dhits float64
var dmisses float64
gotHits = false
gotMisses = false
if err := vstats.Read(hrCB); err != nil {
errExit(err)
}
dhits = float64(hits - lastHits)
dmisses = float64(misses - lastMisses)
lastHits = hits
lastMisses = misses
if dhits+dmisses != 0 {
hr = dhits / (dhits + dmisses)
} else {
hr = 0
}
hr10.update(hr)
hr100.update(hr)
hr1000.update(hr)
hrstats := hrStats{}
hrstats.Avg10 = hr10.acc
hrstats.Avg100 = hr100.acc
hrstats.Avg1000 = hr1000.acc
websocket.JSON.Send(ws, hrstats)
time.Sleep(time.Second)
}
}
func openbrowser(url string) {
switch runtime.GOOS {
case "freebsd":
case "linux":
if err := exec.Command("xdg-open", url).Start(); err != nil {
errExit(err)
}
case "darwin":
if err := exec.Command("open", url).Start(); err != nil {
errExit(err)
}
}
}
func doWebSocks(s *stats.Stats) {
vstats = s
names, err := s.Names()
if err != nil {
errExit(err)
}
len := len(names)
statState = make(map[string]*state, len)
if err := s.Read(initCB); err != nil {
errExit(err)
}
listener, err := net.Listen("tcp", "localhost:0")
if err != nil {
errExit(err)
}
lsnr = listener
http.HandleFunc("/", tableHandler)
http.Handle("/d9ns", websocket.Handler(d9nsHandler))
http.Handle("/stats", websocket.Handler(statsHandler))
http.Handle("/hitrate", websocket.Handler(hitrateHandler))
url := "http://" + listener.Addr().String() + "/"
openbrowser(url)
fmt.Println("Listening at", url)
http.Serve(listener, nil)
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment