DEV Community

M
M

Posted on

golang + sse + Data-Star for real time sys stats

Hello everyone,
today I want to show you, how simple with https://data-star.dev hypermedia framework to is to make real time "things"

lets start with some code.

SysStats using golang and data-star.dev

tools we need today is:
https://templ.guide for html templating
https://data-star.dev for realtime frontend updates via SSE
https://github.com/inhies/go-bytesize to convert bytes to normal representation like Mb Gb
https://github.com/shirou/gopsutil to get sys stats
and https://github.com/go-chi/chi for routing

first, we need to collect sys stats (memory and cpu) so there is our helper function:

type Stats struct {
    MemTotal   string
    MemPercent float64
    MemUsed    string
    MemFree    string
    SwapTotal  string
    CpuUser        int
    CpuSystem      int
    CpuIdle        int
}

func collectStats() Stats {
    v, _ := mem.VirtualMemory()
    cp, _ := cpu.Times(false)
    var stats Stats
    stats.MemUsed = bytesize.New(float64(v.Used)).String()
    stats.MemTotal = bytesize.New(float64(v.Total)).String()
    stats.MemFree = bytesize.New(float64(v.Free)).String()
    stats.MemPercent = v.UsedPercent

    for _, c := range cp {
        stats.CpuSystem += int(c.System)
        stats.CpuUser += int(c.User)
        stats.CpuIdle += int(c.Idle)
    }

    stats.CpuSystem /= len(cp)
    stats.CpuUser /= len(cp)
    stats.CpuIdle /= len(cp)
    return stats
}

Enter fullscreen mode Exit fullscreen mode

now once we have our stats we could implement our realtime updates. here is some main code for it:

r.Get("/live/stats", func(w http.ResponseWriter, r *http.Request) {
        sse := datastar.NewSSE(w, r)
        ticker := time.NewTicker(1 * time.Second)
        defer ticker.Stop()
        for {
            select {
            case <-r.Context().Done():
                return
            case <-ticker.C:
                stats := collectStats()
                sse.MergeFragmentTempl(Stat(stats))
            }
        }
    })
Enter fullscreen mode Exit fullscreen mode

this collects stats every one second, and publishes it to Data-Star via SSE

and our index page renders everything at initial load:

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        stats := collectStats()
        Main(stats).Render(r.Context(), w)
    })
Enter fullscreen mode Exit fullscreen mode

Main(statS) is main layout with doctype and all partials like "stats" section. In piece of code for /live/stats - it renders only part of page and sends that part as html to data-star which updates stats element. (not whole page are send)

and here is our templ.guide template:


templ Main(stats Stats) {
    <!DOCTYPE html>
    <html>
        <head>
            <title>Sys Stats</title>
            <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@v1.0.0-beta.9/bundles/datastar.js"></script>
        </head>
        <body data-on-load="@get('/live/stats')"></body>
        @Stat(stats)
    </html>
}

templ Stat(stats Stats) {
    <div id="stats">
    <p><strong>MemFree</strong> { fmt.Sprintf("%s", stats.MemFree) }</p>
    <p><strong>MemUsed</strong> { fmt.Sprintf("%s",stats.MemUsed) }</p>
    <p><strong>MemTotal</strong> { fmt.Sprintf("%s",stats.MemTotal) }</p>
    <p><strong>MemPercent</strong> { fmt.Sprintf("%f",stats.MemPercent) }</p>
    <p><strong>CPU system</strong> { fmt.Sprintf("%d",stats.CpuSystem) }</p>
    <p><strong>CPU user</strong> { fmt.Sprintf("%d",stats.CpuUser) }</p>
    <p><strong>CPU idle</strong> { fmt.Sprintf("%d",stats.CpuIdle) }</p>
    </div>
}

Enter fullscreen mode Exit fullscreen mode

templ Main - accepts stats as argument, renders whole html and subscribes for changes on /live/stats url.

templ Stat - accepts stats as argument, renders only that part and will be sent to SSE (idiomorph finds element with same id and replaces it )

example could be found at https://github.com/blinkinglight/go-sysstats clone, run go run . and navigate to https://localhost:8088 and it should just work.

Top comments (0)