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:

  1. know what my machine (a PC running Debian) is running and stats about resources
  2. build a tool I can use on a daily basis, produce natural/organic feedback, friction to build cognitive muscles outside work
  3. 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:

  1. Clean Architecture and FCIS (Functional Core, Imperative Shell) [4] paid off here. The domain layer defines three interfaces, MemoryReader, ProcessReader, and CPUReader, and they know nothing about where the data actually comes from. Both the gopsutil adapters and the new /proc adapters implement those same interfaces. The only place in the codebase that knows which one to use is main.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 the gopsutil adapter in place for a while if I needed it. I decided to remove the dead code at the end.
  2. I dropped multi-OS support, so no more binaries on my wife’s laptop or my neighbor’s MacBook. But cktop is 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.
  3. I just learned that /proc is 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 why cktop can use plain os.Open [6].
  4. I found things I didn’t know, things gopsutil was 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 /proc directly 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.