Initial working version
This commit is contained in:
commit
ab068d3aa0
|
@ -0,0 +1,15 @@
|
|||
FROM golang:1-alpine AS build
|
||||
WORKDIR /src
|
||||
COPY go.* ./
|
||||
RUN go mod download -x
|
||||
COPY cmd ./cmd/
|
||||
RUN CGO_ENABLED=0 GOOS=linux go install ./...
|
||||
|
||||
FROM alpine AS runtime
|
||||
WORKDIR /target
|
||||
COPY web web
|
||||
COPY --from=build /go/bin/ .
|
||||
|
||||
FROM scratch
|
||||
COPY --from=runtime /target /
|
||||
ENTRYPOINT ["/webstat"]
|
|
@ -0,0 +1,6 @@
|
|||
# WebStat
|
||||
|
||||
This is a simple service to provide `/proc/stat` on demand.
|
||||
Some JavaScript parses it and graphs it.
|
||||
|
||||
Essentially, this is a browser version of something like top or btop.
|
|
@ -0,0 +1,7 @@
|
|||
#! /bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
tag=git.woozle.org/neale/webstat:latest
|
||||
|
||||
docker buildx build --push --tag $tag $(dirname $0)/.
|
|
@ -0,0 +1,40 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type FileSender struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (s *FileSender) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
f, err := os.Open(s.path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if _, err := io.Copy(w, f); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
address := flag.String("address", ":8080", "Address to bind")
|
||||
web := flag.String("web", "web", "Web directory")
|
||||
|
||||
http.Handle("/proc/stat", &FileSender{"/proc/stat"})
|
||||
http.Handle("/proc/meminfo", &FileSender{"/proc/meminfo"})
|
||||
http.Handle("/", http.FileServer(http.Dir(*web)))
|
||||
|
||||
log.Println("Listening on", *address)
|
||||
http.ListenAndServe(*address, http.DefaultServeMux)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Webstat</title>
|
||||
<meta charset="utf-8">
|
||||
<script src="webstat.mjs" type="module"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div><canvas id="stats"></canvas></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,121 @@
|
|||
//import Chart from "https://esm.run/chart.js@4.2.1/auto"
|
||||
import Chart from "https://cdn.jsdelivr.net/npm/chart.js@4.2.1/auto/+esm"
|
||||
|
||||
const Millisecond = 1
|
||||
const Second = 1000 * Millisecond
|
||||
const Minute = 60 * Second
|
||||
|
||||
function qpush(arr, val, len) {
|
||||
arr.push(val)
|
||||
if (arr.length > len) {
|
||||
arr.shift()
|
||||
}
|
||||
}
|
||||
|
||||
class Stat {
|
||||
constructor(text) {
|
||||
let lines = text.split("\n")
|
||||
for (let line of lines) {
|
||||
let parts = line.split(/\s+/)
|
||||
let key = parts.shift()
|
||||
let vals = parts.map(Number)
|
||||
if (key.startsWith("cpu")) {
|
||||
this[key] = {
|
||||
user: vals[0],
|
||||
nice: vals[1],
|
||||
sys: vals[2],
|
||||
idle: vals[3],
|
||||
wait: vals[4],
|
||||
irq: vals[5],
|
||||
softirq: vals[6],
|
||||
steal: vals[7],
|
||||
guest: vals[8],
|
||||
guestNice: vals[9],
|
||||
}
|
||||
} else {
|
||||
this[key] = vals
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class StatUpdater {
|
||||
constructor(interval, width=60) {
|
||||
this.width = width
|
||||
this.canvas = document.querySelector("#stats")
|
||||
this.data ={
|
||||
labels: [],
|
||||
datasets: []
|
||||
}
|
||||
this.datasets = {}
|
||||
for (let label of ["user", "nice", "sys", "idle", "wait"]) {
|
||||
let d = []
|
||||
this.data.datasets.push({
|
||||
label: label,
|
||||
data: d,
|
||||
})
|
||||
this.datasets[label] = d
|
||||
}
|
||||
this.chart = new Chart(
|
||||
this.canvas,
|
||||
{
|
||||
type: "line",
|
||||
data: this.data,
|
||||
options: {
|
||||
responsive: true,
|
||||
pointStyle: false,
|
||||
scales: {
|
||||
x: {
|
||||
title: { display: false },
|
||||
ticks: { display: false },
|
||||
grid: { display: false },
|
||||
},
|
||||
y: {
|
||||
stacked: true,
|
||||
ticks: { display: false },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
setInterval(() => this.update(), interval)
|
||||
this.update()
|
||||
}
|
||||
|
||||
async update() {
|
||||
let now = Date.now()
|
||||
let resp = await fetch("proc/stat")
|
||||
let stext = await resp.text()
|
||||
let stat = new Stat(stext)
|
||||
if (this.last) {
|
||||
let user = stat.cpu.user - this.last.cpu.user
|
||||
let nice = stat.cpu.nice - this.last.cpu.nice
|
||||
let sys = stat.cpu.sys - this.last.cpu.sys
|
||||
let idle = stat.cpu.idle - this.last.cpu.idle
|
||||
let wait = stat.cpu.wait - this.last.cpu.wait
|
||||
let total = user + nice + sys + idle + wait
|
||||
|
||||
qpush(this.data.labels, now, this.width)
|
||||
qpush(this.datasets.user, user/total, this.width)
|
||||
qpush(this.datasets.nice, nice/total, this.width)
|
||||
qpush(this.datasets.sys, sys/total, this.width)
|
||||
qpush(this.datasets.idle, idle/total, this.width)
|
||||
qpush(this.datasets.wait, wait/total, this.width)
|
||||
|
||||
//this.data.datasets[0].label= `user: ${user/total*100}`
|
||||
}
|
||||
this.last = stat
|
||||
this.chart.update()
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
new StatUpdater(2*Second)
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", init)
|
||||
} else {
|
||||
init()
|
||||
}
|
Loading…
Reference in New Issue