commit ab068d3aa0e5c081aa5dd373d52fc43e9fa92bf0 Author: Neale Pickett Date: Sat Mar 4 23:19:54 2023 -0700 Initial working version diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..342691e --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..34cb530 --- /dev/null +++ b/README.md @@ -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. diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..0250e42 --- /dev/null +++ b/build.sh @@ -0,0 +1,7 @@ +#! /bin/sh + +set -e + +tag=git.woozle.org/neale/webstat:latest + +docker buildx build --push --tag $tag $(dirname $0)/. diff --git a/cmd/webstat/main.go b/cmd/webstat/main.go new file mode 100644 index 0000000..3264378 --- /dev/null +++ b/cmd/webstat/main.go @@ -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) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4f96eca --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.woozle.org/neale/webstat + +go 1.18 diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..ac776e9 --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + Webstat + + + + + +
+ + diff --git a/web/webstat.mjs b/web/webstat.mjs new file mode 100644 index 0000000..eae42b5 --- /dev/null +++ b/web/webstat.mjs @@ -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() +}