Skip to content

Instantly share code, notes, and snippets.

@gocs
Created June 12, 2024 04:30
Show Gist options
  • Save gocs/bc45a304523da8d982679a2eecf21445 to your computer and use it in GitHub Desktop.
Save gocs/bc45a304523da8d982679a2eecf21445 to your computer and use it in GitHub Desktop.
svelte go net stat
module svelted3
go 1.22.2
require github.com/shirou/gopsutil/v3 v3.24.4
require (
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/sys v0.19.0 // indirect
)
package main
import (
"fmt"
"log/slog"
"net/http"
"strings"
"time"
"github.com/shirou/gopsutil/v3/net"
)
type NetState struct {
prevRx uint64
prevTx uint64
bytesRx []int
bytesTx []int
}
const (
// NetInterfaceAll enables all network interfaces
NetInterfaceAll = "all"
// NetInterfaceVpn is the VPN interface
NetInterfaceVpn = "tun0"
)
func getNetTransfer() (uint64, uint64, error) {
interfaces, err := net.IOCounters(true)
if err != nil {
return 0, 0, fmt.Errorf("error getting network activity: %v", err)
}
var totalBytesRecv uint64
var totalBytesSent uint64
interfaceMap := make(map[string]bool)
// Default behaviour
interfaceMap[NetInterfaceAll] = true
interfaceMap[NetInterfaceVpn] = false
// Build a map with wanted status for each interfaces.
for _, iface := range NetInterface {
if strings.HasPrefix(iface, "!") {
interfaceMap[strings.TrimPrefix(iface, "!")] = false
} else {
// if we specify a wanted interface, remove capture on all.
delete(interfaceMap, NetInterfaceAll)
interfaceMap[iface] = true
}
}
for _, _interface := range interfaces {
wanted, ok := interfaceMap[_interface.Name]
if wanted && ok { // Simple case
totalBytesRecv += _interface.BytesRecv
totalBytesSent += _interface.BytesSent
} else if ok { // Present but unwanted
continue
} else if interfaceMap[NetInterfaceAll] { // Capture other
totalBytesRecv += _interface.BytesRecv
totalBytesSent += _interface.BytesSent
}
}
return totalBytesRecv, totalBytesSent, nil
}
var NetInterface = []string{"all"}
type NetTraffic struct {
Recieved uint64
Sent uint64
TotalRecieved uint64
TotalSent uint64
}
// String returns a string representation of the NetTraffic struct
func (nt NetTraffic) String() string {
return fmt.Sprintf("%d,%d,%d,%d", nt.Recieved, nt.Sent, nt.TotalRecieved, nt.TotalSent)
}
func (ns *NetState) events(w http.ResponseWriter, r *http.Request) {
// Set CORS headers to allow all origins. You may want to restrict this to specific origins in a production environment.
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Expose-Headers", "Content-Type")
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
// loop forever by ticks until client cancels
t := time.NewTicker(2 * time.Second)
defer t.Stop()
for {
select {
case <-t.C:
totalRx, totalTx, err := getNetTransfer()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
var rx uint64
var tx uint64
if ns.prevRx != 0 { // if this isn't the first update
rx = totalRx - ns.prevRx
}
if ns.prevTx != 0 { // if this isn't the first update
tx = totalTx - ns.prevTx
}
ns.prevRx = totalRx
ns.prevTx = totalTx
ns.bytesRx = append(ns.bytesRx, int(rx))
ns.bytesTx = append(ns.bytesTx, int(tx))
nt := NetTraffic{Recieved: rx, Sent: tx, TotalRecieved: totalRx, TotalSent: totalTx}
fmt.Fprintf(w, "data:%v\n\n", nt)
w.(http.Flusher).Flush()
slog.Info("client flushed")
case <-r.Context().Done():
slog.Info("client disconnected")
return
}
}
}
func main() {
ns := &NetState{
prevRx: 0,
prevTx: 0,
bytesRx: make([]int, 0),
bytesTx: make([]int, 0),
}
http.HandleFunc("/events", ns.events)
http.ListenAndServe(":8080", nil)
}
<script>
import { onMount } from "svelte";
import { writable } from "svelte/store";
const messages = writable([]);
let width = 1000;
let height = 200;
let gap = width / 100;
let rx_mz = "";
let tx_mz = "";
// let rx_tot_mz = "";
// let tx_tot_mz = "";
onMount(() => {
const evtSource = new EventSource("http://localhost:8080/events");
evtSource.onmessage = function (event) {
let dataobj = event.data.split(",");
messages.update((arr) => {
// remove first element if array length is greater than width / 10
if (arr.length > width / 10) arr.shift();
arr = arr.concat(dataobj);
rx_mz =
arr
.map((m, i) => `${i * gap},${height - 100 - m[0]}`)
.join(" ") + ` ${arr.length * gap},${height}`;
tx_mz =
arr
.map((m, i) => `${i * gap},${height - 100 - m[1]}`)
.join(" ") + ` ${arr.length * gap},${height}`;
// rx_tot_mz =
// arr
// .map((m, i) => `${i * gap},${height - 100 - m[2]}`)
// .join(" ") + ` ${arr.length * gap},${height}`;
// tx_tot_mz =
// arr
// .map((m, i) => `${i * gap},${height - 100 - m[3]}`)
// .join(" ") + ` ${arr.length * gap},${height}`;
return arr;
});
};
});
</script>
<div class="w-full">
<svg {width} {height}>
// triangular filling path
{#if rx_mz}<path
class="fill-red-600/60"
d="M0,{height} {rx_mz}z"
/>{/if}
{#if tx_mz}<path
class="fill-green-600/60"
d="M0,{height} {tx_mz}z"
/>{/if}
<!-- {#if rx_tot_mz}<path class="fill-orange-600/60" d="M0,{height} {rx_tot_mz}z" />{/if}
{#if tx_tot_mz}<path class="fill-yellow-600/60" d="M0,{height} {tx_tot_mz}z" />{/if} -->
</svg>
</div>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment