@@ -29,14 +29,19 @@ import (
2929 "time"
3030 "unsafe"
3131
32+ "net/http"
33+
34+ "github.com/prometheus/client_golang/prometheus"
35+ "github.com/prometheus/client_golang/prometheus/promhttp"
36+
3237 ui "github.com/gizak/termui/v3"
3338 w "github.com/gizak/termui/v3/widgets"
3439 "github.com/shirou/gopsutil/mem"
3540 "howett.net/plist"
3641)
3742
3843var (
39- version = "v0.2.2 "
44+ version = "v0.2.3 "
4045 cpuGauge , gpuGauge , memoryGauge * w.Gauge
4146 modelText , PowerChart , NetworkInfo , helpText * w.Paragraph
4247 grid * ui.Grid
6772 maxPowerSeen = 0.1
6873 powerHistory = make ([]float64 , 100 )
6974 maxPower = 0.0 // Track maximum power for better scaling
70- gpuValues = make ([]float64 , 65 )
75+ gpuValues = make ([]float64 , 100 )
76+ prometheusPort string
77+ )
78+
79+ var (
80+ // Prometheus metrics
81+ cpuUsage = prometheus .NewGauge (
82+ prometheus.GaugeOpts {
83+ Name : "mactop_cpu_usage_percent" ,
84+ Help : "Current Total CPU usage percentage" ,
85+ },
86+ )
87+
88+ gpuUsage = prometheus .NewGauge (
89+ prometheus.GaugeOpts {
90+ Name : "mactop_gpu_usage_percent" ,
91+ Help : "Current GPU usage percentage" ,
92+ },
93+ )
94+
95+ gpuFreqMHz = prometheus .NewGauge (
96+ prometheus.GaugeOpts {
97+ Name : "mactop_gpu_freq_mhz" ,
98+ Help : "Current GPU frequency in MHz" ,
99+ },
100+ )
101+
102+ powerUsage = prometheus .NewGaugeVec (
103+ prometheus.GaugeOpts {
104+ Name : "mactop_power_watts" ,
105+ Help : "Current power usage in watts" ,
106+ },
107+ []string {"component" }, // "cpu", "gpu", "total"
108+ )
109+
110+ memoryUsage = prometheus .NewGaugeVec (
111+ prometheus.GaugeOpts {
112+ Name : "mactop_memory_gb" ,
113+ Help : "Memory usage in GB" ,
114+ },
115+ []string {"type" }, // "used", "total", "swap_used", "swap_total"
116+ )
71117)
72118
119+ func startPrometheusServer (port string ) {
120+ registry := prometheus .NewRegistry ()
121+ registry .MustRegister (cpuUsage )
122+ registry .MustRegister (gpuUsage )
123+ registry .MustRegister (gpuFreqMHz )
124+ registry .MustRegister (powerUsage )
125+ registry .MustRegister (memoryUsage )
126+
127+ handler := promhttp .HandlerFor (registry , promhttp.HandlerOpts {})
128+
129+ http .Handle ("/metrics" , handler )
130+ go func () {
131+ err := http .ListenAndServe (":" + port , nil )
132+ if err != nil {
133+ stderrLogger .Printf ("Failed to start Prometheus metrics server: %v\n " , err )
134+ }
135+ }()
136+ }
137+
73138type CPUUsage struct {
74139 User float64
75140 System float64
@@ -364,7 +429,31 @@ func setupUI() {
364429 pCoreCount ,
365430 gpuCoreCount ,
366431 )
367- helpText .Text = "mactop is open source monitoring tool for Apple Silicon authored by Carsen Klock in Go Lang!\n \n Repo: github.com/context-labs/mactop\n \n Controls:\n - r: Refresh the UI data manually\n - c: Cycle through UI color themes\n - p: Toggle party mode (color cycling)\n - l: Toggle the main display's layout\n - h or ?: Toggle this help menu\n - q or <C-c>: Quit the application\n \n Start Flags:\n --help, -h: Show this help menu\n --version, -v: Show the version of mactop\n --interval, -i: Set the powermetrics update interval in milliseconds. Default is 1000.\n --color, -c: Set the UI color. Default is none. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'.\n \n Version: " + version
432+ prometheusStatus := "Disabled"
433+ if prometheusPort != "" {
434+ prometheusStatus = fmt .Sprintf ("Enabled (Port: %s)" , prometheusPort )
435+ }
436+ helpText .Text = fmt .Sprintf (
437+ "mactop is open source monitoring tool for Apple Silicon authored by Carsen Klock in Go Lang!\n \n " +
438+ "Repo: github.com/context-labs/mactop\n \n " +
439+ "Prometheus Metrics: %s\n \n " +
440+ "Controls:\n " +
441+ "- r: Refresh the UI data manually\n " +
442+ "- c: Cycle through UI color themes\n " +
443+ "- p: Toggle party mode (color cycling)\n " +
444+ "- l: Toggle the main display's layout\n " +
445+ "- h or ?: Toggle this help menu\n " +
446+ "- q or <C-c>: Quit the application\n \n " +
447+ "Start Flags:\n " +
448+ "--help, -h: Show this help menu\n " +
449+ "--version, -v: Show the version of mactop\n " +
450+ "--interval, -i: Set the powermetrics update interval in milliseconds. Default is 1000.\n " +
451+ "--prometheus, -p: Set and enable a Prometheus metrics port. Default is none. (e.g. --prometheus=9090)\n " +
452+ "--color, -c: Set the UI color. Default is none. Options are 'green', 'red', 'blue', 'cyan', 'magenta', 'yellow', and 'white'.\n \n " +
453+ "Version: %s" ,
454+ prometheusStatus ,
455+ version ,
456+ )
368457 stderrLogger .Printf ("Model: %s\n E-Core Count: %d\n P-Core Count: %d\n GPU Core Count: %s" , modelName , eCoreCount , pCoreCount , gpuCoreCount )
369458
370459 processList = w .NewList ()
@@ -392,8 +481,9 @@ func setupUI() {
392481
393482 termWidth , _ := ui .TerminalDimensions ()
394483 numPoints := (termWidth / 2 ) / 2
484+ numPointsGPU := (termWidth / 2 )
395485 powerValues = make ([]float64 , numPoints )
396- gpuValues = make ([]float64 , numPoints )
486+ gpuValues = make ([]float64 , numPointsGPU )
397487
398488 sparkline = w .NewSparkline ()
399489 sparkline .LineColor = ui .ColorGreen
@@ -404,7 +494,7 @@ func setupUI() {
404494
405495 gpuSparkline = w .NewSparkline ()
406496 gpuSparkline .LineColor = ui .ColorGreen
407- gpuSparkline .MaxHeight = 10
497+ gpuSparkline .MaxHeight = 100
408498 gpuSparkline .Data = gpuValues
409499 gpuSparklineGroup = w .NewSparklineGroup (gpuSparkline )
410500 gpuSparklineGroup .Title = "GPU Usage History"
@@ -432,7 +522,7 @@ func setupGrid() {
432522 grid .Set (
433523 ui .NewRow (1.0 / 4 ,
434524 ui .NewCol (1.0 , cpuGauge ),
435- // ui.NewCol(1.0/2, gpuSparklineGroup ),
525+ // ui.NewCol(1.0/2, gpuGauge ),
436526 ),
437527 ui .NewRow (2.0 / 4 ,
438528 ui .NewCol (1.0 / 2 ,
@@ -818,6 +908,14 @@ func cycleColors() {
818908 sparklineGroup .BorderStyle = ui .NewStyle (color )
819909 sparklineGroup .TitleStyle = ui .NewStyle (color )
820910 }
911+ if gpuSparkline != nil {
912+ gpuSparkline .LineColor = color
913+ gpuSparkline .TitleStyle = ui .NewStyle (color )
914+ }
915+ if gpuSparklineGroup != nil {
916+ gpuSparklineGroup .BorderStyle = ui .NewStyle (color )
917+ gpuSparklineGroup .TitleStyle = ui .NewStyle (color )
918+ }
821919
822920 cpuCoreWidget .BorderStyle .Fg , cpuCoreWidget .TitleStyle .Fg = color , color
823921 processList .TextStyle = ui .NewStyle (color )
@@ -858,6 +956,14 @@ func main() {
858956 fmt .Println ("Error: --color flag requires a color value" )
859957 os .Exit (1 )
860958 }
959+ case "--prometheus" , "-p" :
960+ if i + 1 < len (os .Args ) {
961+ prometheusPort = os .Args [i + 1 ]
962+ i ++
963+ } else {
964+ fmt .Println ("Error: --prometheus flag requires a port number" )
965+ os .Exit (1 )
966+ }
861967 case "--interval" , "-i" :
862968 if i + 1 < len (os .Args ) {
863969 interval , err = strconv .Atoi (os .Args [i + 1 ])
@@ -889,6 +995,11 @@ func main() {
889995 }
890996 defer ui .Close ()
891997 StderrToLogfile (logfile )
998+
999+ if prometheusPort != "" {
1000+ startPrometheusServer (prometheusPort )
1001+ stderrLogger .Printf ("Prometheus metrics available at http://localhost:%s/metrics\n " , prometheusPort )
1002+ }
8921003 if setColor {
8931004 var color ui.Color
8941005 switch colorName {
@@ -1060,12 +1171,25 @@ func collectMetrics(done chan struct{}, cpumetricsChan chan CPUMetrics, gpumetri
10601171 cmd .SysProcAttr = & syscall.SysProcAttr {Setpgid : true }
10611172 stdout , err := cmd .StdoutPipe ()
10621173 if err != nil {
1063- log .Fatal (err )
1174+ stderrLogger .Fatal (err )
10641175 }
10651176 if err := cmd .Start (); err != nil {
1066- log .Fatal (err )
1177+ stderrLogger .Fatal (err )
10671178 }
1068- scanner := bufio .NewScanner (stdout )
1179+
1180+ defer func () {
1181+ if err := cmd .Process .Kill (); err != nil {
1182+ stderrLogger .Fatalf ("ERROR: Failed to kill powermetrics: %v" , err )
1183+ }
1184+ }()
1185+
1186+ // Create buffered reader with larger buffer
1187+ const bufferSize = 10 * 1024 * 1024 // 10MB
1188+ reader := bufio .NewReaderSize (stdout , bufferSize )
1189+
1190+ scanner := bufio .NewScanner (reader )
1191+ scanner .Buffer (make ([]byte , bufferSize ), bufferSize )
1192+
10691193 scanner .Split (func (data []byte , atEOF bool ) (advance int , token []byte , err error ) {
10701194 if atEOF && len (data ) == 0 {
10711195 return 0 , nil , nil
@@ -1080,27 +1204,36 @@ func collectMetrics(done chan struct{}, cpumetricsChan chan CPUMetrics, gpumetri
10801204 }
10811205 }
10821206 if atEOF {
1207+ if start >= 0 {
1208+ return len (data ), data [start :], nil
1209+ }
10831210 return len (data ), nil , nil
10841211 }
10851212 return 0 , nil , nil
10861213 })
1214+ retryCount := 0
1215+ maxRetries := 3
10871216 for scanner .Scan () {
1088- plistData := scanner .Text ()
1089- if ! strings .Contains (plistData , "<?xml" ) || ! strings .Contains (plistData , "</plist>" ) {
1090- continue
1091- }
1092- var data map [string ]interface {}
1093- err := plist .NewDecoder (strings .NewReader (plistData )).Decode (& data )
1094- if err != nil {
1095- log .Printf ("Error decoding plist: %v" , err )
1096- continue
1097- }
10981217 select {
10991218 case <- done :
1100- cmd .Process .Kill ()
11011219 return
11021220 default :
1103- // Send all metrics at once
1221+ plistData := scanner .Text ()
1222+ if ! strings .Contains (plistData , "<?xml" ) || ! strings .Contains (plistData , "</plist>" ) {
1223+ retryCount ++
1224+ if retryCount >= maxRetries {
1225+ retryCount = 0
1226+ continue
1227+ }
1228+ continue
1229+ }
1230+ retryCount = 0 // Reset retry counter on successful parse
1231+ var data map [string ]interface {}
1232+ err := plist .NewDecoder (strings .NewReader (plistData )).Decode (& data )
1233+ if err != nil {
1234+ stderrLogger .Printf ("Error decoding plist: %v" , err )
1235+ continue
1236+ }
11041237 cpuMetrics := parseCPUMetrics (data , NewCPUMetrics ())
11051238 gpuMetrics := parseGPUMetrics (data )
11061239 netdiskMetrics := parseNetDiskMetrics (data )
@@ -1271,6 +1404,16 @@ func updateCPUUI(cpuMetrics CPUMetrics) {
12711404 memoryMetrics := getMemoryMetrics ()
12721405 memoryGauge .Title = fmt .Sprintf ("Memory Usage: %.2f GB / %.2f GB (Swap: %.2f/%.2f GB)" , float64 (memoryMetrics .Used )/ 1024 / 1024 / 1024 , float64 (memoryMetrics .Total )/ 1024 / 1024 / 1024 , float64 (memoryMetrics .SwapUsed )/ 1024 / 1024 / 1024 , float64 (memoryMetrics .SwapTotal )/ 1024 / 1024 / 1024 )
12731406 memoryGauge .Percent = int ((float64 (memoryMetrics .Used ) / float64 (memoryMetrics .Total )) * 100 )
1407+
1408+ cpuUsage .Set (float64 (totalUsage ))
1409+ powerUsage .With (prometheus.Labels {"component" : "cpu" }).Set (cpuMetrics .CPUW )
1410+ powerUsage .With (prometheus.Labels {"component" : "total" }).Set (cpuMetrics .PackageW )
1411+ powerUsage .With (prometheus.Labels {"component" : "gpu" }).Set (cpuMetrics .GPUW )
1412+
1413+ memoryUsage .With (prometheus.Labels {"type" : "used" }).Set (float64 (memoryMetrics .Used ) / 1024 / 1024 / 1024 )
1414+ memoryUsage .With (prometheus.Labels {"type" : "total" }).Set (float64 (memoryMetrics .Total ) / 1024 / 1024 / 1024 )
1415+ memoryUsage .With (prometheus.Labels {"type" : "swap_used" }).Set (float64 (memoryMetrics .SwapUsed ) / 1024 / 1024 / 1024 )
1416+ memoryUsage .With (prometheus.Labels {"type" : "swap_total" }).Set (float64 (memoryMetrics .SwapTotal ) / 1024 / 1024 / 1024 )
12741417}
12751418
12761419func updateGPUUI (gpuMetrics GPUMetrics ) {
@@ -1299,7 +1442,10 @@ func updateGPUUI(gpuMetrics GPUMetrics) {
12991442
13001443 gpuSparkline .Data = gpuValues
13011444 gpuSparkline .MaxVal = 100 // GPU usage is 0-100%
1302- gpuSparklineGroup .Title = fmt .Sprintf ("GPU: %d%% (Avg: %.1f%%)" , gpuMetrics .Active , avgGPU )
1445+ gpuSparklineGroup .Title = fmt .Sprintf ("GPU History: %d%% (Avg: %.1f%%)" , gpuMetrics .Active , avgGPU )
1446+
1447+ gpuUsage .Set (float64 (gpuMetrics .Active ))
1448+ gpuFreqMHz .Set (float64 (gpuMetrics .FreqMHz ))
13031449}
13041450
13051451func getDiskStorage () (total , used , available string ) {
0 commit comments