From 45f2785fe214c3dc21f7343ebbb83e7091727ad5 Mon Sep 17 00:00:00 2001 From: Kobeep Date: Thu, 2 Oct 2025 21:36:55 +0200 Subject: [PATCH 1/4] Re-write code|fixed functions --- __pycache__/install.cpython-312.pyc | Bin 0 -> 18701 bytes install.py | 471 ++++++++++++++++++++++------ monitor.go | 314 ++++++++++++++++++- 3 files changed, 686 insertions(+), 99 deletions(-) create mode 100644 __pycache__/install.cpython-312.pyc diff --git a/__pycache__/install.cpython-312.pyc b/__pycache__/install.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bbeab4e60e76eaa766200a82fefc0668f5a56ed2 GIT binary patch literal 18701 zcmc(HeQ*>e|&Lq zCxatA;kiHVW!7hl76Nu7rfg+pSH66im6iEEe)*Dr3IrGeu6KU>KgJL2C5Zotei)ZW z&pi5Tk|1so95F<2Bw)xWc_17SYq$BC5CHV3>LTp3*;w!ik zo}OqmUf8<-jNnS4i{>1(%Ws7?cMerUn?A0b zuLdp-ivpfL1sBdSRDHfW>Co=eFd5)MSgO$pEW_pCkMP6jPg}5A!#vyk?h(~2X zWJB!nxFm-sC&O}7h_izcF*+qPT?tu;g=Jo1qxRD2s65Vc{1hMO_;@7AM#lNb#b|u2 zf$bAmiI)>o4Qx2hu~Jf!`4|V4WqwS=oS84t`ws-kG<7O0k4NmIp$`}FXdgViMeqbn zAOcMum^coYG)|Z>)R3EV4S6^>Ob!oB4H_nf7p4U>hcKRo>XM0ip!@eB`hLIfVWj~(smIeI$nKG}cdrL#xT zz586(K-zP%zwh+XqmX&E>-50!fxa}=+jabTh+10)kocM~b_N@4dj3lr8jpJgG!6agQKX^bOq2PqS zM}LKdf^3#?q^vJ*f|#HXY3B7a!dC9iEJMFw$vHA;=xj9axU6MkwQb}4TrJNK6TZh= z*(niLCL2ae16H}rbL==D=6EqpGD>gff5`P^ zFAUf~(4BTop~kc+hjfXUkCHq{((VY%SYEshGC}4q%MzL^tXPbS{J7Sz)-ACL9&Ld? z=?WxQi3gPT+L5;|-I%>Tdrf$I^lvWy^y2)5#n#384_klP@r#bTA(eSCMRl!G-nU-4 zapC%fYbW3C{hR)u_Rn`N7JX28yYlYo_p8XuYz7%yV8+FMd{b9*3 zD}PaWH*oJXFkPm)@U+jE@o28%8UH=V+#;U->vH4_%xc@*%sT7s>Llq1I+bGW>q&`7 zgqI{X21$5~PoRnYZ*Tt@I}oJAa>#2V9zZ!X*kUCl_7M(fBdoI#tD(3Sk!*tm4>640 zYu8^>{afaH7K#?!Kfm+in45L20b%hdiq{V)`2!@!Fo$oGsvc;~HkgMg%5_CfGz zgMgWcH9!V-0U@D`F$CwYpTBluzG$ACf92=B@AbdiztE{N&C68FpMC`Ni7*`&4%~~n+(zTsE|1hZ+?_z9Q+%O1Eb9c#7Eo?R7f>uOb0AY<_|3NEVSO)@xkufyB9%x zv@KHyv@t-7(6~%Z_+LS;-6Fiij6*zaNrwqj8yXb|V#X;tW{7w$=o_S>*IkC83dS=z zL(VwwINsN(W~mu!B8&9koXF6HvMjn1o?_EJvyoBfpiP23e;;xGsP~avgzL%u<6N8z zwOdQb8abpB=o_uOz(g*ab#Y(^%4kL7YsS>~qt1xq9Km^JT~}O#`hA*|k?h0Q^VBv? zXj<9Nuy7r{%tsP3&xToPoS&Qo3o{l&1}`>@HBmGUzb9cmV)`>dDxDJvPss_9AJ%M7 z5h=2?3!Bbq7iJ@(sc_pNBH<$jQ8Cdw5cH-Q32Z!3Kw>`a6cceEpPG!yX+{K7TZ|*~ zHtm*V4jAJ*bRb>O4YoYbozkiuMba_tlw>jOJ$gC9Paz{8l(I+fxYp!Ovt75yONmK2 zP4W_&PUw|cT_gLT1H(o&RYefeC>-$_NHfH?7i5# zvA+{ms%uw@%U4P&;s39F?vk8qy=yr{QN{hjx~0OpMRKVyxY+tZ=k3mWJ3bD56jBTO zRDa)tY>YG(9tyKy*0E?=6xG6J)!+PwRbTOaU;UD={vkoSYF2WK=0@JGoe%$PhnmYS z)9fEteMK9TtT~~=w-WOAe$Z8Tq=5LOqyy4V3ktipI6vJUIC8-GX$y(+2mFx!h6;3- zI)9Uop8TfN1!)9onwPo$@8R`tt$dC&>_(E1^%X*Zb;~*LP=*WqU3flA%#yPp52W3) zDGk6$25#trx_S=kx@}KUyZFks>N$^%(zIzgdWM`proy_*kT{v{sBB7_38YchU7YtK zOlNSptUfcgHlL?=z9+XhVXkqjE8s2oH>p)%Q5!j^709rbbAq`_eK+!Q+2|;%JJD#t zpGwN(;4X(n-tdrlPN~L}NEd4kFe~!kPeer?J7$fIjY=MPgvbpKg)Z@;1WWPEZBj(5 zPuZ#Sj!JA?kS(4p7&qtvEIy>*R7k|Rb~fnv5>3G`(Kl6U^w-+k*oqn+kB*H)Z=p~6i*PlHM<+>> zjfxu_2w*Z0WU#UnkyOA|6iA$;l&1L0Q5hSSAh=|}71DnFwTF>{O}k>@36M@M@XQ4< z=uCSITXYZbNt448>X<2i3b4c+whyV;2Lz|#Pr3sMvetQjAwiJC}UCq9?yYFvlSlZHXe@n~KmKL>g z@7?AP2k-Acy0ri3{r&w*`}@^>FMZal77Q*^XEbX8I&EHP{L%Y9%EQvaYhT>}su^-& zKS%|z4iM$K%UG*VvK(dscAFtD5faT&_)c*ybQc*f$Po0O(GGxNPR^s#fOOb^hUUB* zmI1u>7*D2_Xa+B2+CdWMK!QOlj0RGH?l+|hXlf@aPco+d^(o~&yv#>r_N8b%ahc_! zXhMx7kUCRxn|L|WBuF8V$7>@^ifQ*K&k15UO^KkVHAhpB2GZ`tNFpvL(r%6)iH758 zDlA6E)1E~9VqBPxgR-Us0JoqPxWj4Jq%aLWaei-fk{=M{UNqOSLJd07hO_^tTO z_~#`%uJx_tmd>{=tS`pzd{-)T-{qbuT;jcoQ}=ReSp`^4#m>?eMlFLt4P141`RW=O+vv|VPEM{ATu_iM0SJEka4Vnzi=`)@h z^=4rWa4^R?=4~g~u+A9@)nf zs&MgCI0AkF8yW;v9~VGSj7G&6Fbz7z??89O0hF9X9rtQ-4Inrr5E=o^ZlLF286a0c zsj%K3m1(HYXkv0QS)-KnhCw!Qwif~*zMXx{NB|bl9l)k1la{5Ry<(#vjv=GLfQQ8n z)M!<=keKAK<)I0*42hem@DXw(cA=yeB}n9@8Sr_cu&WSNcx(`eFQB3qAql$0qbQGb zyx0o~$YLbSg>;@io`#bliOo>_m>*KC*#Tn$7!lUSg?IsI6NvtQL!z4k4Jj)0puBG3 zyT7hWmG@b8fG1~_E}m!Xs=j1zir%-9Tljzi;oOy?j;sbM{$luQ@0yz^Y+5PkS;=eq z%0rd8RjO#sPtZAPe$A4n2CRs=bMu$(SMOP>-t&2Ni@J61-A=WzD@7k!t*Tdp2k-4z z3U+-~l&Tt7qARX>=JMxykrm=ES1Vdm{zr}+5EsA*pQS8!W`+X6>Ad4LAOMnhtAMh*I}DWi4ABRo(G=gD-x55@9NY7N{W%!s zyH4{l;S$e+nJM9Qt?zWePLy~l8UY)5l$`>H7Id_--4rKiJIJhIpF&6$1U17wB2TiD zz%-PAt&`9wtTDh@wQZC}o%tC}?&Pj}tkfShdSmCU$)-jY_ZUPFv~`pbNqA(EH_K~qIY^bzDBF~f#HL9h5}s@tiN>4sJ}HfM5@aK3O>z3&pfg^n zSKI{?=G!gn8xF%T$B}U*ggNmXkSJBBME+7#NSH!g+dKqM2fhcVY#k7?53%7dP&R)7 z3k@Vgg0A(Ff!5&$KtCcePC}*wxC4f%RjM^NY<$dN{mY2R-na#S(j`cAQP7&A+CUVn z(0f-(E5CL*E8M0y_=+LuTos@Q_flaa2nMy}#T4E3ka86<*XTz8RcyJ|z3MNWcP{L| zOZ|&~|C$pXfmh%yQj1#d)h*L6;7a?K>DC9RcIz_T_V}VS?VkTfJz-Irm|(UYvTPO( zcm~L;gG*vOr_mwNWrO*A5-b(@HgpSpEG?(BOZFiPvMZ0|oq81F9@ zTeeF!M8Kl7+hN%(q6|v38-*{9Hd=!ErVTbxVw78V4lo;=V0$tx*m^eFXa53TqgP_d zCcW}zw1jMMd`*-YIqPoPzoaG5Cn~IRBWE8e@K~*}vLild^A2B}kMpmyb{(_Gu69VZ zwz-=#L#?xFpL!12dXQ_33w$lB2X?F2>aEQJw$Tq z)_vL`02(+2VLxd%*4ENGrvP>@#sSL#phk?zBG0GkSok6jUsJ+{LYj)eH%Pky%Zy9| zE)yAtA`NZS42QHAzZ#$}V%jf=(J>V3gMMii1#&YqXJVZR5zwmXuPS~Y=>8u5q>n+T zBEun1&96;Sbq{=j8*SIyZXCXTc)lU!+le7SWF*)iKs!x+%2$hN4yEW$8x1%U`P;7^ zU-fu@IQ_=-53js&WiFaRh+@u-!1Vy+X?B%z|FHj!{yA@os=#n&M~XVMN-_AMe?FR` z>WyFYd=UC(XEvVlR3m1->%O^TKugzynNn18>)_3UKk2yo65@UAjbqo&rKr+1s=@`d z0D@w_0y!(~pY2OkowEFt+!C;Q)QXOKz4s2NWj!hXQPU-P3tSSYx;sTTzhLy@{u(Gx?CcCd_BCiygQH_fbRiTzOr~EB;e!2NKrms)mxN`jpbUe2n zHT@V3vaZ@wbO$oNG}LxWir)HrUlDj^e*i4Yc(_b&{Z>NT8UJl}_qOA8#6Q-R9`A5w z1Z|k+*vyk)sAm??Gq8yJ`E5*)2*ypnI8%*-y0Loav#E=x?^mJ8)$ZQ~f;mdVam|s>q7V?W zk?GthTXbq9TJ?piYYz}}A`h)7H1=R3Q>;?DuC^iD&uD&3sU>icgwgr5vzqp()7Xq7 zNxl2>2Aq_V%>EfIDD}GB%y0t%w2F>KBbvLU$=VuaS0<%~=Va$jk!495aq=+bVYR7X>j`5zMgKEIh-F*S-hVJ`H$?&NYJq zVw^W9@oiY*->@VA$V<+hTmyN*QU=Uy2%X5o0Jh&Imu#kQ(q&uYHt;v;TEA)YmY_!# z7b-Q~*c`x7zkD166gtRYpfF0+ATMKhCNX6O2J5amtl02(;lrFi3--tbriYhs_%Uqi zbn3g5W*Dns5TZzC9O;VJP^Yo&IR=r*i4nZx*)NRn{M3eVuuRt|#2j|Ud3jn8FE+|y z5zGp(#L&j9frK!xhGAc)bKOJc{GbLbU0}{6r}$1O8k?Hr859#T2yEoe>}!@e%>zye z#Q>t9B@dw>#-l+#(%HO+ISa$lDGGwj0G~J)7P*r+Ou2IcOir+5%*S~5$$ZM3*G1t4 z++UtMlI)B@^hPw40EYUTF-no$s5Ra&O->K!ax!*_Rq9y0hxTNANE4dk_h32cFt-I= z>EGTk8rNe;cmND>4Y(JM4@W3lR0@S7auiTJy=rgJfp^4lWJ=nt6)HOi^@wtP;y~aH zK5VK#1l#o~iTVPalmXJxI|4xU zc6d|qWk}MlX)y}325)d9iHf0ah@;8X`vaIY-V03ojR#o9o<*cbQDVZ~kK@@mZhlxF zdK1K&fm#$S4e0~O=mNJfMK!Ha`K8p=lWRMOqFrFpc>~ub=etv0cA;y9_TKPZ_sr#j z8TpNqU~0fja^=m{&mWp^RO??-^G>K|&n;6!t4O*w-UaE}fwN6_vOY!0)`d)+J9l2EATbreDOMS9qBo`DQybs(RUPC1fkSU2p^fWwyAE zZ1+Uqr+JwY8EuI^4LWUE8-O=E0dIy4a2;q zjv#ww7&_N`1_Q5nVQF?r%?aJL0{mHP6XtQ|*3Y3cg2s@?W|QumqL45!UjP&qN7*YI z%0Lt&j6e|^42$gqhZsgrM?aBtBa*jJrLoeb$3R6oPwO~sY<`L(<<1y$+6mXG^js`mKIy%$(w)nlT zXJ;X3Gh4`|=(F^$L5l(-WYf0h>=w5PCt9@D2CwazX>Z$}nUI(9Y}O%{8auurB%{q!d~vqzb9uM%RBkYG6OBgBx`VHlYokEYd));Pr5$89`Eq%-dM)cJ!B60v zZ==Y{nJcgdp;>)?3f_gA@-BLA-o=~pE_rU=rJM3Ddv4z4oAR!BZr+uf^4{{?ysI|l zz4f_yS8vLjeQw@0oATcF+`MZyTMKRWr6+p zkB#%@QYN?=XKt7MZJvqu?oD|&Y`}Z-5eaR^`?*J?@%cDgnl8x%o0O6haDdiuFLWRW z0tJ3N zLUt}F<|w37Aa1qB8DD%(0Zt37_ro`0W14-qPqkY3f*E^^yTd@>t`Cmc8jgE9*Em!t z4o4=#aB{)TPemt%F~x817MyK>eHwmP78ATuF?cch7&ZV_GMp!dP)qCs3J{bDk(^VC zFNA_JG^1F_KWk(SuWdltz2SE-k2*t4^QGp-=Eglr3u+k~=*DpAwz<|iTW{|(J31L2 z3o0f0_td^dx9$VfC}3fgCtN59UPI`6$ivK@L(7y&GrwUnGb2x z06Cf3>21wbRRV@5f`0SL?#x=+uaND~Hzt!{-kChSXZ!IYVduO3Zq$E7sBJv8FBFv9L?UZ7#)BvCt!NSU|>ceerFs; zRHLz4MsFf1L8FHK9)fAAyF-Wy#Rf$rrOap-rlQ0iGKcc8*(wWX;Rj*&DI7!SfHtLhmCrt zwL-lOG)6f_d;BF7U#x7izn-2C8K)6+ntY>lW(b!0{V z((Ou_wiF&mL@b8zf$*6KbFpCLL@CqpDpWO;FqR|)C@GajS3>M7QGVL+fLT1e=tP1A z>9s2@&kpJZE7CLoc~fCg;?p@@;uz+43jGw*m8ZkgHfZq}h>>3X;4^F&hBHUUDA{6b>-Np+$I`5Ch_ACN3DjjfemnEdtWS<~0O9iV7xBf{`QZY%bby z(T-!H!)_o)-MDoTMWsXg2(r&S0zt)K^@~4-=e@`SuK~9hJn%sA{XpGPpl+q;z-k$L zzwFRb*`YOX!l|!4-W)fCFcuLm-+iidi7H*8OIC6#S0IMmL%7PmCMcJCjj@#$<5JdE z>L>gK_kCNId|N*EZN1v_dynsjSAKA1m4@RID|8OtAw8U;UO--bX^JXarOH!Ovqwl2Xejuco4R=Hl>wGmI_BwaL z0S&bP!3A&t98M?Wqq%B%!<|$Pa#apb~t?g)t|re-uZXWFPu=DM^q+~ zqPWcR3+74IwyfpDCi8<~l^ID!DiJ{;SrHJ3i{T7gGCPQ<>ou^}Tf*HX)dQ?N09p{kQuU zJJkb2Dsw(XeRmy$#ygijc>VV4i!rt1oXQNPsPn(~|Dn@n%ochwfkc ze&Ji3Rdcj6Hd<-t&CaR>7YrS>&=3%v9o4W1A zUzhy%%1%c~1{bK&2Q!Q#p(IFcVij?@{nhO|z iD{Colors.ENDC} - Stream container logs") + print(f" {Colors.OKCYAN}monitor --filter 'name=nginx'{Colors.ENDC} - Filter containers") + print(f" {Colors.OKCYAN}monitor remote --host {Colors.ENDC} - Remote monitoring") + print(f" {Colors.OKCYAN}monitor events{Colors.ENDC} - Docker events") + print(f" {Colors.OKCYAN}monitor --version{Colors.ENDC} - Show version") + + if not args.no_systemd: + print(f"\n{Colors.BOLD}Systemd Service:{Colors.ENDC}") + print(f" {Colors.OKCYAN}systemctl status monitor{Colors.ENDC} - Check service status") + print(f" {Colors.OKCYAN}journalctl -u monitor -f{Colors.ENDC} - View service logs") + + print(f"\n{Colors.BOLD}Documentation:{Colors.ENDC}") + print(f" {Colors.OKCYAN}https://github.com/Kobeep/Docker_Container_monitor{Colors.ENDC}") + print() + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print(f"\n{Colors.WARNING}Installation cancelled by user{Colors.ENDC}") + sys.exit(1) + except Exception as e: + print_error(f"Unexpected error: {e}") + sys.exit(1) diff --git a/monitor.go b/monitor.go index 3ae5d93..e7ecbcd 100644 --- a/monitor.go +++ b/monitor.go @@ -5,14 +5,20 @@ import ( "context" "encoding/json" "fmt" + "io" "net/http" "os" "os/exec" + "os/signal" + "runtime" + "strconv" "strings" "sync" + "syscall" "time" "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/fatih/color" "github.com/kevinburke/ssh_config" @@ -27,15 +33,30 @@ type ServiceCheckResult struct { Status string `json:"status"` } +type ContainerStat struct { + Name string `json:"name"` + CPUPercent float64 `json:"cpu_percent"` + MemUsage uint64 `json:"mem_usage"` + MemLimit uint64 `json:"mem_limit"` + MemPercent float64 `json:"mem_percent"` + NetInput uint64 `json:"net_input"` + NetOutput uint64 `json:"net_output"` +} + func main() { app := &cli.App{ - Name: "monitor", - Usage: "Monitor Docker containers, services and events (local and remote)", + Name: "monitor", + Usage: "Monitor Docker containers, services and events (local and remote)", + Version: "1.1.0", Flags: []cli.Flag{ &cli.BoolFlag{ Name: "json", Usage: "Output in JSON format", }, + &cli.StringFlag{ + Name: "filter", + Usage: "Filter containers (e.g., 'name=nginx', 'status=running', 'label=env=prod')", + }, }, Commands: []*cli.Command{ { @@ -48,6 +69,41 @@ func main() { Usage: "Show service statuses", Action: serviceOnly, }, + { + Name: "watch", + Usage: "Continuously monitor containers with auto-refresh", + Flags: []cli.Flag{ + &cli.IntFlag{ + Name: "interval", + Usage: "Refresh interval in seconds", + Value: 2, + }, + }, + Action: watchMode, + }, + { + Name: "stats", + Usage: "Show container resource statistics (CPU, memory, network)", + Action: showStats, + }, + { + Name: "logs", + Usage: "Stream container logs", + ArgsUsage: "", + Flags: []cli.Flag{ + &cli.BoolFlag{ + Name: "follow", + Usage: "Follow log output", + Aliases: []string{"f"}, + }, + &cli.IntFlag{ + Name: "tail", + Usage: "Number of lines to show from the end", + Value: 100, + }, + }, + Action: containerLogs, + }, { Name: "remote", Usage: "Monitor remote Docker via SSH", @@ -85,19 +141,21 @@ Options: // fullStatus displays full local Docker container and service status. func fullStatus(c *cli.Context) error { useJSON := c.Bool("json") + filter := c.String("filter") if !useJSON { color.Cyan("Checking local Docker containers and services...") } - return executeLocalDockerStatus(c.Context, []string{}, useJSON) + return executeLocalDockerStatus(c.Context, []string{}, useJSON, filter) } // stateOnly displays only container states. func stateOnly(c *cli.Context) error { useJSON := c.Bool("json") + filter := c.String("filter") if !useJSON { color.Cyan("Checking local container states...") } - return executeLocalDockerStatus(c.Context, []string{"--format", "📂 {{.Names}}: 🔹 {{.Status}}"}, useJSON) + return executeLocalDockerStatus(c.Context, []string{"--format", "📂 {{.Names}}: 🔹 {{.Status}}"}, useJSON, filter) } // serviceOnly checks local service availability. @@ -184,9 +242,16 @@ func dockerEvents(c *cli.Context) error { } // executeLocalDockerStatus runs "docker ps" locally. -func executeLocalDockerStatus(ctx context.Context, args []string, useJSON bool) error { +func executeLocalDockerStatus(ctx context.Context, args []string, useJSON bool, filter string) error { + baseArgs := []string{"ps"} + + // Apply filter if provided + if filter != "" { + baseArgs = append(baseArgs, "--filter", filter) + } + if useJSON { - baseArgs := []string{"ps", "--format", "{{json .}}"} + baseArgs = append(baseArgs, "--format", "{{json .}}") cmdArgs := append(baseArgs, args...) cmd := exec.CommandContext(ctx, "docker", cmdArgs...) output, err := cmd.CombinedOutput() @@ -199,7 +264,7 @@ func executeLocalDockerStatus(ctx context.Context, args []string, useJSON bool) return nil } - baseArgs := []string{"ps", "--format", "📦 {{.Names}} | 🔹 {{.Status}} | 🔍 {{.Ports}}"} + baseArgs = append(baseArgs, "--format", "📦 {{.Names}} | 🔹 {{.Status}} | 🔍 {{.Ports}}") cmdArgs := append(baseArgs, args...) cmd := exec.CommandContext(ctx, "docker", cmdArgs...) output, err := cmd.CombinedOutput() @@ -468,3 +533,238 @@ func expandPath(path string) (string, error) { } return path, nil } + +// watchMode continuously monitors containers with auto-refresh +func watchMode(c *cli.Context) error { + interval := c.Int("interval") + if interval < 1 { + interval = 2 + } + + color.Cyan("🔄 Watch Mode - Refreshing every %d seconds (Press Ctrl+C to exit)", interval) + + // Setup signal handling for graceful exit + sigChan := make(chan os.Signal, 1) + signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) + + ticker := time.NewTicker(time.Duration(interval) * time.Second) + defer ticker.Stop() + + // Display immediately + clearScreen() + executeLocalDockerStatus(c.Context, []string{}, false, c.String("filter")) + + for { + select { + case <-ticker.C: + clearScreen() + executeLocalDockerStatus(c.Context, []string{}, false, c.String("filter")) + case <-sigChan: + color.Yellow("\n👋 Exiting watch mode...") + return nil + } + } +} + +// clearScreen clears the terminal screen +func clearScreen() { + if runtime.GOOS == "windows" { + cmd := exec.Command("cmd", "/c", "cls") + cmd.Stdout = os.Stdout + cmd.Run() + } else { + fmt.Print("\033[H\033[2J") + } +} + +// showStats displays container resource statistics +func showStats(c *cli.Context) error { + color.Cyan("📊 Fetching container statistics...") + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("Docker client error: %v", err) + } + defer cli.Close() + + containers, err := cli.ContainerList(c.Context, container.ListOptions{}) + if err != nil { + return fmt.Errorf("Failed to list containers: %v", err) + } + + if len(containers) == 0 { + color.Yellow("No running containers found!") + return nil + } + + var stats []ContainerStat + for _, cont := range containers { + resp, err := cli.ContainerStats(c.Context, cont.ID, false) + if err != nil { + color.Yellow("Warning: Could not get stats for %s", cont.Names[0]) + continue + } + + var v types.StatsJSON + if err := json.NewDecoder(resp.Body).Decode(&v); err != nil { + resp.Body.Close() + continue + } + resp.Body.Close() + + // Calculate CPU percentage + cpuPercent := calculateCPUPercent(&v) + + // Memory stats + memUsage := v.MemoryStats.Usage + memLimit := v.MemoryStats.Limit + memPercent := float64(memUsage) / float64(memLimit) * 100.0 + + // Network stats + var netInput, netOutput uint64 + for _, netStats := range v.Networks { + netInput += netStats.RxBytes + netOutput += netStats.TxBytes + } + + containerName := strings.TrimPrefix(cont.Names[0], "/") + stats = append(stats, ContainerStat{ + Name: containerName, + CPUPercent: cpuPercent, + MemUsage: memUsage, + MemLimit: memLimit, + MemPercent: memPercent, + NetInput: netInput, + NetOutput: netOutput, + }) + } + + if c.Bool("json") { + jsonData, err := json.Marshal(stats) + if err != nil { + return fmt.Errorf("JSON marshal error: %v", err) + } + fmt.Println(string(jsonData)) + } else { + printStatsTable(stats) + } + + return nil +} + +// calculateCPUPercent calculates the CPU usage percentage +func calculateCPUPercent(v *types.StatsJSON) float64 { + cpuDelta := float64(v.CPUStats.CPUUsage.TotalUsage - v.PreCPUStats.CPUUsage.TotalUsage) + systemDelta := float64(v.CPUStats.SystemUsage - v.PreCPUStats.SystemUsage) + + if systemDelta > 0.0 && cpuDelta > 0.0 { + return (cpuDelta / systemDelta) * float64(len(v.CPUStats.CPUUsage.PercpuUsage)) * 100.0 + } + return 0.0 +} + +// printStatsTable displays statistics in a formatted table +func printStatsTable(stats []ContainerStat) { + fmt.Println() + color.Cyan("┌─────────────────────────────────────────────────────────────────────────────┐") + color.Cyan("│ CONTAINER STATISTICS │") + color.Cyan("├─────────────────────────────────────────────────────────────────────────────┤") + + // Header + fmt.Printf("│ %-20s │ %8s │ %15s │ %20s │\n", "CONTAINER", "CPU %", "MEMORY", "NETWORK I/O") + color.Cyan("├─────────────────────────────────────────────────────────────────────────────┤") + + for _, s := range stats { + name := truncate(s.Name, 20) + cpuStr := fmt.Sprintf("%.2f%%", s.CPUPercent) + memStr := fmt.Sprintf("%s / %s", formatBytes(s.MemUsage), formatBytes(s.MemLimit)) + netStr := fmt.Sprintf("%s / %s", formatBytes(s.NetInput), formatBytes(s.NetOutput)) + + // Color coding based on resource usage + cpuColor := color.New(color.FgGreen) + if s.CPUPercent > 80 { + cpuColor = color.New(color.FgRed, color.Bold) + } else if s.CPUPercent > 50 { + cpuColor = color.New(color.FgYellow) + } + + memColor := color.New(color.FgGreen) + if s.MemPercent > 80 { + memColor = color.New(color.FgRed, color.Bold) + } else if s.MemPercent > 50 { + memColor = color.New(color.FgYellow) + } + + fmt.Printf("│ %-20s │ ", name) + cpuColor.Printf("%8s", cpuStr) + fmt.Printf(" │ ") + memColor.Printf("%15s", memStr) + fmt.Printf(" │ %20s │\n", netStr) + } + + color.Cyan("└─────────────────────────────────────────────────────────────────────────────┘") + fmt.Println() +} + +// formatBytes converts bytes to human-readable format +func formatBytes(bytes uint64) string { + const unit = 1024 + if bytes < unit { + return fmt.Sprintf("%d B", bytes) + } + div, exp := uint64(unit), 0 + for n := bytes / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) +} + +// truncate truncates a string to the specified length +func truncate(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen-3] + "..." +} + +// containerLogs streams logs from a container +func containerLogs(c *cli.Context) error { + if c.NArg() < 1 { + return fmt.Errorf("Container name required. Usage: monitor logs ") + } + + containerName := c.Args().Get(0) + follow := c.Bool("follow") + tailLines := strconv.Itoa(c.Int("tail")) + + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return fmt.Errorf("Docker client error: %v", err) + } + defer cli.Close() + + options := container.LogsOptions{ + ShowStdout: true, + ShowStderr: true, + Follow: follow, + Tail: tailLines, + Timestamps: true, + } + + out, err := cli.ContainerLogs(c.Context, containerName, options) + if err != nil { + return fmt.Errorf("Failed to get logs for %s: %v", containerName, err) + } + defer out.Close() + + if follow { + color.Cyan("📜 Following logs for %s (Press Ctrl+C to exit)...", containerName) + } else { + color.Cyan("📜 Logs for %s (last %s lines):", containerName, tailLines) + } + fmt.Println() + + _, err = io.Copy(os.Stdout, out) + return err +} From e145f127487627364061e86d0bac43b4c4d8936a Mon Sep 17 00:00:00 2001 From: Kobeep Date: Thu, 2 Oct 2025 21:37:48 +0200 Subject: [PATCH 2/4] Remove old pipelines --- .github/workflows/CI.yml | 81 -------------------------- .github/workflows/CICD.yml | 113 ------------------------------------- 2 files changed, 194 deletions(-) delete mode 100644 .github/workflows/CI.yml delete mode 100644 .github/workflows/CICD.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml deleted file mode 100644 index f13dc6a..0000000 --- a/.github/workflows/CI.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: CI - -on: - pull_request: - branches: [ main ] - workflow_dispatch: - -jobs: - build: - name: Build/Test - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.23' - - - name: Install Dependencies - run: go mod tidy - - - name: Build and Install Monitor - run: | - go build -o monitor - sudo mv monitor /usr/local/bin/ - chmod +x /usr/local/bin/monitor - - - name: Install Docker and Docker Compose - run: | - sudo apt-get update - for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done - sudo apt-get update - sudo apt-get install ca-certificates curl - sudo install -m 0755 -d /etc/apt/keyrings - sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc - sudo chmod a+r /etc/apt/keyrings/docker.asc - - # Add the repository to Apt sources: - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt-get update - sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - - - name: Set Up SSH Config for CI - run: | - mkdir -p ~/.ssh - echo "Host localhost" >> ~/.ssh/config - echo " HostName localhost" >> ~/.ssh/config - echo " User root" >> ~/.ssh/config - echo " IdentityFile /tmp/ci_ssh_key" >> ~/.ssh/config - chmod 600 ~/.ssh/config - ssh-keygen -t rsa -b 2048 -f /tmp/ci_ssh_key -q -N "" - cat /tmp/ci_ssh_key.pub >> ~/.ssh/authorized_keys - chmod 600 ~/.ssh/authorized_keys - ssh-keyscan localhost >> ~/.ssh/known_hosts - - - name: Start Local Docker Containers for Testing - run: docker compose -f docker-compose.yml up -d - - - name: Wait for Local Containers to Initialize - run: sleep 10 # Ensure services are fully started - - - name: Run Installation Script - run: | - python3 install.py - - - name: Verify Monitor Commands - run: | - echo "🔄 Testing monitor --service" - monitor service || { echo "❌ monitor service failed"; exit 1; } - - echo "🔄 Testing monitor --state" - monitor state || { echo "❌ monitor state failed"; exit 1; } - - - name: Stop and Clean Up Docker Containers - run: docker compose down diff --git a/.github/workflows/CICD.yml b/.github/workflows/CICD.yml deleted file mode 100644 index 95e3857..0000000 --- a/.github/workflows/CICD.yml +++ /dev/null @@ -1,113 +0,0 @@ -name: CI/CD - -on: - push: - branches: [ main ] - workflow_dispatch: - -jobs: - build: - name: Build/Test - runs-on: ubuntu-latest - - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Set up Go - uses: actions/setup-go@v4 - with: - go-version: '1.23' - - - name: Install Dependencies - run: go mod tidy - - - name: Build and Install Monitor - run: | - go build -o monitor - sudo mv monitor /usr/local/bin/ - chmod +x /usr/local/bin/monitor - - - name: Install Docker and Docker Compose - run: | - sudo apt-get update - for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done - sudo apt-get update - sudo apt-get install ca-certificates curl - sudo install -m 0755 -d /etc/apt/keyrings - sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc - sudo chmod a+r /etc/apt/keyrings/docker.asc - - # Add the repository to Apt sources: - echo \ - "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ - $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - sudo apt-get update - sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin - - - name: Set Up SSH Config for CI - run: | - mkdir -p ~/.ssh - echo "Host localhost" >> ~/.ssh/config - echo " HostName localhost" >> ~/.ssh/config - echo " User root" >> ~/.ssh/config - echo " IdentityFile /tmp/ci_ssh_key" >> ~/.ssh/config - chmod 600 ~/.ssh/config - ssh-keygen -t rsa -b 2048 -f /tmp/ci_ssh_key -q -N "" - cat /tmp/ci_ssh_key.pub >> ~/.ssh/authorized_keys - chmod 600 ~/.ssh/authorized_keys - ssh-keyscan localhost >> ~/.ssh/known_hosts - - - name: Start Local Docker Containers for Testing - run: docker compose -f docker-compose.yml up -d - - - name: Wait for Local Containers to Initialize - run: sleep 10 # Ensure services are fully started - - - name: Run Installation Script - run: | - python3 install.py - - - name: Verify Monitor Commands - run: | - echo "🔄 Testing monitor --service" - monitor service || { echo "❌ monitor service failed"; exit 1; } - - echo "🔄 Testing monitor --state" - monitor state || { echo "❌ monitor state failed"; exit 1; } - - - name: Stop and Clean Up Docker Containers - run: docker compose down - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: monitor-binary - path: /usr/local/bin/monitor - - release: - name: Create GitHub Release - needs: build - runs-on: ubuntu-latest - # Release tylko przy pushu do main - if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} - - steps: - - name: Checkout Repository - uses: actions/checkout@v3 - - - name: Download Artifact - uses: actions/download-artifact@v4 - with: - name: monitor-binary - - - name: Create GitHub Release - uses: softprops/action-gh-release@v1 - with: - tag_name: v1.0.${{ github.run_number }} - name: "Release v1.0.${{ github.run_number }}" - body: "Automated release for GO Container Monitor" - files: monitor - env: - GITHUB_TOKEN: ${{ secrets.MONITOR_TOKEN_CICD }} From 69d5c57c436ea84a93d4be906c60635834d52dfc Mon Sep 17 00:00:00 2001 From: Kobeep Date: Thu, 2 Oct 2025 21:42:57 +0200 Subject: [PATCH 3/4] Adding new pipelines --- .github/workflows/CI.yml | 61 +++++++++ .github/workflows/ci.yml | 258 +++++++++++++++++++++++++++++++++++++++ .golangci.yml | 56 +++++++++ README.md | 2 +- 4 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/CI.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .golangci.yml diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml new file mode 100644 index 0000000..8c638e0 --- /dev/null +++ b/.github/workflows/CI.yml @@ -0,0 +1,61 @@ +name: CI + +on: + push: + branches: [ main, develop, feature/* ] + pull_request: + branches: [ main, develop ] + +jobs: + build-and-test: + name: Build & Test + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + + - name: Download dependencies + run: | + go mod download + go mod verify + + - name: Build monitor binary + run: | + go build -v -o monitor monitor.go + chmod +x monitor + + - name: Verify binary + run: | + ./monitor --version + ./monitor --help + + - name: Run Go vet + run: go vet ./... + + - name: Test Python installer + run: | + python3 -m py_compile install.py + python3 install.py --help + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: monitor-binary + path: monitor + retention-days: 30 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..723170d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,258 @@ +name: CI Pipeline + +on: + push: + branches: [ main, develop, feature/* ] + pull_request: + branches: [ main, develop ] + release: + types: [ created ] + +env: + GO_VERSION: '1.22' + PYTHON_VERSION: '3.9' + +jobs: + lint: + name: Lint Code + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install golangci-lint + run: | + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.55.2 + + - name: Run golangci-lint + run: golangci-lint run --timeout=5m || true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install Python linters + run: | + pip install flake8 black pylint + + - name: Lint Python code + run: | + flake8 install.py --max-line-length=120 --ignore=E501,W503 || true + black --check install.py || true + + build: + name: Build and Test + runs-on: ubuntu-latest + needs: lint + strategy: + matrix: + os: [ubuntu-latest, ubuntu-20.04] + go: ['1.22', '1.21'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go ${{ matrix.go }} + uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + + - name: Cache Go modules + uses: actions/cache@v4 + with: + path: | + ~/.cache/go-build + ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ matrix.go }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go-${{ matrix.go }}- + + - name: Download dependencies + run: | + go mod download + go mod verify + + - name: Build binary + run: | + go build -v -o monitor monitor.go + chmod +x monitor + + - name: Check binary + run: | + ./monitor --version + ./monitor --help + + - name: Run Go vet + run: go vet ./... + + - name: Run Go fmt check + run: | + if [ "$(gofmt -s -l . | wc -l)" -gt 0 ]; then + echo "Please run 'go fmt' on your code" + gofmt -s -l . + exit 1 + fi + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 + with: + name: monitor-${{ matrix.os }}-go${{ matrix.go }} + path: monitor + retention-days: 7 + + test-install-script: + name: Test Installation Script + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Install Docker + run: | + sudo apt-get update + sudo apt-get install -y docker.io + sudo systemctl start docker + sudo systemctl enable docker + + - name: Test install.py syntax + run: python3 -m py_compile install.py + + - name: Test install.py --help + run: python3 install.py --help + + - name: Test build process (no systemd) + run: | + python3 install.py --no-systemd || true + + security-scan: + name: Security Scanning + runs-on: ubuntu-latest + needs: build + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: 'trivy-results.sarif' + + docker-build: + name: Build Docker Image (Optional) + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + run: | + cat > Dockerfile << 'EOF' + FROM golang:1.22-alpine AS builder + WORKDIR /app + COPY go.mod go.sum ./ + RUN go mod download + COPY monitor.go . + RUN go build -o monitor monitor.go + + FROM alpine:latest + RUN apk --no-cache add ca-certificates docker-cli + WORKDIR /root/ + COPY --from=builder /app/monitor . + ENTRYPOINT ["./monitor"] + EOF + + docker build -t docker-container-monitor:latest . + + release: + name: Create Release + runs-on: ubuntu-latest + needs: [build, test-install-script, security-scan] + if: github.event_name == 'release' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + + - name: Build release binaries + run: | + # Linux AMD64 + GOOS=linux GOARCH=amd64 go build -o monitor-linux-amd64 monitor.go + + # Linux ARM64 + GOOS=linux GOARCH=arm64 go build -o monitor-linux-arm64 monitor.go + + # macOS AMD64 + GOOS=darwin GOARCH=amd64 go build -o monitor-darwin-amd64 monitor.go + + # macOS ARM64 (Apple Silicon) + GOOS=darwin GOARCH=arm64 go build -o monitor-darwin-arm64 monitor.go + + # Create checksums + sha256sum monitor-* > checksums.txt + + - name: Upload release assets + uses: softprops/action-gh-release@v1 + with: + files: | + monitor-linux-amd64 + monitor-linux-arm64 + monitor-darwin-amd64 + monitor-darwin-arm64 + checksums.txt + install.py + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + notify: + name: Notify Status + runs-on: ubuntu-latest + needs: [lint, build, test-install-script, security-scan] + if: always() + + steps: + - name: Check build status + run: | + if [ "${{ needs.build.result }}" != "success" ]; then + echo "Build failed!" + exit 1 + fi + echo "All checks passed! ✅" diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..3240016 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,56 @@ +# golangci-lint configuration +# https://golangci-lint.run/usage/configuration/ + +run: + timeout: 5m + tests: false + skip-dirs: + - .github + - readme + +linters: + enable: + - errcheck # Check for unchecked errors + - gosimple # Simplify code + - govet # Reports suspicious constructs + - ineffassign # Detect ineffectual assignments + - staticcheck # Static analysis checks + - unused # Check for unused code + - gofmt # Check formatting + - goimports # Check imports + - misspell # Check spelling + - revive # Fast, configurable linter + - gosec # Security checker + +linters-settings: + errcheck: + check-type-assertions: true + check-blank: true + + govet: + check-shadowing: true + + gofmt: + simplify: true + + revive: + rules: + - name: exported + severity: warning + disabled: false + + gosec: + severity: medium + confidence: medium + +issues: + exclude-use-default: false + max-issues-per-linter: 0 + max-same-issues: 0 + + exclude-rules: + # Exclude some linters from running on tests files + - path: _test\.go + linters: + - errcheck + - gosec diff --git a/README.md b/README.md index 9185f7e..115c60f 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Docker Container Monitor 🚀 -[![CI/CD Status](https://github.com/Kobeep/Docker_Container_monitor/actions/workflows/CICD.yml/badge.svg)](https://github.com/Kobeep/Docker_Container_monitor/actions) +[![CI Status](https://github.com/Kobeep/Docker_Container_monitor/actions/workflows/CI.yml/badge.svg)](https://github.com/Kobeep/Docker_Container_monitor/actions) Docker Container Monitor is a lightweight CLI tool written in Go that monitors running Docker containers and their services. It provides real-time information about container states, checks service availability, and even supports remote monitoring via SSH. The project also includes a Python installer script for an automated setup. From 118047cd353343d28410e62f5498c09d107a9fdc Mon Sep 17 00:00:00 2001 From: Kobeep Date: Thu, 2 Oct 2025 21:52:12 +0200 Subject: [PATCH 4/4] Fix for pipelines --- .github/workflows/CI.yml | 9 +++++++-- .github/workflows/ci.yml | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8c638e0..d07a9c0 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -2,9 +2,14 @@ name: CI on: push: - branches: [ main, develop, feature/* ] + branches: + - main + - develop + - 'feature/**' pull_request: - branches: [ main, develop ] + branches: + - main + - develop jobs: build-and-test: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 723170d..434c4d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,14 @@ name: CI Pipeline on: push: - branches: [ main, develop, feature/* ] + branches: + - main + - develop + - 'feature/**' pull_request: - branches: [ main, develop ] + branches: + - main + - develop release: types: [ created ]