Initial working version

This commit is contained in:
Neale Pickett 2023-03-04 23:19:54 -07:00
commit ab068d3aa0
7 changed files with 204 additions and 0 deletions

15
Dockerfile Normal file
View File

@ -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"]

6
README.md Normal file
View File

@ -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.

7
build.sh Executable file
View File

@ -0,0 +1,7 @@
#! /bin/sh
set -e
tag=git.woozle.org/neale/webstat:latest
docker buildx build --push --tag $tag $(dirname $0)/.

40
cmd/webstat/main.go Normal file
View File

@ -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)
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.woozle.org/neale/webstat
go 1.18

12
web/index.html Normal file
View File

@ -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>

121
web/webstat.mjs Normal file
View File

@ -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()
}