cktop - from /proc to gopsutil and back
| 4 min read
Notes on the rationale behind a recent decision I made for cktop [1] (my cli tool to monitor machines CPU, memory and processes) to start reading stats from /proc instead of using gopsutil. The story is a full circle: the original version of the tool was already reading /proc [2], then I switched to gopsutil, and then I went back to /proc. Here is why.
Context
I started to build cktop because I wanted to:
- know what my machine (a PC running Debian) is running and stats about resources
- build a tool I can use on a daily basis, produce natural/organic feedback, friction to build cognitive muscles outside work
- be exposed to topics I am interested in: observability, OS, golang
Why gopsutil
gopsutil was my go-to library to get what I needed: stats CPU, memory and processes, and was great because it also supports other OS like Windows, so I could even test my binaries in my wife’s laptop, cool.
The issue
Nothing wrong with gopsutil, it does what it’s supposed to do. The problem was that I was not getting the feedback and learning I wanted, because gopsutil abstracts all that knowledge for me: how /proc is structured, what fields to read, how to parse them, how to interpret them. I destroyed my learning experience unintentionally. That is the main reason I decided to go back to read directly /proc.
The move
Shortly after I realized I was not getting the knowledge I originally intended to, I decided to go back to read /proc directly again [3]. A few things came up:
- Clean Architecture and FCIS (Functional Core, Imperative Shell) [4] paid off here. The domain layer defines three interfaces,
MemoryReader,ProcessReader, andCPUReader, and they know nothing about where the data actually comes from. Both thegopsutiladapters and the new/procadapters implement those same interfaces. The only place in the codebase that knows which one to use ismain.go. So the full migration ended up being 3 lines changed there, just swapping the adapter names. The UI layer, the domain logic, the collector, none of that had to change. I could also leave thegopsutiladapter in place for a while if I needed it. I decided to remove the dead code at the end. - I dropped multi-OS support, so no more binaries on my wife’s laptop or my neighbor’s MacBook. But
cktopis a learning tool for me, and my interests are Linux, Go, and systems observability, so Linux-only aligns better with what I actually wanted to build. - I just learned that
/procis actually NOT a directory, it is defined as a “pseudo file-system” where we can find statistic data written by the Linux kernel: CPU, memory, processes [5]. Being a “pseudo file-system”, we can read files there as if they were real directories and files, that’s whycktopcan use plainos.Open[6]. - I found things I didn’t know, things
gopsutilwas handling silently. For example, I had no idea that CPU usage is not a single number you just read, it requires two consecutive reads and computing the difference between them. Or that memory “available” and memory “free” are not the same thing. Reading/procdirectly made all of that visible, and that’s exactly what I was looking for from the beginning.
Conclusions
Using gopsutil saved me time getting features done. But it also blocked the learning I originally wanted, and I didn’t realize it until I was already deep in it. This time, learning > features.
Also, because the data source was behind an interface, I could swap the whole implementation and the rest of the app didn’t even notice. That’s Clean Architecture and FCIS doing their job, not as a concept but as something practical that actually saved me time when I needed it.
cktop still is my personal side project I use to learn concepts about what’s interesting to me these days: Go + Observability + Systems + Linux. I have some more ideas to introduce to this tool soon, they relate to metrics and monitoring and how to persist stats, which is also huge for me.
[1]: https://github.com/ckinan/cktop/[2]: https://github.com/ckinan/cktop/commit/974e02cfa0d1d1b840af3b4fa31dcd375a11275a[3]: https://github.com/ckinan/cktop/commit/66d8d651d59da31a3ae4b4bb2ddcd84e08b34bb1[4]: https://cesarkina.com/blog/clean-architecture-fcis/[5]: https://man7.org/linux/man-pages/man5/proc.5.html[6]: https://github.com/ckinan/cktop/blob/7e64ad620183d2e1fc9e1dde7f7f8fa573b35121/internal/adapters/proc/mem.go#L17