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