pax_global_header00006660000000000000000000000064134370041750014516gustar00rootroot0000000000000052 comment=b654edc1100c0b1912e694c7123746b965b570a8 pg_activity-1.5.0/000077500000000000000000000000001343700417500140435ustar00rootroot00000000000000pg_activity-1.5.0/.gitignore000066400000000000000000000000301343700417500160240ustar00rootroot00000000000000pgactivity/__pycache__/ pg_activity-1.5.0/LICENSE.txt000066400000000000000000000016731343700417500156750ustar00rootroot00000000000000Copyright (c) 2012 - 2019, Julien Tachoires Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL JULIEN TACHOIRES BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF JULIEN TACHOIRES HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. JULIEN TACHOIRES SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND JULIEN TACHOIRES HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. pg_activity-1.5.0/MANIFEST.in000066400000000000000000000000661343700417500156030ustar00rootroot00000000000000recursive-include *.py include docs/man/pg_activity.1 pg_activity-1.5.0/README.md000066400000000000000000000116351343700417500153300ustar00rootroot00000000000000[![Latest PyPI version](https://img.shields.io/pypi/v/pg_activity.svg)](https://pypi.python.org/pypi/pg_activity) pg_activity =========== Command line tool for PostgreSQL server activity monitoring. Dependencies ------------ - `python` ≥ **2.6** - `psycopg2` ≥ **2.2.1** - `psutil` ≥ **0.5.1** Installation from sources: `setuptools` ≥ **0.6.14** Installation ------------ sudo python setup.py install ### Installation with man page sudo python setup.py install --with-man Usage ----- `pg_activity` works localy or remotely. In local execution context, to obtain sufficient rights to display system informations, the system user running `pg_activity` must be the same user running postgresql server (`postgres` by default), or have more rights like `root`. Otherwise, `pg_activity` can fallback to a degraded mode without displaying system informations. On the same way, PostgreSQL user used to connect to the database must be super-user. ex: sudo -u postgres pg_activity -U postgres Options ------- pg_activity [options] Options: --version Show program's version number and exit -U USERNAME, --username=USERNAME Database user name (default: "postgres"). -p PORT, --port=PORT Database server port (default: "5432"). -h HOSTNAME, --host=HOSTNAME Database server host or socket directory (default: "localhost"). -d DBNAME, --dbname=DBNAME Database name to connect to (default: "postgres"). -C, --no-color Disable color usage. --blocksize=BLOCKSIZE Filesystem blocksize (default: 4096). --rds Enable support for AWS RDS. --output=FILEPATH Store running queries as CSV. --help Show this help message and exit. --debug Enable debug mode for traceback tracking. --no-db-size Skip total size of DB. --verbose-mode=VERBOSE_MODE Queries display mode. Values: 1-TRUNCATED, 2-FULL(default), 3-INDENTED Display options, you can exclude some columns by using them : --no-database Disable DATABASE. --no-user Disable USER. --no-client Disable CLIENT. --no-cpu Disable CPU%. --no-mem Disable MEM%. --no-read Disable READ/s. --no-write Disable WRITE/s. --no-time Disable TIME+. --no-wait Disable W. --no-app-name Disable App. Notes ----- Length of SQL query text that pg_activity reports relies on PostgreSQL parameter `track_activity_query_size`. Default value is `1024` (expressed in bytes). If your SQL query text look truncated, you should increase `track_activity_query_size`. Interactives commands --------------------- | Key | Action | |-----------|--------------------------------------------------------| | `C` | Activate/deactivate colors | | `r` | Sort by READ/s, descending | | `w` | Sort by WRITE/s, descending | | `c` | Sort by CPU%, descending | | `m` | Sort by MEM%, descending | | `t` | Sort by TIME+, descending | | `Space` | Pause on/off | | `v` | Change queries display mode: full, truncated, indented | | `UP/DOWN` | Scroll processes list | | `q` | Quit | | `+` | Increase refresh time. Maximum value : 3s | | `-` | Decrease refresh time. Minimum Value : 1s | | `F1/1` | Running queries list | | `F2/2` | Waiting queries list | | `F3/3` | Blocking queries list | | `h` | Help page | | `R` | Refresh | Navigation mode --------------- | Key | Action | |---------|-----------------------------------------------| | `UP` | Move up the cursor | | `DOWN` | Move down the cursor | | `k` | Terminate the current backend/tagged backends | | `Space` | Tag or untag the process | | `q` | Quit | | `Other` | Back to activity | Screenshot ---------- ![pg_activity screenshot](https://raw.github.com/julmon/pg_activity/master/docs/imgs/screenshot.png) pg_activity-1.5.0/docs/000077500000000000000000000000001343700417500147735ustar00rootroot00000000000000pg_activity-1.5.0/docs/imgs/000077500000000000000000000000001343700417500157325ustar00rootroot00000000000000pg_activity-1.5.0/docs/imgs/screenshot.png000066400000000000000000001273651343700417500206330ustar00rootroot00000000000000PNG  IHDRasRGBbKGD pHYs+tIME  'V/tEXtCommentCreated with GIMPW IDATx}k<ˬkw.X-Q9֨] Ǹe$c]8@" *Z>?[fʶ?^5r.,ӦJ|7V[E_^ nOi~s.o|>ߖvjӳmGV:,y5-/xשWe2۩)[s^3~8=8pc㲦+i4Z2?R)h6B6( h!sVS6ajj>}Sߕ^qTv|KƵ[2(-WyHM4cX;^ p=>S)|ʡ~*`5O= X2h͵LT[cuu6Q(M,8Heֿ3uXg^򎕣&8pqChQ"5o94?CièlIεK+j-rr0%o>-x~5o1ِ\[CCQhgIH/ pQmǸH2m!ȃ^(:klPugχü@PD+&R}JI%r]|jH=JͳdGX/)6rr~LUϜ44dW3N/r0H{f3JGOY#s?7Y8pFz_G$F軄|XHn) wMۈ%m ބ%m"Ӵjg˃.0vTרt ]I*t4Cm,{sM`XwGNHZ8pGiGъp:B9[8Rk-к-^nȆJH3_b%g^j=:7H4Lk:Do6Sc ̲㢕EE1l&#*Zxxҽhh-x=3AA@;ȴ6J-rhi=zDEA}H|]cnCb2 "=|hEiWG(u ͆VuԶr1 i/&+_J5g)6=&B~k 4ҹh}^QrXM`qJ5ژPnC|s` " +D 0͆W Y,) 2kF+#7ր5Y-g_e\6:_mo|Ŀw:{ޏ$\JQn=i+k}꛽UƵrO;~!8p2(dej5W$E^hdGUt[ϗmI~jףQk5<HKϣN,D 9mGn?D@|##^{M CK6VmnZ[=kNr}9CNIcz+3&5WK$VGY$zGҬmzYi]Hsk\K/\p$D2 cEޮ% 4=;e[H[,nbLZKEi$mW!_C~{?ӣo6oOU f>nؾFʴ@jg}O޻zȧǒ?X۳³鑿FɬjWb %Hyv/yApy" ;D 0] &"4$^(|~K>x^3 p[" p8FºRK~f"K9L?GR~k7`ŷ|>(چd-^)T9zVx;ZyyL3w]d<5ǪFkJfEҊA{t]D=W7Ǒ^vXٰ/+0xIjAItF ϛ o.Iפ\yKg ɓdL;-}_OO&G'Sfexk]o.&bV"Mڎ&;+4&G9ox~5{4+i(4ZFƺ),rKH o{wx=W; WX^K$WV0"}58H" MD,;`͆ hš԰d-ZXh;l+iy߇gE1gOpG6 .?՟,^pŵ3ˏY2d6Y^Z6mQc< W3 iAÈD[?H4 DaXi0`iLoc~=hگCQ@7XHЏt~9G*vQxL4\@`GP~4Ē;8լ﷕-甝D06[|tSoiUmi.FA802 0 TS^rY?sDkwP;, 4Ǵ aM4O/k?ڽ3)yJo=+::q;+}V{{]_7 WS|H@ _.U5[}-ύjrLMguMSBb')OއFqEm{z|^ٟڵk T{,yw֞Dz 䥼u<8-9~BS5,Hi!OK{ȼfVҩO$,=KʫA=Gtj~ypZ~eTx7}z_׶g'5VYr'ëɁ?sӦtM20 ,24Fj7֤#e*5!N͚ZNo{0MN-=0D_e׎uiJQUk~K_uѲm rD;OYtD;6Hgm6sɤj"}-NJJ;#j?5{f)uM2_=o"-ik\PmB}ݒrdX^/{d3=ZʋvJڼقDKZe)pDgkF/En,YSQkyN%R!PYn-߶ʲ揖>.(?yt.OwG3z{v/D5,O6fф _0 `(r\-%C5F9>?u-=gY.KyM[*/e|WVNҟW|iL7f.X?JM<2ҎvO}IW%7Kn&+|md}RzޛdkT#jZ.*g5jbtZ)ZD>lO[Gߚ46R@=|tk L˧uiis$X*b- =@Ҵ]D.<#֚иz!% ^Dr@XwSVڞ:h|43c}Dϑ3{}FYQ//h%Q$yٮXKl9kGlO[GߚnNYzJ-#7ȡ?irhl.r9-v$z7&yGݟ^4)NjYHnb*?[3h FZ}#g~<6˻K'c-3\Wo-ȴD1C;g<`[v~e5VCۏ{]6=&a߉tkD[e՛-?)J$qm{K׆.s n Evrz=1S?[=ꍍ=f R5K=r?[כkߌg}^|_QVڦ/kGMO>5eLm KZ=ZM>\dmFz.m3UUZgD׸O!Qfk{`#&ĺrc/Fui&]"Lm"Zy\~n\2)䠯j]pt󰱃*Z&C ~9D! - m4?ِQo,Ծ ȶ~0;rS;%ObQ8}qu)CO ,+VZkƍVo1.'z?:ܕ (d]ϩ~Vz}^m:>OJoeiU.;UVoהcmwTGP:NhZ`9|]wԼ{gx+]6lyy&B,_Ͻyq5ȒsQr`k@K~ZYI R^/yK_D.n= #'מ0ږTcA=~o ދY۽o\2)1֚6QcLVOݗ|5/(Qr٦WCJ5 ]d9CKARs)yLͿ"m&KG-(]6=m6/ˀv[p*_"fʒ:j5];s.(|m@cyR|K rL^uz#aAU.?nckGg[XPK蹞p$MC#4r@X) omOAs}Dϑ3{}FY|_"m%mko=[<ַKaҥ3[Ι/gy*QjqU9g~HtIsM" S,M($k_s~sˉ0X`MI IDATV9mIcRgz{#{>?2㿷j<0r>UV9?k5W|su!m!<$f#}XS٦Ȱnci٤eiZe#vR;#ϑ_܍gDsOe9-t+flFV|b]l9MuEGSwi&ݲ~gʡNi晟iSk>|o"3TX d7ZfYW,'pIsVUֹˁWyN~Sirzm[~|L[%m}Yӣ)}7WT{kiP;[3zP:Nh W/#D5EyLM;4m2aq5ڇH8_r"o>X(KXNpAXkR /\~||aT[GSNvnAg5q$ZC%dDW-S+hg8K(Yl+!xCk\;$Ȱ#=W$=$sk#I*j J-Pl4wouZ3lrr%jM]$vtgV78m%QHƹ6UkggEn~)F3<9ŖϵPnm.r9-vXlkOAISsQz^'?r#ɳN1z71egF5)ʄ?3Wv~Ͻ;"jVb/^|C%[< +-ȴÇob BXA>+VeWM#-m"G<烈C*fGgpz=zש/;!|GöiGm\FO 5m9MaI͕ '{)og6dmջ}1[M0 Yls6E'ŖӤERLPvjҵ/”mi|"HeCkmּ 9Cڳ]pt󰱃*Z>]B>/A{&/f p.Ck'@@[iX g!坃 =hٶһbGFD;#rS<Z폴Dd%dsJp<edy0g!^k@kVaT@ >Dz+T9zdb[}<--?+5弝弅r,G3zP:Nh W/#c;j޽34nrӦ_2<4eF!>msJϺ.52OO9@hCl G@2p5b◺ErAXkR /\~||a?Ɍ^>gs}7wB1bjl}IW%7KmxE9dk_*l`8( "\ RKezoɴm+C;NghrF7UDdO[GߚnǨ$Bg2J-#7A4N#\($Mr9-vWqQz^T9Eijc6ibMLYgxk{/SDo|=Gvg9x8QrD0jo=[<mynAq]_G<`[v~e5VCۏ44@%{fM" R/˪7[~ƄMO}IfZ2ѯ$rrzԩ-|)yDϧQS[Fϸi#Z3pڳ>S_w|KZid|^Ҩ d&֔=3E)2,i[;h}Z6imMQȬFi yP#LRk/! 5.H~-FMjQu4i?j&Թt0,R>kryR#Y*o6nrl"3\HNDH+H+!@`fC; .& Vz[ȈH=u{p5uo-̞{km*,y%Y 5Bs[?t,ʉgz;>TXk d7Z5W-'dcewur\KvOBrx Qk]({ IT`LNR/}qCX/?-Q$ 0˺@=GuF]9 kRi/z/&Di/Q哤8~ז4 WAXY<{RB@ ,5oR.T>Ht緑j&mv79N A 1}mIƏq+ϚryC{Y^$yA|Oc W,^"+p;tMv[dXϑ+ѹ-dzBN[m[ @e|nO(B2YM,o֚o2KbGHwk4>ֽ95#9aQ2̸/>t荙ϱ&OVl#Wv~Ͻ;"RܻnK7~^bp-3`.yfp2Mcn_9w UnG{E9i놴!q${fM" R/>UW|mZ1\ ђgqIqQE94j|j=m6Pk恑,7JqG9DoI`XmQlkR2]&#\zr"Q~kJ\j''t/fy^M2͑)=*b̑t o0Aڤ5X[NKm2AkکI׾S6؛#ݗUyR#Y*Z.D8gyvy .! V{5فfetyH4tp" +Dvl6m7TZ}mmwŎSjnۼuF;x'מH~fV[huOyxvzY6s_9E?klM\z}^m:>z+F9:{d{5]S%oҹ,ϋkH[m_"\Kl9 -30=F2܆L[{)/;sXA>+VeWM:#~H=&%{fM" Rog4Fc̍e!.pKR*Z<&uq |>G<ӌyA*#b>Cyܺ\f<[*Gr&֗iGK6JtMkhxSdXbsBɍzvG5+i0s|"f}kKO ]_?߭rHq~CIԢbi"uZGCZɺ"̜/fm6sɤbĥYSu~hgԜFݗ|5/(Qr٦W,^ĵKEOR%Ӝ:PRzޛdӴǯx:1o狺e>"kK97e$Ҕo5UohG?4j'gf-_XgNAa^6ͅRDX*b-l ]HW>5ESAdE14^&c[Sɐ&zcs;+$g۝`Uip/Ngc-3)j$"^ 2x+5NW^!Ya-;TjV*6*qhHs )H̚D~'}j ZH4You,ɧd,(?Uf"&.p)+BGtG1S?[=qzQ0s6D#c3YEVY庣_QQs5mQ{Q d&֔=3E)2,i[ػ/4oYiT*L$^-FC˳A)wvT kk6E'ŖӤERLPvj-)M|%oK^͚W!1JBs lw>]B>/0蝚9l=p"]^; "" ܆H ? )o\o_eJG7"RO=FƲRzMfv*}5BstSHlg;; AJ9LFK_."^stݕֹ+ɡiT+ؽ֎]w~g4RzǤIBSAkkEFiV6DK[X= Z]o5Z4μ d R^/yKD'y& (B}t}z'.=P)WY*czs%_ J \TcWj旊0S ' .I)ϥ27d򶕡nUpg;_TChS ̬%ȍ%K~ܚ?t>Ϲ`SYzJ-#7A^QSn.HDi[w"[+JngԽPI+ST^Qhr@XwSVڞ:h|43c}Dϑ3{}FYu_)$4H"ֵַk-g; /;s\|VrկWjrۏ4g\@;=&a߉tkD[(K>Qcg[3 Gϥsekz<&uq |>Gm ui<'!6Rff}3y[zֻmӎz`Ik%4wLsĝAӲIlnljEf6Jlfu_qe}6Ht}{ҥzok6E'Ŷտ<j \NMED)M|d;AWkv!9 ˫hmVt Hܜ;J?@g;qm*Y):oD~jK(Y?v˜}E9ETDZF Qf .I)ϥ2725{"1ym4jW! X(r}ݒ:j5:QeIܳDi#7 ,{oŗ:'E $$9-vXN&2w iɎYw|Zϟ"Ki>|OlGcz hѲ>z_p"﬐؞{owETjK//4G~xuZb˙g\]S4`/Qty2!=~1ܡYa-;TjV*#-I$$Թ&a߉tZ?k|%dfGKO7a"R~)y6;IqQE9q9Iep,ȡ6sg}^:_QQEzיvԤm#-y$kI#]/ӥqw_hG\dVmgvZaxSksy6Ht}ϥq6@pe'Ŗ:j&Թt0,R>kr_#֋;ˡGvl"3[?eK("u.T;'{˷q)R+ҭDY!cR~{#jF巖]#CCf+<[*er&umQ<FO 5)MaIݲ}$|J;GzϥvJٚYKkl ]_9*r%2.86E'Ŷ5N1m3bM&sM;5ajY|"龬rh͓YK8u.D8gy?}Z%" avv * .nWVnC`͆ . 塣R|mw#]3ii ⵽ږv?3-ťIOyx VYW @2-n3 y2{?:ܕWkPp#wDݝ0. *ͽIFL<{(Ta(Cr?;S\4s ClgK8Dt?f8֏.WC/M;8Rl!˃IԊ=hid3ﭷl&߮~)@wafe _Z(ѭ d R^/yKD}~Iuޛ&Gr5yOczI|$zvl3LJ4~[ӔcQlC%Jn=2g_Q#xrj3&' .I)ϥ27dx%>pԄdzrj5Σ4ZQ5LE?tvYd?+[˷ 9И8d[IVحw ;@F]!9޴hMeHSyLr@XoM~$&hѲ>z_p=>,bHrT_H[IN&ֵַk-gdޕ(yΔd-^ߣxio~k6R5YYoZeVE9DoIέTmӎz`mQI#]/ӥqwlҤQ~)>oF*ψ.!]lUEO nk?6e:IzQ@m2AkکI׾S"c}i/[5r>qzg9DSn"3H+mζ%ζam5g?_gglw9eݴGY= [{]>u]3GϛF~G̫\>Քo]g Ιnp6v]^d`q7 ;ZҿX{b.#͓3&MnIx欓Z#+IcmgpCY62wpa5c(ؚ׵lYi纮L(x 'W5=:3|tGMLkɑmS& {Q_2ן/F/uΚ8H4-_ _,j'Lk33jaM.|>h,z-D{dyħ[k8Ϩ7rKVkGn"?XLeU%7몥l$%)jhR\^/{lehZggF "nI=LoQ$Y/ܧjM|8fSkogc2Ws8-/RhE4|ɘZHK U.?n[ BȸaݻGKi.C#vs!YHʹҌ!rY">[c?3^,"\;o[OfJ/c-3zcECh4˷Uӕa<Iweڨ5?ZΫEm~wӼ쾑5Q#N|F훔?DSxV'͒|]z+jqm,I&?qiI1#ND5{PƳ8yleD<9P_f)GVGmQ.mk|tM){flSdXbwd|SJS2)7rR;OtklY\Zsy6Ht}*"'qMS^2V"ER4j{9yyj[iOv>MRh{=o"3,+k$~b͵; Q$HW\?v_m? sgD= ;3K.?e՛=yjgƗ|]=nE^\:Gx<"яI?D,tZ㬱k5 *=ܬZ2}Q}kt_߈{FmQE׸2]Rt)?GW`u#Hzm6NNR-ϝu 5."rdZ~;bZZ|u/M}3oZzOeO?Kmz\>|nUUyzc\u OK;nk&j Fh`_v.rVvDV+^>ll6s 7T‰gyŏt=t$7cs5I}_\<uo~öoI/'V_z:^Z}P;Xi]׺"#^ڳc.fyH!Zh. ڪ7RώRzX<~D|u_wjK˚ sdy@DBkK嗂pWX,A8"gs v(yZ?-&Ŭycimf;[{15k|es_Q_4RBմziJ\shH!¹幔"ʽ ɖ~J4ѩ^O4n\OB&&\iؿu:ݓy#-\gN"2Wtj\9s<~B)Dl'$Ho4pfmy;-O*JS<5Ӗ{jnHLi]bz x\qU*OCMB`zvoJ/Х$fBM#G/EܞL[&6Jt<X+. j^Ȝ&=&a߉tןYZvQQ/Yhs箱kkZ oc׫GԩsNĸg1id1"rzkɔO)ݨz#}lg۴%]uHy9zd&%R~I(e?0ZV;9{3Z<=v_G-wS\:7#2y֭GF=4F}g>&MzR\Fk(sH{_-8x^߯׫AV^7(U>{}>_3fbY[]ܐl[`^ e_@ 7`5lG`͆wz`^ۄ89\xHװu)g&RĮ{kE.ӄ+ޗq$;>.%]M{Y`BPsom,[Fi3۟JJ{{%QzՙOhM(pɾkkv fpg-5ݬY]YGp oyp}ݭޫ//kR<1q~KA^R >@GuĄ[BxrSbv;gSl1 yDSg^ʗ;Q>{+ʄLӞVЪ>qz5>(Aiɟ&a>=;PSU ,QͿ*_=dkFjE-w_ZsZ8a M>W7YS5gF@hG@EWtj\9s<~B)D6NUHoXhSY$z/IQ1m4&+my0_ε:f~ͪIH3˘/w{%Q$y.5'4K&+ee9[^ ;]3ø:1iZAvkj~FgD= ;VG> YfowD#ߥkm |+l0ԔA:urQ ]8kS)&sߌOk}XY=3 q\vM;j_ҥ׎l642]:.dZZ{;!eПQF[-zP[}C(+Gk\~*M?9PKr[ :wTzG?y=gϳ=YJz{x{_-86mw|,Wj^7(U?;ŕ#߳]]vz7raScO9wTGP~(ڈ`M6siOtݑr#y+6,6z=h*D$ˑ2|Ko65Oi_toj@={ZzW9Y/u-5D ,5oR.THtD&Hҵ^j|fSj򟬟Sbvl3o)MDŽJ;>#j?5Ʃ{f),zL\(wj3Vsv $<+Hlїy^4fVQ5?un~kUCDžl ,WJoAr5zX'Ҥm\sa%\~N*RQgn*Lm} (0ibMLP-/Rp%Dϑ3{}FY:H[m'LZZ{%y/$:FM*˳56z<`[v~e5F ~9d1=&a߉tkDݩKB]Qa*M?tӷ66`ԩ-|7ѷ9f7zW9k5W|kC6m%SΟ@kbM3Sd"Òe֧eii.4o#Hdsŵ%Okk ]_Soh BO4+/,5N<הÝkکI׾S"1Ke5OZf}.r\.D8gyvy&CSH(x@! - m4?WelHy87 j6RA\'m+-hwtV g(Gdl)5zv:j צSNmY)OʯGQϜmtv<5t=L@3X"fx-fo"ZN mhmT>%*=\0TKPR =nFkkWݗZکy!AVUnNWz6܋슲Ѧ=E4@?Y q  R\8ֈ I2q|J]Gn^H[L ,5oR.TYL=F3~#KNiGM2{'i%4wLsĽ v EwBʠ1EKS;)k ]_\߽+R~*0M?3g\kqdS:osQybk}Qo}Tˮ:HkΖw˴rz5ӹ|8ܪޫűgAvfL1k t=K25Wa;" +Dvl6pP9T^7y?%,QN:g3^m)blJF@g)'~ʻ'&mZ;9`BPsom,ys_ EBz[A^AT'kӹrۜK$cM1rfnƱn9ڟu_uh+y R۰hwd3z=uNl躣ֱ>ënK\M;,ZُtD[BF>_t{ޮ6p>8\Mou-=0pf^j$a)i~K嗂pW]F.VEʮ#XCG}YuDKI7f Sޛ^gM9V5ZϾGn=Rr|/.Y|d$ )幔^^mFDiG9ZOů&T߶έoMw.(|mcyR|K rY_@__Q0L[*bʯԫgىeo qVq|"-ɴ ?;_bz h(f>Z}|!C+Ѯo"͑mko=[<ꚢ;կt1_ wh<`[v~e5V[ǡ#-k.H̚D~'}j zdlx]%~)̯/V<[ gg{-cR~{c8-f,mSmawv3#f-\ǵ+!|Ktnԙvm#N'j%4wLsĝ};Io˷lV6JlO\ZVklWH0Gk\~*Mkx+mٔxQXI-j>.&/Fd:״S}6E|-r<_.?Nif}.rCi?@]v!9 F>]B>/၄_G p"]^; "" ܆H ? )oFAG4^^6/VzG[NsQ(9p[S-4ߚ]kyNowwmmz UTVQ|ƜԒ}-gN6Ȇ3ӕuM@6;hY|DNxͥP^z9k2\fAkrPώRzo{z 7tj#zTL_^#$GG~/MGsk>]hӎR<ȟ4Kg/os.}(ܵ\8~~w }k.>8uۏPZ`pAXkR /\~U;v´y혷X=r+FKU%e$rݤҿX,`O:!¥ )幔^=?ق[*f?r^uݶ/~5ɲ$b]k}krWDmkxzjT!]dBeq1j!G tW _acmԚ-g+9:GZ"ϟ;=&a߉toֈ}G]6hSjFxXwӷ6- R9$1S?7zPƳ8z;՞#ZsT>wYR?=G+\dnuui!i7yMʙt^m1&zm';y)S'K]/k3z=uj叜fAv19]SѦQATE9|oe;` 'r$?;=A0>D`%>KȆBJa)i~K嗂pW]F.VLfVgiΡ$zVV}z'.OzlY>ksD|폒^oDaM%-ryb)'PRzޛw{{mYD2+ -oQjik"a#}IKTr}ؚ:淶_:ldo}hcf r^|sRĘx8Vحw !;?Y@z"kewN&R9]2s[ }לm?ҋa.KyM[*/jN;-ZR3ig{5\Ϡ }mVSSzd9i5#Z`d9/ɿ^eZr55뎒o"-iku@:_2?CKARs)yL{7;5ތu ƮZ-F)f$s5HӬY~k~ܤG?)+b P"42_VP[0L[*b- =dg%QҚh ?j4Z&GA=4L,!7x+BnM^Fˋjo(p+OGg&YN$=XKl9+oixzSTvG>Gɴ՘.dEfw+D].|Qk~#Xq*GZ"!3kFHVi8D/D]C=}kc[ߺ>/I7FeؘK2yԟcgc-#ȡX'h{i4Xu5i~>HsӉlI#]/ӥqwdo,[_o ![62m s$o{8Υ67Xkm5"?YIύ +{Eu=\4Z"JmBǢIS=hʪj6gy?=9[S_Yj~G h]܆H ? )/\E\eJh=ڧj"UDgv6~m#Ggg~-$DU+-o jeqf_`g"GWگZ7y˥O8^ڶ~*˚?Z.q폺/kY򏾯o~)Kmo9Gux|;jΩ3z>ؽ׏]wԼ{gx9X׵OM;4Zf.\68̛Ydhtpz%mF[;Sڣ̓ȣҞ P,|@K3/DpAXkR /\~U Y{K&UQ\3je@ g?,zL\(Qߛ—0Su+HJy.?z߉wrr5j"Фm z,GR%ȍ%K~ܚ?trs?kdDi=_)Z?x9ZѮaqHk6r9-v`SJ7^ordbtj/&3=e h f>Z}#g~,/^a4-ZZ{%yQY> ! Yޭ4&.CW^a ܲG+iUz#ml0L$=&a߉tZ%Gu]6YgwO#eOVn0\D? yGm uYA86R5yFVY庬_QV}/mӎFIk5YtM){flSdXbw_h2I`˷d;kG}.s䦱E\A`D׸T8*HzvSMHԢbiҬ(B>rsM;5ajY|"O龬rh͓yq 9O'lgǙnp6O8f.!0wx /q"]^; "" ܆H ? )e8:͓>ٶ;b)iemDgdmiiT(oKyPڴF:"ʨxAJZc7ZRСTW޻#]Fl]msMJ7yߛ>x^߯k3cΣMD9Qz۪ǵ?[Z }^o~)KVQgAz[;9͔<ϫ?x79h$iFY`t7J3X M8.r{|J[5$:[=|?u~-_ _uX2AYhá>U幼gPz%zKIpjo=[<ꚢQwz)bsfyҕmw!!q-RV/.r9?~#-뉐5Q#Npo-hvy nɝQ}i6rmZ IDAT2jm |+[7z6!F1S?[=|&:Ɯy_0 e)VY:_QV~3IB>IsM){flSdXbw_h$-Uo۪#OstAk\~*M?$__&k•1X[N1OKBu_>_Z֚{\HNDH+H+!@`fC.J}~3ٶ;b[{#"%&ɦQ!S|K{oGiT(t;SH`B`~h6G\9{}9dKkw]5P,g sWbn+x]Ahb d)nr=qXKr${1 ]g~5f* /ӞV7rbkF&Σ5^)eiǂX9dq±{Γ=G(d !)H_`\rlSLȹ ,5oR.TE;aZYháTwt5\iH"=喘&-~ttK8Ϩ7rKVkGny8?XLeU%VP8K?QE`\t2_v .I)ϥ27?\;.:3 =/fT[ x|֤[טE^"[[䦙zvrr^S/R"[B{Zr4fSX*b- =`'IjԽXN}92MmҀz3m4c#{FHsӉlI#]/ӥqwd|SJS3\IBZ~Gc#t-׳Ak\zS(`7COӤG95ιƋx̚7"׺MYӞ`}.t-6!zc{ӯ(o"3so"ʉڟu_e9\]= w7%T(yq-9wFZ wSyLM;4Zf.\[Y2F8CjƕPv .m ښߚ~3AJpu/<ܓX4i#m!$ R^/yKYcbE55#=|{[haxֆc|&hgAW {f),zL\(=jRgp)HJy.?wwAGf{v4[hk hKWMq5~=8Hmk~ܚ?tps?kDi=_)Z?x9F5Қͅa%\~N*/UJhg֛%O};PBq{YEĔU~jmW>7ok' vg9PX^4%7ZZ{%yQ`Y¼]1w Un ~ߌeCɞY0DO-_ qo+H>K"u4axit+2:O"ْF^K9cLa|ƕzm6hBKi5Ԛ?gD׸^:(iZ|b]l1Kݗ~Km2AkکI׾S"1JeCk\2\Yq7$+||p_$zFqh<\HNDH+H+!@`fC;GѰL/pN@أEDJZQ6" y_oI6R9BuDQ+hz63E#DO2GY?vWׯZIox9*gz_׶g'5VYrǵӚ%z)9dsE7%۶Q;#^OhS}g|{+='yjrO3iiL/95Xdh4s7"<$:ٚߚ%\95F޵Եh$rpAXkR /\~U-Km '|72͖gɭwA{1k K&%&Rx֚TY>GrYL%.?x[+a×0{#&F<~KDKԹ5T<ʒyKI,3E 9$"0N#\8D7?n[ {ۉQu/gLےq6ibMLYgxk{zmhYY~[k/8s{owEՖu+&J!µĖ3ϸH ` xz5 饞3(R1!yu0X喝?_YM;*#m",7r!3kFH%)[B)esQΫHwsKRoMh<&uq |>GD;DMYmW6 5QL_IJ:OvEX8{Fil/%NT:˞ϯ|M(J3M57K{ӽ3w˚|,wڏj=nA@7b}Nm3ˆ x_Ů/)ou-CmŴBG.b xfZcy{|'g۝o!Os>[q"[4ك[Ϫb,v+HYڭwvu8Q#-EH3[v=;k@z/jD.IgfFDFRr#ՒߛQ"+>O^?w24u,-#Y3-S++3'+#gK{ݵ#ȑXj;\XKb$Y?MϙZlYmw窞2-`%; /.-+<>tI? o{lqi&t]z^rzZҭ/¥Ed_,Wj7}b=nu+yIR >]bؾgƫ΀` D4B&^! ?0? K6-q WzG_1{46 H̞;U_۾^_hf) BZ~9(:BYhIfŻ_ dwAk?k8R$SQ\jzwJyV={%%N"UIhb;2q³! RxY/"QÞ\ji\ a-ݒGц"?yp ;Ի-J19Yͣ3у&ml[朥x6aIY_Eʯ0lqwp0ETn65t+S; ˽ w\$WR\eoʥտ痮KZ.pvxq+K3ͥ 'smm忎wz{gBWܔΫ~k^D6^9,uS9 z+<;g~kڕ׵T;F=w`0-FD5K | Q,?E3Dt^KGƚ˓4K~o l\EXٽ~O :nK -~~֌lJevڑGHvb;yv[Z~IKػ4=gje#|"-޾Sžu˵}*"ߓGTKaεEjQwesaK9zZҭ/¥Ed_,W/S)v{/yIR Vc0}o$rzZu3iyCi7C)]Z:zGشLKd^ v,6\b)ۿEԳVҳ_.-jQP)=v}<]Wtl&)Zx~7?4z 3wS@a{]cj>{ *oPfOz{ːy9)wǰY} ~uD;DoqڑǒNvb;+ ӹ.3vI k53 زo -֬;+칮!S1)'=,AA[՟x[i&t]z^rzZҭ/¥Ed_,W/SyKv<$ ) mO؇*rb?߾E93#chtM"Z1B`~`?s \+/ҽwnioxw SUp_S/#5PKogF!ʰNmPkvh %-f~wU;3nfi(x7p5wuٞp!\*Rt߶sE~SRz$Ɵqtgƨ vCv(Qvo@>1..gxPbd[ٛߛnTN^-!X-z蝕5΅$;kioKM^ZZ7cџZEt>g6(fsXfXR8od=v[{>h:4Jywm忎wz]l٭e=<9: 4da/ O57K~o{SY7d~͒W^3L󩤳D"oQ4;fϖz/e{ ^AQ:4fˮB~'듼4kTjGIDATG1k%rSF\-1--{%iaԎ»kGK:ڑGIL:i_y{/ީŜ"K}~k%.ܗOEDǤDE mi3yIR5".nls]pB:=v4B~,6,EI Wz89xQ̎4V|qbT=GmSx}ҭsگ;}-,<+Z{?Z+~{ߥ\#2G8N.oXoس(RάN5\tǘ6XO/߅KZEVzy)X"<Yx0RTZ{1vjL;xM)!$fDvo$.VvIK-|Mh>ٛV:n&zfM朹ph-&&,1ߖkHѯ۳-ұ^`ȯw@[DMϟK9-⼑m{ߣQvxz"U|I&V7II_Ww6)5]9HK w0j@Dےbﭿ{zz?y[Ǘsi7 m'ڭmFںp U)4]+Ǎ_> #N!^ZDA`VҎm]ԫ[;]2{׳ fUp{ܙvw:&{_w:gVrπI"+[vJK_ÞveH;=.vXMH{???;3[v=;nq筴1:ooio(e;$-{%i5Qݟ?ϏswiiZYil?Q[;kGh0ڑڌtzZu;1>ߢ"qu=坍`fu+0ts^<\%N"U?+UCR;z$F'' ҵMl%"j㗢(~?+nI˳E@o=:fg̅$-m’m)ɋ_+A#;џ|&n6W{ϣNhkPy"YO+]|((Qvz>h/puJHtԵMRߵ4n~B﵋-0v zZi,Q[%MŒ?to?\*KIstob7 ,$.یyWJYZm2{1ʮ[:Rt#v7eHa>җh΅uɟ$KbXŮc>IŖx@6Y E"+/*VmZTluy79]&J[iIE{=v_)OZ/=';^{aE§KuypP.ܤ!@D k + i4Bi4B! i4B! 4B! @HB! @H ! @H @H i@H i4 i4Bi4B! i4B! 4B! @HB! @H ! @H @H i@Hv? |> s=0# z^9?'?^ϛ|N6$]ӶVj'~&o'i3w],@wNwmzlmx%l!H_>^Zْv2iE xg?nϧg^o?#wk#gBVzpߵmxF;Y=xӱ_~ , ެ@JI3Z~kb_BJi%?{EqnO~[z%Wo{99lgO[Ɂ|=!ZR[4&  k;u֙CGޒJ6ʏN+Nfj~dK.tEDuƗo?9y.K;߃ i{Jvo 2Cw?Fs3 ɐv" !-> cDI߯ L=vG$cAHakYN= ,N/;l2ꚼi79GϢit yDʉܧM"±km$xPO3&[h^kv zj;9Yma K=Hea7>yT,_*g8E1R|izk{md8+e1i}Jh?+Dod7='>G>ɕ]~o2AX?Sr3vB}߻.|{,[3 y?7 h'@]e܌4AޖIENDB`pg_activity-1.5.0/docs/imgs/screenshot2.png000066400000000000000000000651031343700417500207040ustar00rootroot00000000000000PNG  IHDR$csRGBbKGD pHYs+tIME +mtEXtCommentCreated with GIMPW IDATxymYΛǞY=<  lmd^qq`` #j zZ=ܯo{suծڻ>Z9ԭ]W_U}UU\tkg/Of~%[mˎe8BntuY_ɩS瞕3g6E׾v?.ٽgw {9qɟy EDnʁe6lmF.""RƓپ}d] @O9~|[n*<|+[Y݊a!'Oe]N.d¶DD g7^&~g_ 6B#%^r auuUe8"[n[__3uPl1O"gЍ%_~p)9viڹj3y/;vpT_gEDg3/=O/#Oʚ!Ln*g{'N+[<̳~yٽ{^Mov*wyK;\q5rzRz>yKoUA/6Y_ʅ{K&9LT^]d8*"3y]d۶2:}JN:-;vl7 G/voE^v:'>#\#۷[J=_[ ޿rK-[""G?'yͫ( &v)` gVS""uV 2duuUDDm&N=e˖-zɓduuUfرCvQ'۷o];wn9{I16@x䑩<_2#9u$^"]"/ȯēOˎ/^뮒 y_)y_#) ={erwLVVVϓ'O=|YDD^[_OYٹsٳ[ ʙkW$[MysnygK/g{^[C7ٳge:;vLv)^{Ӽ #ye5GJ~_V^"?^&g駟9s~\zɹ׿fyn'N_kZL{Z+uP.|ycr_;G~CCSy衇7_}3/~wǞTO_zJ3C;\qMA6ɧnk7l|cȇ?ٱcUrϽyMpvR'N-*^XhcǞm(""/Dow .h$>/ 猿ogM;`\~ŵ{x7urC?"w}>Y=sFá||zDn~ ;d}}]r_|/uf5];wʽ?,gg]vx\w _J=mWtH?.7o|vǗcϽE6SO}/@N'NE^(yeॲw^*>x|*^"_y1AWo5L]__7|'>ߪ}e+Wog$?/\|n?O<)\~ŵo~o۾`vnp]G<*sG^v)W]yP%~;'?#ry/;w8\}e[ nF.zwy2˵W_!go'O++/_qGn/~AyGeY|e(~lV]<ؓr'Ow2}qwʕWp43gduuU!,""ߕwYg6Mfً 7:pPVWWUi:7xPLήUUBb ̌a{W.Lٽ!4O<-+/L>mݻgYO8{v˅] ? qڎϖ`ąȳ>'[[ {wge/;o7urϽȮ]ɓIBΜ9#۷oݻvw}Yn*go ]𫲶.o\^ꗿ 7 SOofr''?E/>iY[[cO<+?'NH!;vlJo߁Vٳw׈_}# zl$۷oG{Rqyx|ӍO~^#%$/2|ߓ?.{kJMܶm<r+]o5Wk^2gN:-"";wno~d8"~K` ~K1(ګ.`0`좋.+V.׽rr_?Oȝw+^r{뮖'<&[̙3k .`ĉS}6yko׿fC%/g}N>şI:ʯE[B>ٿo| /SWȎ;6ϗ/sd֭r)ٵk/Oɽ<";o];[erI_M9tlٲEf>}Z3gdhqp{ngOI*l` =VQ G(]^ġo'|J=ٿo<l߾]^vPvڕ4._>Y[;+^zۻбcL\iBj^r5޷=g47, @<}B9+++&h14=N?4^TCxLǦg>ĔŸS? b*)rߦ<70C|N6?#VX6zm5eIfeee ױԧ=Zſ&:>i*+篏9,lRLrUW)ֆjr:?M,ɔ?02Of"tt[|^zn*VOg ;\͗V=ztiË:ުwHH[p8nh<8Wg6gAMm˶VR"+u۪1ߛN6ٮlj'vYa뜴ULrLs[)cRq4#&`KN.}U[W!҆sJP}7w#Lۨ^˷ݮ}ed$0x2;zpeZ_.b׵yhc'DklO O>y^=UC&`'~S;MVMo\lЭ~ʭz,|p8SREnrd6=[hlipaKDP]Q`Cǭ݌N#c&\'{M)ou"FXcջ\۵>a2@ܕM7jésð$[ͲǺ6v=;$>>7WKCmslצspHwp84 gsbq:Һ(iel2mԧڵ.-wWmQ{`SzM*-~t |IiH}ìܻ.aU.m*v3ծ?u.%)@.bly,dԄӦKѩiW]XU ltSU;eiڵz m)=65`QkBaQBA\cX:DOٮcT2,\ܻJfeZuS)i׶lŨy J~i3K҄]/CkS9" @3s};C.uMq^tr Ps6*k=Uy'0br]oeW4i(e[LYb{b4̎9Ed3mh_]d44c\6/2 @ ش\ҫ ya:Ĵg"$SZ/TTA\B67kW[Ԯl/D.U#LRT1W{ j}ZCڻ|1Lu8457@s1r(U'Jыer{M*U r,zi K{|g|cDNu|zg"U ,'$#DO.bq(B˱ &n!hsUJoC6U }sg-˄L3h]ԅiL\ʪ-%7dI8كK>p).;rQCܕ|Mk7hnf,YOl˄%sVCzU8oUr5U',7:X,iІ٥`Lͯ8l+rPMMuN i/5%}e3tާrwhPbMm:{f,''۩8%f;ה˳&SvhrXe ʫEU[gV.^ŬmM+D^u9?h<=rαjfΈepș' FI{RіR2 @bDhZSuM!n:4-xgdXЋr熫jڛ(2u;vk ^1!7V]Ɔz2eNC7r TB \G54I5%:R:7fs7p`bS);7ܴb򐟢ZMujUKk2gʟDW.B~@=iMC:3ž*ff%Vǫ2 0j}` yǣQsVmA&RgPLNYK^n1fCۻm%|nHSm~0gqYN $rW:?w[>'ԣM?7;g=3՘FJ/A[X3UMUM4nM[o1-/1>OubOsY٦һO]h߫UPbwlZwH=KUкZ4!bx2;zp֑M\~ͧ]POh<=)mg π,` 0` +/(tSa`F"g?wojczʊ5W?MQF|Ӗ0N_Tuc)N&U W9ڋS#{릙-rS~[RL]TRM5䛶rP~XR8(+]6?rmSOrHW&@& tyK9O{J!-XFi > \:z82=gu:evhSQ|2ͨpfh¶cۃW͌*V=i"MzI|F:cE=:d#|m)ȵ5FX]UMzeG C  FYL) 1 Dހ>&&:a@D R(J(`Lf#UB[,wL.+c ϚSW \B{L#םU&y>n@6Z~n&Ġ'Gӹx*}za,>3O2; IDATX+WCg&(?X. DlZ:1eBpPm9Nqe' SX/Om8uCym 93DD( 2_\FFLDn:V>NFUqk:|&]!,zOtiN9j+EXr\V>q8KFlsݨl:96OL1.4?]^M<]G9&_HSۦ?N7\E7qнR Swn̴>aU{Q%vSpr[yh^[[T )Ǫ|LM'⅃r*~)/Ql3)BMW}ך\}S^&|H?h?la6YtU'Sdz*6PUuҮq,~U}ȷ7]ͤ~w\|7}t:Jռ8Ofͨs!%ykGMRIuXe;k ݒ:fU(pSk6RKؕ$_>.s.@l[Gb37Y&U[6+̔.}9S.V}м7䣏@]ԙ Om)aʢ!}^,AfXe5@_v"6.F)?sZbm53o4s)_smoCէ֏ʽ'^1ua\ C[ P5.iXslSEͅMuQO(g{*")+Qo!r?ր'u?j˶So_*ѥQ:Ak`܅xֻ?9m^iM,M:J-ObJ;8Q%lۨT-A*&7-i7ժ&}j7ڣ?<`r.[W>)X+glGm̀E-c>hH&ۅVϔHc=P.]/ˮWXt8]vuӫ-m6ps>bk小yre[ ?0};btiJ[[|^,|yC}]?ڍ{oZ\'GBgNW҅T?Yn图`Y'Wr}B omE@|lXdiudi-:Mƹ]Bud'UllsxVZu̚k??nu]JF8'31t|V}1]-ֶ8R']xN4I"ctj-Xp8'fͷj@_V{GHXz[CHC/)|Etu"Gsh(ˤb11SVZ~oBC \JW4Ru[.]/V<^oh%9tϠ;kXV`5=p Tm7U{dݰ\'Di&xSg| m+)Cc6R5hjpj2}go*:M<)/M*F]IfS1txRSg?I[:G0didZc{V-6:V7SϾK{<a~wM~ozKh8׆QUds]I6U4ɥ:hL9A$HE훀MJKy;e _& PY߶coޕtijGxUMN:TNV2 RPƓ# 蔒GŨ+Ԇ<2O9B~#91 M+&& |©CjXo@[GHj6j.s4tm:IWnT74'kۅmCXbFLDn::7w=uk:|˻+'>r.[d{,1Nop8nyh< 4!oM:uT3򵷰.sbd8}3v`;tQģJnh]Ln"41T cuTMfYE.j+]fC]N7=$h9uT6E'~LuF6ȍ6 k) ځ2N1=im;qYy`j+' 9ծKOת)Ipb=3{ on,kmy*.ZмlS>M#ЕtijGU'4~5ЧIb4̎9LN@gHW҅y e4_hshc)G< ::9sc@,VL5U{6ܜ>prp[S˧J4vcoS[UA{`2??=./V:@@FLDn::=bt8' i.MuҺXr>q8KFlc-4C=5zt͟񌝷{\\B@*"os4]cz8/PRv R1թ^ BXv>kC\zs6}ů϶ *i3Ϡ'7-OJ6Ŷ9_EW| hl:q'7%Ns9=)gY].G{VVV6ܢ{6Aië|gM7E!5W>A{g @*jlRZ5-rwrf絊ongkeoò7 ID;Zo>Ldvar:C_M=.Lmc. @[#GΐēF'ta a ɴƴ д7As]u6EK4txV 93?~P&Lt{f=%f4DөYwX.vxM/߲{:dyw%]xVWYKnLSkzڪ8[^7Ofors}o+V> gt+ 1`;tQ4?Hj=bHP tM̶ljMmf>+kcŊOW.> u@Xw62>HWnkE6(+kJ hl:q'7N r`ܾhۉdq r 2ٕtiZu%gB{g @*jlRZ5}o6İllg7^ſM{WҥŰe82 RPƓ# }5 J0!Ɠ+muml8%'u@G'ta a ɴƴ u:GsmlǧrYM[{-uӤ7mc_gO*ͯ Mq*^ĪzC/]r{|uOjk+Ɠt:zfm?ԥq|U=t-6UKX}Wrc^l"oICh[?I>m7ӏhE%f7%7C;Ofŵv9KXޙأqz죶]} ?vZY|Iꁜ>gn^Y%WR\fB =bt_%5$};+[:Tv S.:3h@w>P(CFr>hV6]F>uӔr|p }:-2Ž[Y<c=ik@LV;ӶlXzk> oLӧfuPyʓz픛i-69*>&%KOm,֑?\ `ﭓJ\rR2C,%2Aտhz cfnyI7e늏6T.Zf]&AZʴP6m~Y^; K9Wٴ'>5!3bJ6OŒC͵@7k7[kd:dש?h<=r:C_M@.̦Yھ gzPxRo ڧ xی#:  a eiMؐM9Bnmvqҥg |94 B/CE?mCXbFLDn:z=36N~T6>ο |yݕtiW/4,~%7ʲ3ph< 4Ug.t c3<~ȟ>Πs{fYva#D>4507I ?aζl6=Z/b\DIk.+>]`K0t9]SmTHk3r])SEߛ%u:q$'ENlE%;wTmg2-3XA6+ӵf gWzM'ڣ?SNP RQ&` ;m{WkCʲM4m<7HY7.M<]*[5y@U| )(Ftt%]]FI6;Gȏ66ēcb ҀX$.!kF0)gS#ϪM)Mwy&UY7MxNU#˘Es TU}Vc m/MêEIx.CͿ>7)l}ۄp,̳(B۝&2cQV"_MemɶR*iPrfUo }VcsשEfm}Kv6JƆY5:%,遫v6"֪Oɽi+|Mjm6S.W]S+u=׆!F>6sXv `UruXP]4')m%@oG,6>sW퍼!J|{4 \*[mW-UDRWj!iov9Rq{`bI1-m:Bho&Gɫ}g/i`܅x%?9_m^iM,*I3crS!Mڄ;KT67Չ xfLLդ7wz5w]US}o|(`(}c[|H-ôA@Bo۟ØLrWlZul!ݬbpBC6۵鲇^M<]l '\L! ټפ3Gy[61}s_´3fO]Cɷ6ChgyC}mR_闵c1OfGFN (XW.̦Yr(> EDFI6Еt]嶢iF_|d=@æĶ>#&&Ox`_etYw%]xVmX+cl&""EQk&M'´]`'31t75>ο |yݕti1FZYKnpTn4?5̦Cgk8/O]}u {\vʿRE7GsiȒe;1TP"KafZҕFbS_՞"$4=ElϠuFbN_QW\AeӆB~^{VVV6ܢmnUmĹLL'VmA9JtI.A{g @*ufۄnvɶxY6Ϳ̈́doޕtijGxڦ<ЖԚ˜7|Uʗߵt|\[ S+CK\pFl;V5i=|-˜;R ҵahmޏQ>!xmQMmL8' M,S81gM!:51ʱ_Ϫʵ2$6M\|x>kC>r7|ȩҥMoJiT."ۖϭ_G\.\`fB1NuVP`=vڊࣘe7DAl/|򶩲-vy@ X'@,*@udEp ͽLtTdE \6;P:$mp:+4|SgcٖI^d?N7\]W16ɇW9|2*ϾUuMd% IDAT0}oz MI;1-_fr-]M |l놓sGyWUxǘeBٴ'>}`>oh<=reꮧ+V1CW=hs-}Uid4܆i(AGဪ2F'%+iæ[#&m"S%b:JBOhH#g @Cۑ9d&"F7Ns>ο |yݕtiQ=u"cɍr}>p8ʍƓ@3Wu13X{x;3SSt '9 {\D!U'f|QqDOT)1'6ځ&X ʾ͜|l(+r"g|ȡƼ 4IRθ|+HWi+ `yB;6ȗ.`>Y:񶕌! fZf*' 9(]I6U7MPȭj)/d'GjҕtaC@w'V!?pJONæii ogSb,U'Ϳ߶ 8V Jf{j93?~P{h~-^8Xw|像FvGX}DqYn y>V}/}V8Of"btY5>ns^W՗>o*wIKtiY -rC[F>26F|Owe|OBpn{])??-x2jFA@3Mutƿ6ra](_PwN߇iVj6EN9ô\We oA;Mg:Km񬊿3uCT6Y&%Z|DSk&cO> v R > ?48ֹ'>iטE +L[kgnVkC tui&*yh7Dfƨ!F>/LL-C$U8J5钓!{)ڐ (*KunU81nytXsEQ$[\*[Ialo]E$ey6m!{O[67mRI1е\vm(;ZX^<~ rreʷc+E_!)>n?Z5QӮLjf XH=ʇ<ʵmmymB8kl]Gv ]^]mu['3>c_ƘĢ1b ^zS~1K{v?'y[6խ 7N'tiJ[[|pYPT>ob4̎96 8q֕ta6zy>q;Ɠz+9u >ml8%'+VT 1ėV`@,V|l.Lms}~i\7]魛 dFĶ?<")meoU!e9>像^"rG~o-0Of"btYo qk:etZ\̷,|4Yޱ哖:'xi˥*|[d{]-#t5]ͤ~w\|7r>Ƨi})?Ɠ@3:s9,.0=r3VRI+]l9'k9eWmZ`PFjiYBk@݌ (h@kj o:PQ7Ƭ?>&eU.cu&mP[?]_59M0f7}zefYt՚CWfcf#jt#S8Κ6CkI ɷ-dJU<}*͝1'tiJ䛶\Re·zjl_|GLSx2;z0Z*tj@ZW҅T?Y.{1\3X;Ɠz+9u >ml8%'+לVDg D,`yشcsY ?RrjTl 516-CB.9ʙ5e뢟6!,1d&"F7N+qO^,ꞫN6Y]I&>qtĒ3slʍtjMO[pFl:s,=C{~vMt 'q*gCgSxp)m( kL' Wt=jRˤb1k/١:Y˔U͑CEe?V|rhhW<}t6U7DnŜ.f Grh4GW @SCADt|b)Z.]H6U7S8.]3&(؃uֿ=1D<^ݲ)lm&i,KOW;2~5g2 RPƓ# }5 J0!Ɠz+mvml8%'u@G'ta a ɴuSycm~Tu4i JfT=הc,9S,B0[KUyUb}D9C򿏗(~y|uOjk>Hox2N^ʿ'q|_MO*ήI&UqpW|ؾgm>>iXC=fUھ.ێb{zᓟh< #?fqibԫG>]6E<+eq?t?,*Rpw]'l:m>YЩGڗimK%ŸW/ n,S r=t>07y#U7}ts: ?>hb|Hg &l|*$yzQ} YIWfTي>kCZܶD{QTU{6=p]gQIg۸1Mjm+ץy]Ū?u=׆!F>l#>}@6u&T*W}p b mHmn6 3m},6ڻR|T5rΤ 򶭙Ig{*)+QoZo<*=%|9-24u}{'Fkgږm0+RU3}͇Tغ{j9 7#g0\*$S_z&˪S&*YϐjM]-]^]mu['3>c_Ƙǔ1b)6}/umCWIUo}S^ØkjU& qϞ&aY [Cu-ef(bӖ.TSuCeo1K&9۲iO<}k6BxQ]Cɷ6y힄F{fk_S˦zRh<=rm:C_g~.fYr(&E3O1@#\sq:  %,a; BC=NngC[78S+3YJ[ݴfKLC`1]`'31t|濎Kf,l7]]I&>qpK#g|׺EǒԚ!׍Ɠ5rsgPXᇆSur>&{qMv6sO86P-Mb ?aV]a- "L+*ALm}{=zAE *xO_xmuk\i{dݰ\ʾɏyKmkʆ[T]MP2Rt8W)\]H6U7S88Ihc._H>ԓif;">~V].bŧmyQƓt:zfz>N~T6>ο |yݕtiW8iɣrC[F om[|mZY+]]'ɇȝ:V~jtr&V~p4ыm\pf!T3򵷰.sbM9?tP5S"_ΊO /)Mo{m u1KZ}_Ipڙy?ˤ mƌ6r(Ϛ1Rf_br3 5,aȈ"W?S[6Y*Է6#׆[m8)㩵]mG)AM(F>l#>}@́$L>I1rՇ* v̷ /ͽeQO_xmK#:Զʣ(d+ SWyk 3u<6$V8r*!7s.VϪmB&ix[χCo4 @q. uMjgbC'3B]ɇT}ZMu3M6jvMl628Btvq-]Fڣ?<Кr>&UQs/m峏)E-c>R"z9Can$M@ml^zwbZȴ+ՎL:. 9S1⮽w?Wy^ VbO;>4Ѯ|u~|H/Bkf[_ԉmm,FhG M_.{1\3^;}hq0 ]d{,QgRx2hfO y=?Jbe]C@*.;&:G狊o}{'E5t@,e?TQOQljm6æZ/r<銹 ri>~=p۹VuPmt`6E'JuF6jhc(+UGlj \ʾɏy&,9=+++nQɶ=q9>H7sm5XA6:ٕtiZu3NϔAT$ 8&[[UZмlS>˿z,mܻ.M<]r\m7!gR=B&@ x2;z09f]If71txn uƆSxRttr@ƀ,X6LkL cm:ͷlJm5ni0>p:2UI??n}շ601Of"btyo!.vxM_L~m߫5FMBR']xVU>r*Xr>q8KFl:t3۷f*>]6E<\7 e>&Rw)j~IRE7hG;uiZR=R&;b2LfSk 6}4<٦L,zUœ.V>.bH6:-یD|rﻂմzSuSeTEW\ hlRM~lsP(4Qeͳ)xXYYpJy.vx`oGUrjrm#-]>S(x_SObϔAT$ 8MpR`; MI3m r+_۪Ma)A峭\\kIn8} )(Ftt%]]FI&:Gȏ&O!'m NK:Xu; :6(bJ7jdF,1;6_q29zKhο |yݕtiWBѸEǒԚ!׍Ɠ5{w3#&5.~ҷAOH 1;G_O86P\hR1M֤AbQsU"5]x)Q?fx!s-V|r `I5 > :^))D@(EQ^p_;PB@cRo}ޤ"yC0lE%[ %#H73mxȮKOת)[SPE6AT qmyeY߶coޕtijGUڰۯ-].B&@ x2;z09Q1J0!Ɠn,'Xtm:9sc@,VL5x>}Ǣ[Yeh&NrpCS Js#0]b3eΜϋCUϛL;뤥N4CU~Wm]d{,QNgRx2hf\o\#&]O=."ME/)`mj3E9狊oE?ߍ @AbXmUMͶY B:>i5q_Ĺel @X(mux *xȥ>7-OJvp2kEQ{/*2ll5 pfY[`kʆ[T]&m()CA\G,0&XBm:hL9A$HEClyS(KR"CEyYt,Masi`1N爦2 RPƓ# }5J0!Ɠz+mvXtm:9sc@,VL5SRBǢ[Yehm9த QuC{!m_Tӗџ'31ty^,l7]]I)|׻&h~ zIDAT괫p|[',~o>,sNzSSSmS%)in4ugC=s}o+4|CoM>ct?&PmeӱA,/9T/@?Ԏ(cU9ĔKU)re "MqųN<ЩS.Uiz֟ƈgh/SN:ȶ^^ /|$}Yn 52IU?PoH PH{UɖrfcUϵЦ=LkOb4e-7rOSgM<5ylª?,ft^cFofje Y٤C+|7RE6gCe`!M|ʹJ?CCh-Trаj@Tf+r~ShO[5ևz3|OsvT%7_϶:YWq׶SNd&] ǟt[p67aqL7Km=}ͿLn3v;xwY>`Usg]L#isPڕtgS}??=cm49IueUTݩ^nIޜ]}NUԡD|pm{Ke6(r2z{.j'k\Лq=\&]!.'S?{_kŌg~6AWՀK9$OyRߍƓ#ђ*ft1jYg&4_s^u1]s_ڼ@<4C+drJ;3 >ohmG\NRwsS4vUzOm~lΦpRsC6"O'Un4%=?q-z:׼욊gmj?3ȒIʍƓ197>;/k}%+b%4ssOy <_$,#WV=#gsl35}&9+]Ug:xVr=s7Fz0ʿio=?(Nq:*C$P>Ew"(mիq݀&e&P rQlC/<?IN4`rW)U>7TLb7[]宽,xsij p lJ. W3z PR+mM+!@GQq=|5MޜtTNꔕzCJYBsS|\ڶhc! h<=rتil}a2ٌXj mMCdz?]`ޟ :nVM1Jj>_Bs UX d+(I2Vv9F^{SPD%'z·>v;+@D̙3@F 9s~g%?SV IENDB`pg_activity-1.5.0/docs/man/000077500000000000000000000000001343700417500155465ustar00rootroot00000000000000pg_activity-1.5.0/docs/man/build-man.sh000077500000000000000000000002431343700417500177540ustar00rootroot00000000000000#!/bin/bash pod2man -r "pg_activity 1.5.0" -d `date +%Y-%m-%d` -c "Command line tool for PostgreSQL server activity monitoring." pg_activity.pod > pg_activity.1; pg_activity-1.5.0/docs/man/pg_activity.1000066400000000000000000000231321343700417500201530ustar00rootroot00000000000000.\" Automatically generated by Pod::Man 4.09 (Pod::Simple 3.35) .\" .\" Standard preamble: .\" ======================================================================== .de Sp \" Vertical space (when we can't use .PP) .if t .sp .5v .if n .sp .. .de Vb \" Begin verbatim text .ft CW .nf .ne \\$1 .. .de Ve \" End verbatim text .ft R .fi .. .\" Set up some character translations and predefined strings. \*(-- will .\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left .\" double quote, and \*(R" will give a right double quote. \*(C+ will .\" give a nicer C++. Capital omega is used to do unbreakable dashes and .\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, .\" nothing in troff, for use with C<>. .tr \(*W- .ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' .ie n \{\ . ds -- \(*W- . ds PI pi . if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch . if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch . ds L" "" . ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ . ds -- \|\(em\| . ds PI \(*p . ds L" `` . ds R" '' . ds C` . ds C' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" .\" If the F register is >0, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. .\" .\" Avoid warning from groff about undefined register 'F'. .de IX .. .if !\nF .nr F 0 .if \nF>0 \{\ . de IX . tm Index:\\$1\t\\n%\t"\\$2" .. . if !\nF==2 \{\ . nr % 0 . nr F 2 . \} .\} .\" .\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). .\" Fear. Run. Save yourself. No user-serviceable parts. . \" fudge factors for nroff and troff .if n \{\ . ds #H 0 . ds #V .8m . ds #F .3m . ds #[ \f1 . ds #] \fP .\} .if t \{\ . ds #H ((1u-(\\\\n(.fu%2u))*.13m) . ds #V .6m . ds #F 0 . ds #[ \& . ds #] \& .\} . \" simple accents for nroff and troff .if n \{\ . ds ' \& . ds ` \& . ds ^ \& . ds , \& . ds ~ ~ . ds / .\} .if t \{\ . ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" . ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' . ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' . ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' . ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' . ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' .\} . \" troff and (daisy-wheel) nroff accents .ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' .ds 8 \h'\*(#H'\(*b\h'-\*(#H' .ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] .ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' .ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' .ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] .ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] .ds ae a\h'-(\w'a'u*4/10)'e .ds Ae A\h'-(\w'A'u*4/10)'E . \" corrections for vroff .if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' .if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' . \" for low resolution devices (crt and lpr) .if \n(.H>23 .if \n(.V>19 \ \{\ . ds : e . ds 8 ss . ds o a . ds d- d\h'-1'\(ga . ds D- D\h'-1'\(hy . ds th \o'bp' . ds Th \o'LP' . ds ae ae . ds Ae AE .\} .rm #[ #] #H #V #F C .\" ======================================================================== .\" .IX Title "PG_ACTIVITY 1" .TH PG_ACTIVITY 1 "2019-03-03" "pg_activity 1.5.0" "Command line tool for PostgreSQL server activity monitoring." .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh .SH "NAME" pg_activity \- Realtime PostgreSQL database server monitoring tool .SH "SYNOPSIS" .IX Header "SYNOPSIS" \&\fBpg_activity\fR [option..] .SH "DESCRIPTION" .IX Header "DESCRIPTION" Command line tool for PostgreSQL server activity monitoring. .PP pg_activity must run on the same server than the instance and as the user running the instance (or root) to show \&\s-1CPU, MEM, READ\s0 or \s-1WRITE\s0 columns and other system informations. .SH "COMMAND-LINE OPTIONS" .IX Header "COMMAND-LINE OPTIONS" .IP "\fB\-U \s-1USERNAME\s0\fR, \fB\-\-username=USERNAME\fR" 2 .IX Item "-U USERNAME, --username=USERNAME" .Vb 1 \& Database user name (default: $USER). .Ve .IP "\fB\-p \s-1PORT\s0\fR, \fB\-\-port=PORT\fR" 2 .IX Item "-p PORT, --port=PORT" .Vb 1 \& Database server port (default: "5432"). .Ve .IP "\fB\-h \s-1HOSTNAME\s0\fR, \fB\-\-host=HOSTNAME\fR" 2 .IX Item "-h HOSTNAME, --host=HOSTNAME" .Vb 1 \& Database server host or socket directory (default: "localhost"). .Ve .IP "\fB\-d \s-1DBNAME\s0\fR, \fB\-\-dbname=DBNAME\fR" 2 .IX Item "-d DBNAME, --dbname=DBNAME" .Vb 1 \& Database name to connect to (default: "postgres"). .Ve .IP "\fB\-C\fR, \fB\-\-no\-color\fR" 2 .IX Item "-C, --no-color" .Vb 1 \& Disable color usage. .Ve .IP "\fB\-\-blocksize=BLOCKSIZE\fR" 2 .IX Item "--blocksize=BLOCKSIZE" .Vb 1 \& Filesystem blocksize (default: 4096). .Ve .IP "\fB\-\-rds\fR" 2 .IX Item "--rds" .Vb 1 \& Enable support for AWS RDS. .Ve .IP "\fB\-\-output=FILEPATH\fR" 2 .IX Item "--output=FILEPATH" .Vb 1 \& Store running queries as CSV. .Ve .IP "\fB\-\-no\-db\-size\fR" 2 .IX Item "--no-db-size" .Vb 1 \& Skip total size of DB. .Ve .IP "\fB\-\-verbose\-mode=VERBOSE_MODE\fR" 2 .IX Item "--verbose-mode=VERBOSE_MODE" .Vb 1 \& Queries display mode. Values: 1\-TRUNCATED, 2\-FULL(default), 3\-INDENTED .Ve .IP "\fB\-\-help\fR" 2 .IX Item "--help" .Vb 1 \& Show this help message and exit. .Ve .IP "\fB\-\-version\fR" 2 .IX Item "--version" .Vb 1 \& Show program\*(Aqs version number and exit. .Ve .SH "ENVIRONMENT VARIABLES" .IX Header "ENVIRONMENT VARIABLES" .IP "\fB\s-1PGPASSWORD\s0\fR" 2 .IX Item "PGPASSWORD" .Vb 1 \& PostgreSQL password. .Ve .IP "\fB\s-1PGPORT\s0\fR" 2 .IX Item "PGPORT" .Vb 1 \& Database server port. .Ve .IP "\fB\s-1PGUSER\s0\fR" 2 .IX Item "PGUSER" .Vb 1 \& Database user name. .Ve .IP "\fB\s-1PGHOST\s0\fR" 2 .IX Item "PGHOST" .Vb 1 \& Database server host or socket directory. .Ve .IP "\fB\s-1PGPASSFILE\s0\fR" 2 .IX Item "PGPASSFILE" .Vb 1 \& Path to .pgpass file (default is ~/.pgpass) .Ve .SH "DISPLAY OPTIONS" .IX Header "DISPLAY OPTIONS" .IP "\fB\-\-no\-database\fR" 2 .IX Item "--no-database" .Vb 1 \& Disable DATABASE. .Ve .IP "\fB\-\-no\-user\fR" 2 .IX Item "--no-user" .Vb 1 \& Disable USER. .Ve .IP "\fB\-\-no\-client\fR" 2 .IX Item "--no-client" .Vb 1 \& Disable CLIENT. .Ve .IP "\fB\-\-no\-cpu\fR" 2 .IX Item "--no-cpu" .Vb 1 \& Disable CPU%. .Ve .IP "\fB\-\-no\-mem\fR" 2 .IX Item "--no-mem" .Vb 1 \& Disable MEM%. .Ve .IP "\fB\-\-no\-read\fR" 2 .IX Item "--no-read" .Vb 1 \& Disable READ/s. .Ve .IP "\fB\-\-no\-write\fR" 2 .IX Item "--no-write" .Vb 1 \& Disable WRITE/s. .Ve .IP "\fB\-\-no\-time\fR" 2 .IX Item "--no-time" .Vb 1 \& Disable TIME+. .Ve .IP "\fB\-\-no\-wait\fR" 2 .IX Item "--no-wait" .Vb 1 \& Disable W. .Ve .IP "\fB\-\-no\-app\-name\fR" 2 .IX Item "--no-app-name" .Vb 1 \& Disable App. .Ve .SH "INTERACTIVE COMMANDS" .IX Header "INTERACTIVE COMMANDS" .IP "\fBC\fR Activate/deactivate colors." 2 .IX Item "C Activate/deactivate colors." .PD 0 .IP "\fBr\fR Sort by READ/s, descending." 2 .IX Item "r Sort by READ/s, descending." .IP "\fBw\fR Sort by WRITE/s, descending." 2 .IX Item "w Sort by WRITE/s, descending." .IP "\fBc\fR Sort by CPU%, descending." 2 .IX Item "c Sort by CPU%, descending." .IP "\fBm\fR Sort by MEM%, descending." 2 .IX Item "m Sort by MEM%, descending." .IP "\fBt\fR Sort by \s-1TIME+,\s0 descending." 2 .IX Item "t Sort by TIME+, descending." .IP "\fBSpace\fR Pause on/off." 2 .IX Item "Space Pause on/off." .IP "\fBv\fR Change queries display mode: full, truncated, indented" 2 .IX Item "v Change queries display mode: full, truncated, indented" .IP "\fB\s-1UP / DOWN\s0\fR Scroll process list." 2 .IX Item "UP / DOWN Scroll process list." .IP "\fBq\fR Quit" 2 .IX Item "q Quit" .IP "\fB+\fR Increase refresh time. Maximum value : 3s" 2 .IX Item "+ Increase refresh time. Maximum value : 3s" .IP "\fB\-\fR Decrease refresh time. Minimum Value : 1s" 2 .IX Item "- Decrease refresh time. Minimum Value : 1s" .IP "\fBF1/1\fR Running queries monitoring." 2 .IX Item "F1/1 Running queries monitoring." .IP "\fBF2/2\fR Waiting queries monitoring." 2 .IX Item "F2/2 Waiting queries monitoring." .IP "\fBF3/3\fR Blocking queries monitoring." 2 .IX Item "F3/3 Blocking queries monitoring." .IP "\fBh\fR Help page." 2 .IX Item "h Help page." .IP "\fBR\fR Refresh." 2 .IX Item "R Refresh." .PD .SH "NAVIGATION MODE" .IX Header "NAVIGATION MODE" .IP "\fB\s-1UP\s0\fR Move up the cursor." 2 .IX Item "UP Move up the cursor." .PD 0 .IP "\fB\s-1DOWN\s0\fR Move down the cursor." 2 .IX Item "DOWN Move down the cursor." .IP "\fBk\fR Terminate the current backend/tagged backends." 2 .IX Item "k Terminate the current backend/tagged backends." .IP "\fBSpace\fR Tag or untag the process." 2 .IX Item "Space Tag or untag the process." .IP "\fBq\fR Quit." 2 .IX Item "q Quit." .IP "\fBOther\fR Back to activity." 2 .IX Item "Other Back to activity." .PD .SH "EXAMPLES" .IX Header "EXAMPLES" PGPASSWORD='mypassword' pg_activity \-U pgadmin \-h 127.0.0.1 \-\-no\-client .PP pg_activity \-h /var/run/postgresql .PP pg_activity \-h myserver \-p 5433 \-d nagios \-U nagios pg_activity-1.5.0/docs/man/pg_activity.pod000066400000000000000000000062551343700417500206040ustar00rootroot00000000000000=head1 NAME pg_activity - Realtime PostgreSQL database server monitoring tool =head1 SYNOPSIS B [option..] =head1 DESCRIPTION Command line tool for PostgreSQL server activity monitoring. pg_activity must run on the same server than the instance and as the user running the instance (or root) to show CPU, MEM, READ or WRITE columns and other system informations. =head1 COMMAND-LINE OPTIONS =over 2 =item B<-U USERNAME>, B<--username=USERNAME> Database user name (default: $USER). =item B<-p PORT>, B<--port=PORT> Database server port (default: "5432"). =item B<-h HOSTNAME>, B<--host=HOSTNAME> Database server host or socket directory (default: "localhost"). =item B<-d DBNAME>, B<--dbname=DBNAME> Database name to connect to (default: "postgres"). =item B<-C>, B<--no-color> Disable color usage. =item B<--blocksize=BLOCKSIZE> Filesystem blocksize (default: 4096). =item B<--rds> Enable support for AWS RDS. =item B<--output=FILEPATH> Store running queries as CSV. =item B<--no-db-size> Skip total size of DB. =item B<--verbose-mode=VERBOSE_MODE> Queries display mode. Values: 1-TRUNCATED, 2-FULL(default), 3-INDENTED =item B<--help> Show this help message and exit. =item B<--version> Show program's version number and exit. =back =head1 ENVIRONMENT VARIABLES =over 2 =item B PostgreSQL password. =item B Database server port. =item B Database user name. =item B Database server host or socket directory. =item B Path to .pgpass file (default is ~/.pgpass) =back =head1 DISPLAY OPTIONS =over 2 =item B<--no-database> Disable DATABASE. =item B<--no-user> Disable USER. =item B<--no-client> Disable CLIENT. =item B<--no-cpu> Disable CPU%. =item B<--no-mem> Disable MEM%. =item B<--no-read> Disable READ/s. =item B<--no-write> Disable WRITE/s. =item B<--no-time> Disable TIME+. =item B<--no-wait> Disable W. =item B<--no-app-name> Disable App. =back =head1 INTERACTIVE COMMANDS =over 2 =item B Activate/deactivate colors. =item B Sort by READ/s, descending. =item B Sort by WRITE/s, descending. =item B Sort by CPU%, descending. =item B Sort by MEM%, descending. =item B Sort by TIME+, descending. =item B Pause on/off. =item B Change queries display mode: full, truncated, indented =item B Scroll process list. =item B Quit =item B<+> Increase refresh time. Maximum value : 3s =item B<-> Decrease refresh time. Minimum Value : 1s =item B Running queries monitoring. =item B Waiting queries monitoring. =item B Blocking queries monitoring. =item B Help page. =item B Refresh. =back =head1 NAVIGATION MODE =over 2 =item B Move up the cursor. =item B Move down the cursor. =item B Terminate the current backend/tagged backends. =item B Tag or untag the process. =item B Quit. =item B Back to activity. =back =head1 EXAMPLES PGPASSWORD='mypassword' pg_activity -U pgadmin -h 127.0.0.1 --no-client pg_activity -h /var/run/postgresql pg_activity -h myserver -p 5433 -d nagios -U nagios =cut pg_activity-1.5.0/pg_activity000077500000000000000000000366271343700417500163310ustar00rootroot00000000000000#!/usr/bin/env python """ pg_activity author: Julien Tachoires license: PostgreSQL License Copyright (c) 2012 - 2019, Julien Tachoires Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL JULIEN TACHOIRES BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF JULIEN TACHOIRES HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. JULIEN TACHOIRES SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND JULIEN TACHOIRES HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. """ from __future__ import print_function PGTOP_VERSION = "1.5.0" import os import sys if os.name != 'posix': sys.exit("FATAL: Platform not supported.") import signal from optparse import OptionParser, OptionGroup import socket import getpass import psutil import curses import psycopg2 import time from psycopg2 import errorcodes from pgactivity import UI # Customized OptionParser class ModifiedOptionParser(OptionParser): """ ModifiedOptionParser """ def error(self, msg): raise OptionParsingError(msg) class OptionParsingError(RuntimeError): """ OptionParsingError """ def __init__(self, msg): self.msg = msg # Create the UI PGAUI = UI.UI(PGTOP_VERSION) def main(): """ Main function """ try: try: parser = ModifiedOptionParser( add_help_option = False, version = "%prog "+PGTOP_VERSION, description = "htop like application for PostgreSQL \ server activity monitoring.") # -U / --username parser.add_option( '-U', '--username', dest = 'username', default = os.environ.get('PGUSER') or getpass.getuser(), help = "Database user name (default: \"%s\")." % (getpass.getuser(),), metavar = 'USERNAME') # -p / --port parser.add_option( '-p', '--port', dest = 'port', default = os.environ.get('PGPORT') or '5432', help = "Database server port (default: \"5432\").", metavar = 'PORT') # -h / --host parser.add_option( '-h', '--host', dest = 'host', help = "Database server host or socket directory \ (default: \"localhost\").", metavar = 'HOSTNAME', default = os.environ.get('PGHOST') or 'localhost') # -d / --dbname parser.add_option( '-d', '--dbname', dest = 'dbname', help = "Database name to connect to (default: \"postgres\").", metavar = 'DBNAME', default = 'postgres') # -C / --no-color parser.add_option( '-C', '--no-color', dest = 'nocolor', action = 'store_true', help = "Disable color usage.", default = 'false') # --blocksize parser.add_option( '--blocksize', dest = 'blocksize', help = "Filesystem blocksize (default: 4096)", metavar = 'BLOCKSIZE', default = 4096) # --rds parser.add_option( '--rds', dest = 'rds', action = 'store_true', help = "Enable support for AWS RDS", default = False) # --output parser.add_option( '--output', dest = 'output', help = "Store running queries as CSV.", metavar = "FILEPATH", default = None) # --help parser.add_option( '--help', dest = 'help', action = 'store_true', help = "Show this help message and exit.", default = 'false') # --debug parser.add_option( '--debug', dest = 'debug', action = 'store_true', help = "Enable debug mode for traceback tracking.", default = 'false') # --no-db-size parser.add_option( '--no-db-size', dest = 'nodbsize', action = 'store_true', help = "Skip total size of DB.", default = False) # --verbose-mode parser.add_option( '--verbose-mode', dest = 'verbosemode', help = "Queries display mode. Values: 1-TRUNCATED, 2-FULL(default), 3-INDENTED", metavar = 'VERBOSE_MODE', choices = ['1', '2', '3'], default = '2') group = OptionGroup( parser, "Display Options, you can exclude some columns by using them ") # --no-database group.add_option( '--no-database', dest = 'nodb', action = 'store_true', help = "Disable DATABASE.", default = 'false') # --no-user group.add_option( '--no-user', dest = 'nouser', action = 'store_true', help = "Disable USER.", default = 'false') # --no-client group.add_option( '--no-client', dest = 'noclient', action = 'store_true', help = "Disable CLIENT.", default = 'false') # --no-cpu group.add_option( '--no-cpu', dest = 'nocpu', action = 'store_true', help = "Disable CPU%.", default = 'false') # --no-mem group.add_option( '--no-mem', dest = 'nomem', action = 'store_true', help = "Disable MEM%.", default = 'false') # --no-read group.add_option( '--no-read', dest = 'noread', action = 'store_true', help = "Disable READ/s.", default = 'false') # --no-write group.add_option( '--no-write', dest = 'nowrite', action = 'store_true', help = "Disable WRITE/s.", default = 'false') # --no-time group.add_option( '--no-time', dest = 'notime', action = 'store_true', help = "Disable TIME+.", default = 'false') # --no-wait group.add_option( '--no-wait', dest = 'nowait', action = 'store_true', help = "Disable W.", default = 'false') # --no-app-name group.add_option( '--no-app-name', dest = 'noappname', action = 'store_true', help = "Disable App.", default = 'false') parser.add_option_group(group) (options, _) = parser.parse_args() except OptionParsingError as err: print('pg_activity: error: %s' % err.msg) print('Try "pg_activity --help" for more information.') sys.exit(1) if options.help is True: print(parser.format_help().strip()) sys.exit(1) password = pg_connect(options, password=os.environ.get('PGPASSWORD'), service=os.environ.get('PGSERVICE')) debug = options.debug pg_version = PGAUI.data.pg_get_version() PGAUI.data.pg_get_num_version(pg_version) hostname = socket.gethostname() # reduce DATABASE column length PGAUI.set_max_db_length(16) # blocksize PGAUI.set_blocksize(int(options.blocksize)) # verbose mode PGAUI.set_verbose_mode(int(options.verbosemode)) # output PGAUI.set_output(options.output) # does pg_activity runing against local PG instance if not PGAUI.data.pg_is_local(): PGAUI.set_is_local(False) PGAUI.set_start_line(2) hostname = options.host # if not connected to a local pg server, then go to degraded mode elif not PGAUI.data.pg_is_local_access(): PGAUI.set_is_local(False) PGAUI.set_start_line(2) hostname = options.host # top part interval = 0 if PGAUI.get_mode() == 'activities': queries = PGAUI.data.pg_get_activities() procs = PGAUI.data.sys_get_proc(queries, PGAUI.get_is_local()) elif PGAUI.get_mode() == 'waiting': procs = PGAUI.data.pg_get_waiting() elif PGAUI.get_mode() == 'blocking': procs = PGAUI.data.pg_get_blocking() # draw the flag flag = PGAUI.get_flag_from_options(options) # main loop disp_procs = None delta_disk_io = None # get DB informations db_info = PGAUI.data.pg_get_db_info(None, using_rds=options.rds, skip_sizes=options.nodbsize) PGAUI.set_max_db_length(db_info['max_length']) # indentation indent = PGAUI.get_indent(flag) # Init curses PGAUI.init_curses() # color ? if options.nocolor == True: PGAUI.set_nocolor() else: PGAUI.set_color() while 1: try: PGAUI.check_window_size() old_pgtop_mode = PGAUI.get_mode() # poll process (disp_procs, new_procs) = PGAUI.poll(interval, flag, indent, procs, disp_procs) if PGAUI.get_mode() != old_pgtop_mode: indent = PGAUI.get_indent(flag) if PGAUI.get_is_local(): delta_disk_io = PGAUI.data.get_global_io_counters() procs = new_procs # refresh the winodw db_info = PGAUI.data.pg_get_db_info( db_info, using_rds=options.rds, skip_sizes=options.nodbsize ) PGAUI.set_max_db_length(db_info['max_length']) # get active connections active_connections = PGAUI.data.pg_get_active_connections() # bufferize PGAUI.set_buffer({ 'procs': disp_procs, 'extras': (PGAUI.data.get_pg_version(), hostname, options.username, options.host, options.port, options.dbname), 'flag': flag, 'indent': indent, 'io': delta_disk_io, 'tps': db_info['tps'], 'active_connections': active_connections, 'size_ev': db_info['size_ev'], 'total_size': db_info['total_size'] }) # refresh PGAUI.refresh_window( disp_procs, (PGAUI.data.get_pg_version(), hostname, options.username, options.host, options.port, options.dbname), flag, indent, delta_disk_io, db_info['tps'], active_connections, db_info['size_ev'], db_info['total_size'] ) interval = 1 except psycopg2.OperationalError: # PostgreSQL connection has been lost, trying to reconnect # Clean curses PGAUI.at_exit_curses() not_connected = True while not_connected: print("ERROR: Not connected to PostgreSQL, trying to open " "a new connection") try: pg_connect(options, password, os.environ.get('PGSERVICE'), exit_on_failed=False) not_connected = False except Exception: print("ERROR: Connecton failed, retrying in 1 second...") time.sleep(1) # Reinit curses env. when the connection is back PGAUI.init_curses() except psutil.AccessDenied as err: PGAUI.at_exit_curses() sys.exit( "FATAL: Access denied, can't access system informations for PID %s" % (str(err),)) except curses.error as err: PGAUI.at_exit_curses() if debug is True: import traceback exc_type, exc_value, exc_traceback = sys.exc_info() traceback.print_exception( exc_type, exc_value, exc_traceback, file=sys.stdout) sys.exit("FATAL: %s" % (str(err),)) except KeyboardInterrupt as err: PGAUI.at_exit_curses() sys.exit(1) except Exception as err: PGAUI.at_exit_curses() # DEBUG if debug is True: import traceback exc_type, exc_value, exc_traceback = sys.exc_info() traceback.print_exception( exc_type, exc_value, exc_traceback, file=sys.stdout) sys.exit("FATAL: %s" % (str(err),)) def pg_connect(options, password=None, service=None, exit_on_failed=True): nb_try = 0 while nb_try < 2: try: PGAUI.data.pg_connect( host=options.host, port=options.port, user=options.username, password=password, database=options.dbname, rds_mode=options.rds, service=service, ) break except psycopg2.OperationalError as err: if nb_try < 1 and (err.pgcode == errorcodes.INVALID_PASSWORD or str(err).strip().startswith( "FATAL: password authentication failed for user")): nb_try += 1 password = PGAUI.ask_password() elif nb_try < 1 and str(err).strip() == "fe_sendauth: no password supplied": nb_try += 1 password = PGAUI.ask_password() elif exit_on_failed: sys.exit("pg_activity: FATAL: %s" % (PGAUI.clean_str(str(err),))) else: raise Exception("Could not connect to PostgreSQL") # Return the password in case of reconnection return password # Call the main function if __name__ == '__main__': signal.signal(signal.SIGTERM, PGAUI.signal_handler) main() pg_activity-1.5.0/pgactivity/000077500000000000000000000000001343700417500162265ustar00rootroot00000000000000pg_activity-1.5.0/pgactivity/Data.py000066400000000000000000000661221343700417500174600ustar00rootroot00000000000000""" pg_activity author: Julien Tachoires license: PostgreSQL License Copyright (c) 2012 - 2019, Julien Tachoires Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL JULIEN TACHOIRES BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF JULIEN TACHOIRES HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. JULIEN TACHOIRES SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND JULIEN TACHOIRES HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. """ import psycopg2 import psycopg2.extras import re import psutil import time from pgactivity.Process import Process import os from warnings import catch_warnings, simplefilter if psutil.version_info < (2, 0, 0): class PSProcess(psutil.Process): """ Due to the new psutil 2 API we need to create a new class inherited from psutil.Process and wrap old methods. """ def status_iow(self,): return str(self.status) def io_counters(self,): return self.get_io_counters() def cpu_time(self,): return self.get_cpu_times() def memory_info(self,): return self.get_memory_info() def memory_percent(self,): return self.get_memory_percent() def cpu_percent(self, interval = 0): return self.get_cpu_percent(interval = interval) def cpu_times(self,): return self.get_cpu_times() else: class PSProcess(psutil.Process): def status_iow(self,): return str(self.status()) def clean_str(string): """ Strip and replace some special characters. """ msg = str(string) msg = msg.replace("\n", " ") msg = re.sub(r"\s+", r" ", msg) msg = re.sub(r"^\s", r"", msg) msg = re.sub(r"\s$", r"", msg) return msg class Data: """ Data class """ pg_conn = None pg_version = None pg_num_version = None io_counters = None prev_io_counters = None read_bytes_delta = 0 write_bytes_delta = 0 read_count_delta = 0 write_count_delta = 0 def __init__(self,): """ Constructor. """ self.pg_conn = None self.pg_version = None self.pg_num_version = None self.io_counters = None self.prev_io_counters = None self.read_bytes_delta = 0 self.write_bytes_delta = 0 self.read_count_delta = 0 self.write_count_delta = 0 def get_pg_version(self,): """ Get self.pg_version """ return self.pg_version def pg_connect(self, host = None, port = 5432, user = 'postgres', password = None, database = 'postgres', rds_mode = False, service = None): """ Connect to a PostgreSQL server and return cursor & connector. """ self.pg_conn = None if host is None or host == 'localhost': # try to connect using UNIX socket try: if service is not None: self.pg_conn = psycopg2.connect( service = service, connection_factory = psycopg2.extras.DictConnection ) else: self.pg_conn = psycopg2.connect( database = database, user = user, port = port, password = password, connection_factory = psycopg2.extras.DictConnection ) except psycopg2.Error as psy_err: if host is None: raise psy_err if self.pg_conn is None: # fallback on TCP/IP connection if service is not None: self.pg_conn = psycopg2.connect( service = service, connection_factory = psycopg2.extras.DictConnection ) else: self.pg_conn = psycopg2.connect( database = database, host = host, port = port, user = user, password = password, connection_factory = psycopg2.extras.DictConnection ) self.pg_conn.set_isolation_level(0) if rds_mode != True: # Make sure we are using superuser if not on RDS cur = self.pg_conn.cursor() cur.execute("SELECT current_setting('is_superuser')") ret = cur.fetchone() if ret[0] != "on": raise Exception("Must be run with database superuser privileges.") def pg_is_local_access(self,): """ Verify if the user running pg_activity can acces system informations for the postmaster process. """ try: query = "SELECT setting||'/postmaster.pid' AS pid_file FROM pg_settings WHERE name = 'data_directory'" cur = self.pg_conn.cursor() cur.execute(query) ret = cur.fetchone() pid_file = ret['pid_file'] with open(pid_file, 'r') as fd: pid = fd.readlines()[0].strip() try: proc = PSProcess(int(pid)) proc.io_counters() proc.cpu_times() return True except psutil.AccessDenied: return False except Exception: return False except Exception: return False def pg_get_version(self,): """ Get PostgreSQL server version. """ query = "SELECT version() AS pg_version" cur = self.pg_conn.cursor() cur.execute(query) ret = cur.fetchone() return ret['pg_version'] def pg_cancel_backend(self, pid,): """ Cancel a backend """ query = "SELECT pg_cancel_backend(%s) AS cancelled" cur = self.pg_conn.cursor() cur.execute(query, (pid,)) ret = cur.fetchone() return ret['cancelled'] def pg_terminate_backend(self, pid,): """ Terminate a backend """ if self.pg_num_version >= 80400: query = "SELECT pg_terminate_backend(%s) AS terminated" else: query = "SELECT pg_cancel_backend(%s) AS terminated" cur = self.pg_conn.cursor() cur.execute(query, (pid,)) ret = cur.fetchone() return ret['terminated'] def pg_get_num_version(self, text_version): """ Get PostgreSQL short & numeric version from a string (SELECT version()). """ res = re.match( r"^(PostgreSQL|EnterpriseDB) ([0-9]+)\.([0-9]+)(?:\.([0-9]+))?", text_version) if res is not None: rmatch = res.group(2) if int(res.group(3)) < 10: rmatch += '0' rmatch += res.group(3) if res.group(4) is not None: if int(res.group(4)) < 10: rmatch += '0' rmatch += res.group(4) else: rmatch += '00' self.pg_version = str(res.group(0)) self.pg_num_version = int(rmatch) return self.pg_get_num_dev_version(text_version) def pg_get_num_dev_version(self, text_version): """ Get PostgreSQL short & numeric devel. or beta version from a string (SELECT version()). """ res = re.match( r"^(PostgreSQL|EnterpriseDB) ([0-9]+)(?:\.([0-9]+))?(devel|beta[0-9]+|rc[0-9]+)", text_version) if res is not None: rmatch = res.group(2) if res.group(3) is not None: if int(res.group(3)) < 10: rmatch += '0' rmatch += res.group(3) else: rmatch += '00' rmatch += '00' self.pg_version = str(res.group(0)) self.pg_num_version = int(rmatch) return raise Exception('Undefined PostgreSQL version.') def pg_get_db_info(self, prev_db_infos, using_rds=False, skip_sizes=False): """ Get current sum of transactions, total size and timestamp. """ query = """ SELECT EXTRACT(EPOCH FROM NOW()) AS timestamp, SUM(pg_stat_get_db_xact_commit(oid)+pg_stat_get_db_xact_rollback(oid))::BIGINT AS no_xact, {db_size} AS total_size, MAX(LENGTH(datname)) AS max_length FROM pg_database {no_rds} """.format( db_size="0" if skip_sizes else "SUM(pg_database_size(datname))", no_rds="WHERE datname <> 'rdsadmin'" if using_rds else '' ) cur = self.pg_conn.cursor() cur.execute(query,) ret = cur.fetchone() tps = 0 size_ev = 0 if prev_db_infos is not None: tps = int((ret['no_xact'] - prev_db_infos['no_xact']) / (ret['timestamp'] - prev_db_infos['timestamp'])) size_ev = float(float(ret['total_size'] - prev_db_infos['total_size']) / (ret['timestamp'] - prev_db_infos['timestamp'])) return { 'timestamp': ret['timestamp'], 'no_xact': ret['no_xact'], 'total_size': ret['total_size'], 'max_length': ret['max_length'], 'tps': tps, 'size_ev': size_ev} def pg_get_active_connections(self,): """ Get total of active connections. """ query = """ SELECT COUNT(*) as active_connections FROM pg_stat_activity WHERE state = 'active' """ cur = self.pg_conn.cursor() cur.execute(query,) ret = cur.fetchone() active_connections = int(ret['active_connections']) return active_connections def pg_get_activities(self,): """ Get activity from pg_stat_activity view. """ if self.pg_num_version >= 100000: # PostgreSQL 10 and more query = """ SELECT pg_stat_activity.pid AS pid, pg_stat_activity.application_name AS application_name, CASE WHEN LENGTH(pg_stat_activity.datname) > 16 THEN SUBSTRING(pg_stat_activity.datname FROM 0 FOR 6)||'...'||SUBSTRING(pg_stat_activity.datname FROM '........$') ELSE pg_stat_activity.datname END AS database, CASE WHEN pg_stat_activity.client_addr IS NULL THEN 'local' ELSE pg_stat_activity.client_addr::TEXT END AS client, EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, CASE WHEN pg_stat_activity.wait_event_type IN ('LWLock', 'Lock', 'BufferPin') THEN true ELSE false END AS wait, pg_stat_activity.usename AS user, pg_stat_activity.state AS state, pg_stat_activity.query AS query, pg_stat_activity.backend_type AS backend_type FROM pg_stat_activity WHERE state <> 'idle' AND pid <> pg_backend_pid() ORDER BY EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) DESC """ elif self.pg_num_version >= 90600: # PostgreSQL prior to 10.0 and >= 9.6.0 query = """ SELECT pg_stat_activity.pid AS pid, pg_stat_activity.application_name AS application_name, CASE WHEN LENGTH(pg_stat_activity.datname) > 16 THEN SUBSTRING(pg_stat_activity.datname FROM 0 FOR 6)||'...'||SUBSTRING(pg_stat_activity.datname FROM '........$') ELSE pg_stat_activity.datname END AS database, CASE WHEN pg_stat_activity.client_addr IS NULL THEN 'local' ELSE pg_stat_activity.client_addr::TEXT END AS client, EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, pg_stat_activity.wait_event IS NOT NULL AS wait, pg_stat_activity.usename AS user, pg_stat_activity.state AS state, pg_stat_activity.query AS query, null AS backend_type FROM pg_stat_activity WHERE state <> 'idle' AND pid <> pg_backend_pid() ORDER BY EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) DESC """ elif self.pg_num_version >= 90200: # PostgreSQL prior to 9.6.0 and >= 9.2.0 query = """ SELECT pg_stat_activity.pid AS pid, pg_stat_activity.application_name AS application_name, CASE WHEN LENGTH(pg_stat_activity.datname) > 16 THEN SUBSTRING(pg_stat_activity.datname FROM 0 FOR 6)||'...'||SUBSTRING(pg_stat_activity.datname FROM '........$') ELSE pg_stat_activity.datname END AS database, CASE WHEN pg_stat_activity.client_addr IS NULL THEN 'local' ELSE pg_stat_activity.client_addr::TEXT END AS client, EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, pg_stat_activity.waiting AS wait, pg_stat_activity.usename AS user, pg_stat_activity.state AS state, pg_stat_activity.query AS query, null AS backend_type FROM pg_stat_activity WHERE state <> 'idle' AND pid <> pg_backend_pid() ORDER BY EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) DESC """ elif self.pg_num_version < 90200: # PostgreSQL prior to 9.2.0 query = """ SELECT pg_stat_activity.procpid AS pid, '' AS application_name, CASE WHEN LENGTH(pg_stat_activity.datname) > 16 THEN SUBSTRING(pg_stat_activity.datname FROM 0 FOR 6)||'...'||SUBSTRING(pg_stat_activity.datname FROM '........$') ELSE pg_stat_activity.datname END AS database, CASE WHEN pg_stat_activity.client_addr IS NULL THEN 'local' ELSE pg_stat_activity.client_addr::TEXT END AS client, EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, pg_stat_activity.waiting AS wait, pg_stat_activity.usename AS user, pg_stat_activity.current_query AS query, null AS backend_type FROM pg_stat_activity WHERE current_query <> '' AND procpid <> pg_backend_pid() ORDER BY EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) DESC """ cur = self.pg_conn.cursor() cur.execute(query) ret = cur.fetchall() return ret def pg_get_waiting(self,): """ Get waiting queries. """ if self.pg_num_version >= 90200: query = """ SELECT pg_locks.pid AS pid, pg_stat_activity.application_name AS appname, CASE WHEN LENGTH(pg_stat_activity.datname) > 16 THEN SUBSTRING(pg_stat_activity.datname FROM 0 FOR 6)||'...'||SUBSTRING(pg_stat_activity.datname FROM '........$') ELSE pg_stat_activity.datname END AS database, pg_stat_activity.usename AS user, pg_locks.mode AS mode, pg_locks.locktype AS type, pg_locks.relation::regclass AS relation, EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, pg_stat_activity.state as state, pg_stat_activity.query AS query FROM pg_catalog.pg_locks JOIN pg_catalog.pg_stat_activity ON(pg_catalog.pg_locks.pid = pg_catalog.pg_stat_activity.pid) WHERE NOT pg_catalog.pg_locks.granted AND pg_catalog.pg_stat_activity.pid <> pg_backend_pid() ORDER BY EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) DESC """ elif self.pg_num_version < 90200: query = """ SELECT pg_locks.pid AS pid, '' AS appname, CASE WHEN LENGTH(pg_stat_activity.datname) > 16 THEN SUBSTRING(pg_stat_activity.datname FROM 0 FOR 6)||'...'||SUBSTRING(pg_stat_activity.datname FROM '........$') ELSE pg_stat_activity.datname END AS database, pg_stat_activity.usename AS user, pg_locks.mode AS mode, pg_locks.locktype AS type, pg_locks.relation::regclass AS relation, EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, NULL AS state, pg_stat_activity.current_query AS query FROM pg_catalog.pg_locks JOIN pg_catalog.pg_stat_activity ON(pg_catalog.pg_locks.pid = pg_catalog.pg_stat_activity.procpid) WHERE NOT pg_catalog.pg_locks.granted AND pg_catalog.pg_stat_activity.procpid <> pg_backend_pid() ORDER BY EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) DESC """ cur = self.pg_conn.cursor() cur.execute(query) ret = cur.fetchall() return ret def pg_get_blocking(self,): """ Get blocking queries """ if self.pg_num_version >= 90200: query = """ SELECT pid, application_name AS appname, CASE WHEN LENGTH(datname) > 16 THEN SUBSTRING(datname FROM 0 FOR 6)||'...'||SUBSTRING(datname FROM '........$') ELSE datname END AS database, usename AS user, relation, mode, locktype AS type, duration, state, query FROM ( SELECT blocking.pid, pg_stat_activity.application_name, pg_stat_activity.query, blocking.mode, pg_stat_activity.datname, pg_stat_activity.usename, blocking.locktype, EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, pg_stat_activity.state as state, blocking.relation::regclass AS relation FROM pg_locks AS blocking JOIN ( SELECT transactionid FROM pg_locks WHERE NOT granted) AS blocked ON (blocking.transactionid = blocked.transactionid) JOIN pg_stat_activity ON (blocking.pid = pg_stat_activity.pid) WHERE blocking.granted UNION ALL SELECT blocking.pid, pg_stat_activity.application_name, pg_stat_activity.query, blocking.mode, pg_stat_activity.datname, pg_stat_activity.usename, blocking.locktype, EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, pg_stat_activity.state as state, blocking.relation::regclass AS relation FROM pg_locks AS blocking JOIN ( SELECT database, relation, mode FROM pg_locks WHERE NOT granted AND relation IS NOT NULL) AS blocked ON (blocking.database = blocked.database AND blocking.relation = blocked.relation) JOIN pg_stat_activity ON (blocking.pid = pg_stat_activity.pid) WHERE blocking.granted ) AS sq GROUP BY pid, application_name, query, mode, locktype, duration, datname, usename, state, relation ORDER BY duration DESC """ elif self.pg_num_version < 90200: query = """ SELECT pid, CASE WHEN LENGTH(datname) > 16 THEN SUBSTRING(datname FROM 0 FOR 6)||'...'||SUBSTRING(datname FROM '........$') ELSE datname END AS database, usename AS user, relation, mode, locktype AS type, duration, state, query FROM ( SELECT blocking.pid, '' AS appname, pg_stat_activity.current_query AS query, blocking.mode, pg_stat_activity.datname, pg_stat_activity.usename, blocking.locktype,EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, NULL AS state, blocking.relation::regclass AS relation FROM pg_locks AS blocking JOIN ( SELECT transactionid FROM pg_locks WHERE NOT granted) AS blocked ON (blocking.transactionid = blocked.transactionid) JOIN pg_stat_activity ON (blocking.pid = pg_stat_activity.procpid) WHERE blocking.granted UNION ALL SELECT blocking.pid, '' AS appname, pg_stat_activity.current_query AS query, blocking.mode, pg_stat_activity.datname, pg_stat_activity.usename, blocking.locktype, EXTRACT(epoch FROM (NOW() - pg_stat_activity.query_start)) AS duration, NULL AS state, blocking.relation::regclass AS relation FROM pg_locks AS blocking JOIN ( SELECT database, relation, mode FROM pg_locks WHERE NOT granted AND relation IS NOT NULL) AS blocked ON (blocking.database = blocked.database AND blocking.relation = blocked.relation) JOIN pg_stat_activity ON (blocking.pid = pg_stat_activity.procpid) WHERE blocking.granted ) AS sq GROUP BY pid, appname, query, mode, locktype, duration, datname, usename, state, relation ORDER BY duration DESC """ cur = self.pg_conn.cursor() cur.execute(query) ret = cur.fetchall() return ret def pg_is_local(self,): """ Is pg_activity connected localy ? """ query = """ SELECT inet_server_addr() AS inet_server_addr, inet_client_addr() AS inet_client_addr """ cur = self.pg_conn.cursor() cur.execute(query) ret = cur.fetchone() if ret['inet_server_addr'] == ret['inet_client_addr']: return True return False def get_duration(self, duration): """ Returns 0 if the given duration is negative else, returns the duration """ if duration is None or float(duration) < 0: return 0 return float(duration) def __sys_get_iow_status(self, status): """ Returns 'Y' if status == 'disk sleep', else 'N' """ if status == 'disk sleep': return 'Y' else: return 'N' def sys_get_proc(self, queries, is_local): """ Get system informations (CPU, memory, IO read & write) for each process PID using psutil module. """ processes = {} if not is_local: return processes for query in queries: try: psproc = PSProcess(query['pid']) process = Process( pid = query['pid'], database = query['database'], user = query['user'], client = query['client'], duration = query['duration'], wait = query['wait'], state = query['state'], query = query['query'], extras = {}, appname = query['application_name'] ) process.set_extra('meminfo', psproc.memory_info()) process.set_extra('io_counters', psproc.io_counters()) process.set_extra('io_time', time.time()) process.set_extra('mem_percent', psproc.memory_percent()) process.set_extra('cpu_percent', psproc.cpu_percent(interval=0)) process.set_extra('cpu_times', psproc.cpu_times()) process.set_extra('read_delta', 0) process.set_extra('write_delta', 0) process.set_extra('io_wait', self.__sys_get_iow_status(psproc.status_iow())) process.set_extra('psutil_proc', psproc) process.set_extra('backend_type', query['backend_type']) process.set_extra('appname', query['application_name']) processes[process.pid] = process except psutil.NoSuchProcess: pass except psutil.AccessDenied: pass return processes def set_global_io_counters(self, read_bytes_delta, write_bytes_delta, read_count_delta, write_count_delta): """ Set IO counters. """ self.read_bytes_delta = read_bytes_delta self.write_bytes_delta = write_bytes_delta self.read_count_delta = read_count_delta self.write_count_delta = write_count_delta def get_global_io_counters(self,): """ Get IO counters. """ return { 'read_bytes': self.read_bytes_delta, 'write_bytes': self.write_bytes_delta, 'read_count': self.read_count_delta, 'write_count': self.write_count_delta} def get_mem_swap(self,): """ Get memory and swap usage """ with catch_warnings(): simplefilter("ignore", RuntimeWarning) try: # psutil >= 0.6.0 phymem = psutil.virtual_memory() buffers = psutil.virtual_memory().buffers cached = psutil.virtual_memory().cached vmem = psutil.swap_memory() except AttributeError: # psutil > 0.4.0 and < 0.6.0 phymem = psutil.phymem_usage() buffers = getattr(psutil, 'phymem_buffers', lambda: 0)() cached = getattr(psutil, 'cached_phymem', lambda: 0)() vmem = psutil.virtmem_usage() mem_used = phymem.total - (phymem.free + buffers + cached) return ( phymem.percent, mem_used, phymem.total, vmem.percent, vmem.used, vmem.total) def get_load_average(self,): """ Get load average """ return os.getloadavg() pg_activity-1.5.0/pgactivity/Process.py000066400000000000000000000040431343700417500202170ustar00rootroot00000000000000""" pg_activity author: Julien Tachoires license: PostgreSQL License Copyright (c) 2012 - 2019, Julien Tachoires Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL JULIEN TACHOIRES BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF JULIEN TACHOIRES HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. JULIEN TACHOIRES SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND JULIEN TACHOIRES HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. """ class Process(): """ Simple class for process management. """ def __init__(self, pid = None, database = None, user = None, \ client = None, cpu = None, mem = None, read = None, write = None, \ state = None, query = None, duration = None, wait = None, extras = None, \ appname = None): self.pid = pid self.database = database self.user = user self.client = client self.cpu = cpu self.mem = mem self.read = read self.write = write self.state = state self.query = query self.duration = duration self.wait = wait self.extras = extras self.appname = appname def set_extra(self, key, value): """ Set a pair of key/value in extras dict """ self.extras[key] = value def get_extra(self, key): """ Get a value from extras dict """ if self.extras is not None and key in self.extras: return self.extras[key] pg_activity-1.5.0/pgactivity/UI.py000066400000000000000000002300331343700417500171160ustar00rootroot00000000000000""" pg_activity author: Julien Tachoires license: PostgreSQL License Copyright (c) 2012 - 2019, Julien Tachoires Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL JULIEN TACHOIRES BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF JULIEN TACHOIRES HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. JULIEN TACHOIRES SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND JULIEN TACHOIRES HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. """ from __future__ import print_function from pprint import pprint import curses import re import time import sys from datetime import timedelta, datetime as dt from pgactivity.Data import Data, clean_str import psutil from getpass import getpass # Define some color pairs C_BLACK_GREEN = 1 C_CYAN = 2 C_RED = 3 C_GREEN = 4 C_YELLOW = 5 C_MAGENTA = 6 C_WHITE = 7 C_BLACK_CYAN = 8 C_RED_BLACK = 9 C_GRAY = 10 # Columns PGTOP_FLAG_DATABASE = 1 PGTOP_FLAG_APPNAME = 2 PGTOP_FLAG_CLIENT = 4 PGTOP_FLAG_USER = 8 PGTOP_FLAG_CPU = 16 PGTOP_FLAG_MEM = 32 PGTOP_FLAG_READ = 64 PGTOP_FLAG_WRITE = 128 PGTOP_FLAG_TIME = 256 PGTOP_FLAG_WAIT = 512 PGTOP_FLAG_RELATION = 1024 PGTOP_FLAG_TYPE = 2048 PGTOP_FLAG_MODE = 4096 PGTOP_FLAG_IOWAIT = 8192 PGTOP_FLAG_NONE = None # Display query mode PGTOP_TRUNCATE = 1 PGTOP_WRAP_NOINDENT = 2 PGTOP_WRAP = 3 # Cancel/Terminate backend PGTOP_SIGNAL_CANCEL_BACKEND = ord('c') PGTOP_SIGNAL_TERMINATE_BACKEND = ord('k') PGTOP_SIGNAL_MESSAGE = { PGTOP_SIGNAL_CANCEL_BACKEND: { 's': "Cancel query at backend with PID %s ? ", 'p': "Cancel query at backends with PID %s ? " }, PGTOP_SIGNAL_TERMINATE_BACKEND: { 's': "Terminate backend with PID %s ? ", 'p': "Terminate backends with PID %s ? " }, } # Maximum number of column PGTOP_MAX_NCOL = 15 PGTOP_COLS = { 'activities': { 'pid' : { 'n': 1, 'name': 'PID', 'template_h': '%-6s ', 'flag': PGTOP_FLAG_NONE, 'mandatory': True }, 'database': { 'n': 2, 'name': 'DATABASE', 'template_h': '%-16s ', 'flag': PGTOP_FLAG_DATABASE, 'mandatory': False }, 'appname': { 'n': 3, 'name': 'APP', 'template_h': '%16s ', 'flag': PGTOP_FLAG_APPNAME, 'mandatory': False }, 'user': { 'n': 4, 'name': 'USER', 'template_h': '%16s ', 'flag': PGTOP_FLAG_USER, 'mandatory': False }, 'client': { 'n': 5, 'name': 'CLIENT', 'template_h': '%16s ', 'flag': PGTOP_FLAG_CLIENT, 'mandatory': False }, 'cpu': { 'n': 6, 'name': 'CPU%', 'template_h': '%6s ', 'flag': PGTOP_FLAG_CPU, 'mandatory': False }, 'mem': { 'n': 7, 'name': 'MEM%', 'template_h': '%4s ', 'flag': PGTOP_FLAG_MEM, 'mandatory': False }, 'read': { 'n': 8, 'name': 'READ/s', 'template_h': '%8s ', 'flag': PGTOP_FLAG_READ, 'mandatory': False }, 'write': { 'n': 9, 'name': 'WRITE/s', 'template_h': '%8s ', 'flag': PGTOP_FLAG_WRITE, 'mandatory': False }, 'time': { 'n': 10, 'name': 'TIME+', 'template_h': '%9s ', 'flag': PGTOP_FLAG_TIME, 'mandatory': False }, 'wait': { 'n': 11, 'name': 'W', 'template_h': '%2s ', 'flag': PGTOP_FLAG_WAIT, 'mandatory': False }, 'iowait': { 'n': 12, 'name': 'IOW', 'template_h': '%4s ', 'flag': PGTOP_FLAG_IOWAIT, 'mandatory': False }, 'state': { 'n': 13, 'name': 'state', 'template_h': ' %17s ', 'flag': PGTOP_FLAG_NONE, 'mandatory': True }, 'query': { 'n': 14, 'name': 'Query', 'template_h': ' %2s', 'flag': PGTOP_FLAG_NONE, 'mandatory': True }, }, 'waiting': { 'pid': { 'n': 1, 'name': 'PID', 'template_h': '%-6s ', 'flag': PGTOP_FLAG_NONE, 'mandatory': True }, 'database': { 'n': 2, 'name': 'DATABASE', 'template_h': '%-16s ', 'flag': PGTOP_FLAG_DATABASE, 'mandatory': False }, 'appname': { 'n': 3, 'name': 'APP', 'template_h': '%16s ', 'flag': PGTOP_FLAG_APPNAME, 'mandatory': False }, 'relation': { 'n': 4, 'name': 'RELATION', 'template_h': '%9s ', 'flag': PGTOP_FLAG_RELATION, 'mandatory': False }, 'type': { 'n': 5, 'name': 'TYPE', 'template_h': '%16s ', 'flag': PGTOP_FLAG_TYPE, 'mandatory': False }, 'mode': { 'n': 6, 'name': 'MODE', 'template_h': '%16s ', 'flag': PGTOP_FLAG_MODE, 'mandatory': False }, 'time': { 'n': 7, 'name': 'TIME+', 'template_h': '%9s ', 'flag': PGTOP_FLAG_TIME, 'mandatory': False }, 'state': { 'n': 8, 'name': 'state', 'template_h': ' %17s ', 'flag': PGTOP_FLAG_NONE, 'mandatory': True }, 'query': { 'n': 9, 'name': 'Query', 'template_h': ' %2s', 'flag': PGTOP_FLAG_NONE, 'mandatory': True }, }, 'blocking': { 'pid': { 'n': 1, 'name': 'PID', 'template_h': '%-6s ', 'flag': PGTOP_FLAG_NONE, 'mandatory': True }, 'database': { 'n': 2, 'name': 'DATABASE', 'template_h': '%-16s ', 'flag': PGTOP_FLAG_DATABASE, 'mandatory': False }, 'appname': { 'n': 3, 'name': 'APP', 'template_h': '%16s ', 'flag': PGTOP_FLAG_APPNAME, 'mandatory': False }, 'relation': { 'n': 4, 'name': 'RELATION', 'template_h': '%9s ', 'flag': PGTOP_FLAG_RELATION, 'mandatory': False }, 'type': { 'n': 5, 'name': 'TYPE', 'template_h': '%16s ', 'flag': PGTOP_FLAG_TYPE, 'mandatory': False }, 'mode': { 'n': 6, 'name': 'MODE', 'template_h': '%16s ', 'flag': PGTOP_FLAG_MODE, 'mandatory': False }, 'time': { 'n': 7, 'name': 'TIME+', 'template_h': '%9s ', 'flag': PGTOP_FLAG_TIME, 'mandatory': False }, 'state': { 'n': 8, 'name': 'state', 'template_h': ' %17s ', 'flag': PGTOP_FLAG_NONE, 'mandatory': True }, 'query': { 'n': 9, 'name': 'Query', 'template_h': ' %2s', 'flag': PGTOP_FLAG_NONE, 'mandatory': True }, } } MAP_STATES = { 'idle in transaction': 'idle in trans', 'idle in transaction (aborted)': 'idle in trans (a)', } def bytes2human(num): """ Convert a size into a human readable format. """ symbols = ('K', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y') prefix = {} nume = '' if num < 0: num = num * -1 nume = '-' for pos, sym in enumerate(symbols): prefix[sym] = 1 << (pos+1)*10 for sym in reversed(symbols): if num >= prefix[sym]: value = "%.2f" % float(float(num) / float(prefix[sym])) return "%s%s%s" % (nume, value, sym) return "%s%.2fB" % (nume, num) class UI: """ UI class """ def __init__(self, version): """ Constructor. """ self.version = version self.win = None self.sys_color = True self.lineno = 0 self.lines = [] # Maximum number of columns self.max_ncol = 13 # Default self.verbose_mode = PGTOP_WRAP_NOINDENT # Max IOPS self.max_iops = 0 # Sort self.sort = 't' # Color self.color = True # Default mode : activites, waiting, blocking self.mode = 'activities' # Does pg_activity is connected to a local PG server ? self.is_local = True # Start line self.start_line = 5 # Window's size self.maxy = 0 self.maxx = 0 # Init uibuffer self.uibuffer = None # Refresh time self.refresh_time = 2 # Maximum DATABASE columns header length self.max_db_length = 16 # Array containing pid of processes to yank self.pid_yank = [] self.pid = [] # Data collector self.data = Data() # Maximum number of column self.max_ncol = PGTOP_MAX_NCOL # Default filesystem blocksize self.fs_blocksize = 4096 # Output file self.output = None def set_verbose_mode(self, verbose_mode): """ Set self.verbose_mode """ self.verbose_mode = verbose_mode def get_verbose_mode(self,): """ Get self.verbose_mode """ return self.verbose_mode def set_is_local(self, is_local): """ Set self.is_local """ self.is_local = is_local def get_is_local(self,): """ Get self.is_local """ return self.is_local def get_mode(self,): """ Get self.mode """ return self.mode def set_start_line(self, start_line): """ Set self.start_line """ self.start_line = start_line def set_buffer(self, uibuffer): """ Set self.uibuffer """ self.uibuffer = uibuffer def set_blocksize(self, blocksize): """ Set blocksize """ if not isinstance(blocksize, int): raise Exception('Unvalid blocksize value.') if blocksize != 0 and not ((blocksize & (blocksize - 1)) == 0): raise Exception('Unvalid blocksize value.') if not blocksize > 0: raise Exception('Unvalid blocksize value.') self.fs_blocksize = int(blocksize) def init_curses(self,): """ Initialize curses environment and colors. """ self.__init_curses() # Columns colors definition self.line_colors = { 'pid': { 'default': self.__get_color(C_CYAN), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'database': { 'default': curses.A_BOLD|self.__get_color(C_GRAY), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'appname': { 'default': curses.A_BOLD|self.__get_color(C_GRAY), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'user': { 'default': curses.A_BOLD|self.__get_color(C_GRAY), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'client': { 'default': self.__get_color(C_CYAN), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'cpu': { 'default': self.__get_color(0), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'mem': { 'default': self.__get_color(0), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'read': { 'default': self.__get_color(0), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'write': { 'default': self.__get_color(0), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'time_red': { 'default': self.__get_color(C_RED), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'time_yellow': { 'default': self.__get_color(C_YELLOW), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'time_green': { 'default': self.__get_color(C_GREEN), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'wait_green': { 'default': self.__get_color(C_GREEN)|curses.A_BOLD, 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'wait_red': { 'default': self.__get_color(C_RED)|curses.A_BOLD, 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'state_default': { 'default': self.__get_color(0), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'state_yellow': { 'default': self.__get_color(C_YELLOW), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'state_green': { 'default': self.__get_color(C_GREEN), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'state_red': { 'default': self.__get_color(C_RED), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'query': { 'default': self.__get_color(0), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'relation': { 'default': self.__get_color(C_CYAN), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'type': { 'default': self.__get_color(0), 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'mode_yellow': { 'default': self.__get_color(C_YELLOW)|curses.A_BOLD, 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD }, 'mode_red': { 'default': self.__get_color(C_RED)|curses.A_BOLD, 'cursor': self.__get_color(C_CYAN)|curses.A_REVERSE, 'yellow': self.__get_color(C_YELLOW)|curses.A_BOLD } } def __init_curses(self,): """ Initialize curses environment. """ curses.setupterm() self.win = curses.initscr() self.win.keypad(1) curses.noecho() try: # deactivate cursor curses.curs_set(0) # use colors curses.start_color() curses.use_default_colors() except Exception: # Terminal doesn't support curs_set() and colors self.sys_color = False curses.cbreak() curses.endwin() self.win.scrollok(0) (self.maxy, self.maxx) = self.win.getmaxyx() def get_flag_from_options(self, options): """ Returns the flag depending on the options. """ flag = PGTOP_FLAG_DATABASE | PGTOP_FLAG_USER | PGTOP_FLAG_CLIENT flag = flag | PGTOP_FLAG_CPU | PGTOP_FLAG_MEM | PGTOP_FLAG_READ flag = flag | PGTOP_FLAG_WRITE | PGTOP_FLAG_TIME | PGTOP_FLAG_WAIT flag = flag | PGTOP_FLAG_RELATION | PGTOP_FLAG_TYPE | PGTOP_FLAG_MODE flag = flag | PGTOP_FLAG_IOWAIT | PGTOP_FLAG_APPNAME if options.nodb is True: flag -= PGTOP_FLAG_DATABASE if options.nouser is True: flag -= PGTOP_FLAG_USER if options.nocpu is True: flag -= PGTOP_FLAG_CPU if options.noclient is True: flag -= PGTOP_FLAG_CLIENT if options.nomem is True: flag -= PGTOP_FLAG_MEM if options.noread is True: flag -= PGTOP_FLAG_READ if options.nowrite is True: flag -= PGTOP_FLAG_WRITE if options.notime is True: flag -= PGTOP_FLAG_TIME if options.nowait is True: flag -= PGTOP_FLAG_WAIT if options.noappname is True: flag -= PGTOP_FLAG_APPNAME # Remove some if no running against local pg server. if not self.get_is_local() and (flag & PGTOP_FLAG_CPU): flag -= PGTOP_FLAG_CPU if not self.get_is_local() and (flag & PGTOP_FLAG_MEM): flag -= PGTOP_FLAG_MEM if not self.get_is_local() and (flag & PGTOP_FLAG_READ): flag -= PGTOP_FLAG_READ if not self.get_is_local() and (flag & PGTOP_FLAG_WRITE): flag -= PGTOP_FLAG_WRITE if not self.get_is_local() and (flag & PGTOP_FLAG_IOWAIT): flag -= PGTOP_FLAG_IOWAIT return flag def __get_color(self, color): """ Wrapper around curses.color_pair() """ if self.sys_color: return curses.color_pair(color) else: return 0 def set_max_db_length(self, new_length): """ Set new DATABASE column length """ global PGTOP_COLS if new_length > 16: new_length = 16 if new_length < 8: new_length = 8 self.max_db_length = new_length str_nl = str(new_length) PGTOP_COLS['activities']['database']['template_h'] = '%-'+str_nl+'s ' PGTOP_COLS['waiting']['database']['template_h'] = '%-'+str_nl+'s ' PGTOP_COLS['blocking']['database']['template_h'] = '%-'+str_nl+'s ' def at_exit_curses(self,): """ Called at exit time. Rollback to default values. """ try: self.win.keypad(0) self.win.move(0, 0) self.win.erase() except KeyboardInterrupt: pass except AttributeError: # Curses not initialized yet return curses.nocbreak() curses.echo() try: curses.curs_set(1) except Exception: pass curses.endwin() def signal_handler(self, signal, frame): """ Function called on a process kill. """ self.at_exit_curses() print("FATAL: Killed with signal %s ." % (str(signal),)) print("%s" % (str(frame),)) sys.exit(1) def set_nocolor(self,): """ Replace colors by white. """ if not self.sys_color: return self.color = False curses.init_pair(C_BLACK_GREEN, curses.COLOR_BLACK, curses.COLOR_WHITE) curses.init_pair(C_CYAN, curses.COLOR_WHITE, -1) curses.init_pair(C_RED, curses.COLOR_WHITE, -1) curses.init_pair(C_RED_BLACK, curses.COLOR_WHITE, curses.COLOR_BLACK) curses.init_pair(C_GREEN, curses.COLOR_WHITE, -1) curses.init_pair(C_YELLOW, curses.COLOR_WHITE, -1) curses.init_pair(C_MAGENTA, curses.COLOR_WHITE, -1) curses.init_pair(C_WHITE, curses.COLOR_WHITE, -1) curses.init_pair(C_BLACK_CYAN, curses.COLOR_WHITE, -1) curses.init_pair(C_GRAY, curses.COLOR_WHITE, -1) def set_color(self,): """ Set colors. """ if not self.sys_color: return self.color = True curses.init_pair(C_BLACK_GREEN, curses.COLOR_BLACK, curses.COLOR_GREEN) curses.init_pair(C_CYAN, curses.COLOR_CYAN, -1) curses.init_pair(C_RED, curses.COLOR_RED, -1) curses.init_pair(C_RED_BLACK, curses.COLOR_RED, curses.COLOR_BLACK) curses.init_pair(C_GREEN, curses.COLOR_GREEN, -1) curses.init_pair(C_YELLOW, curses.COLOR_YELLOW, -1) curses.init_pair(C_MAGENTA, curses.COLOR_MAGENTA, -1) curses.init_pair(C_WHITE, curses.COLOR_WHITE, -1) curses.init_pair(C_BLACK_CYAN, curses.COLOR_BLACK, curses.COLOR_CYAN) curses.init_pair(C_GRAY, 0, -1) def set_output(self, output): self.output = output def clean_str(self, string): """ Strip and replace some special characters. """ msg = str(string) msg = msg.replace("\n", " ") msg = re.sub(r"\s+", r" ", msg) msg = msg.replace("FATAL:", "") msg = re.sub(r"^\s", r"", msg) msg = re.sub(r"\s$", r"", msg) return msg def ask_password(self, ): """ Ask for PostgreSQL user password """ password = getpass() return password def check_window_size(self,): """ Update window's size """ (self.maxy, self.maxx) = self.win.getmaxyx() return def __get_pause_msg(self,): """ Returns PAUSE message, depending of the line size """ msg = "PAUSE" line = "" line += " " * (int(self.maxx/2) - len(msg)) line += msg line += " " * (self.maxx - len(line) - 0) return line def __pause(self,): """ PAUSE mode """ self.__print_string( self.start_line, 0, self.__get_pause_msg(), self.__get_color(C_RED_BLACK)|curses.A_REVERSE|curses.A_BOLD) while 1: try: k = self.win.getch() except KeyboardInterrupt as err: raise err if k == ord('q'): curses.endwin() exit() if k == ord(' '): curses.flushinp() return 0 if k == curses.KEY_RESIZE: if self.uibuffer is not None and 'procs' in self.uibuffer: self.check_window_size() self.refresh_window( self.uibuffer['procs'], self.uibuffer['extras'], self.uibuffer['flag'], self.uibuffer['indent'], self.uibuffer['io'], self.uibuffer['tps'], self.uibuffer['active_connections'], self.uibuffer['size_ev'], self.uibuffer['total_size']) self.__print_string( self.start_line, 0, self.__get_pause_msg(), self.__get_color(C_RED_BLACK)|\ curses.A_REVERSE|curses.A_BOLD) curses.flushinp() def __current_position(self,): """ Display current mode """ if self.mode == 'activities': msg = "RUNNING QUERIES" if self.mode == 'waiting': msg = "WAITING QUERIES" if self.mode == 'blocking': msg = "BLOCKING QUERIES" color = self.__get_color(C_GREEN) line = "" line += " " * (int(self.maxx/2) - len(msg)) line += msg line += " " * (self.maxx - len(line) - 0) self.__print_string(self.start_line, 0, line, color|curses.A_BOLD) def __help_key_interactive(self,): """ Display interactive mode menu bar """ colno = self.__print_string( (self.maxy - 1), 0, "c", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Cancel current query ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, "k", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Terminate the backend ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, "Space", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Tag/untag the process ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, "Other", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Back to activity ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, "q", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Quit ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, self.__add_blank(" "), self.__get_color(C_CYAN)|curses.A_REVERSE) def __change_mode_interactive(self,): """ Display change mode menu bar """ colno = self.__print_string( (self.maxy - 1), 0, "F1/1", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Running queries ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, "F2/2", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Waiting queries ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, "F3/3", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Blocking queries ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, "Space", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Pause ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, "q", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Quit ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, "h", self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, "Help ", self.__get_color(C_CYAN)|curses.A_REVERSE) colno += self.__print_string( (self.maxy - 1), colno, self.__add_blank(" ", colno + 1), self.__get_color(C_CYAN)|curses.A_REVERSE) def __ask_terminate_or_cancel_backends(self, action, pids,): """ Ask for cancelling or terminating some backends """ if len(pids) == 1: colno = self.__print_string( (self.maxy - 1), 0, PGTOP_SIGNAL_MESSAGE[action]['s'] % (str(pids[0]),), self.__get_color(0)) else: pos = 0 disp = "" for pid in pids: if pos > 5: disp += "..." break if pos > 0: disp += ", " disp += "%s" % (pid,) pos += 1 colno = self.__print_string( (self.maxy - 1), 0, PGTOP_SIGNAL_MESSAGE[action]['p'] % (str(disp),), self.__get_color(0)) colno += self.__print_string( (self.maxy - 1), colno, self.__add_blank(" "), self.__get_color(C_CYAN)|curses.A_REVERSE) while 1: try: key = self.win.getch() except KeyboardInterrupt as err: raise err # quit if key == ord('q'): curses.endwin() exit() # yes if key == ord('y') or key == ord('Y'): for pid in pids: if action == PGTOP_SIGNAL_TERMINATE_BACKEND: self.data.pg_terminate_backend(str(pid),) else: self.data.pg_cancel_backend(str(pid),) self.__empty_pid_yank() return 1 # no if key == ord('n') or key == ord('N') or key == ord(' '): return 0 # resize => exit if key == curses.KEY_RESIZE: return 0 def __empty_pid_yank(self,): """ Empty pid list to be yanked """ self.pid_yank = [] def __check_pid_yank(self,): """ Check if PIDs in PGTOP_PID_YANK list are still attached to live processes """ if len(self.pid_yank) > 0: for pid in self.pid_yank: if self.pid.count(pid) == 0: self.pid_yank.remove(pid) def __interactive(self, process, flag, indent,): """ Interactive mode trigged on KEY_UP or KEY_DOWN key press If no key hit during 3 seconds, exit this mode """ # Force truncated display old_verbose_mode = self.verbose_mode self.verbose_mode = PGTOP_TRUNCATE # Refresh lines with this verbose mode self.__scroll_window(process, flag, indent, 0) self.__help_key_interactive() current_pos = 0 offset = 0 self.__refresh_line( process[current_pos], flag, indent, 'cursor', self.lines[current_pos] - offset) self.win.timeout(int(1000)) nb_nk = 0 while 1: known = False try: k = self.win.getch() except KeyboardInterrupt as err: raise err if k == -1: nb_nk += 1 # quit if k == ord('q'): curses.endwin() exit() # terminate/cancel the backend attached to this PID if k == PGTOP_SIGNAL_TERMINATE_BACKEND or k == PGTOP_SIGNAL_CANCEL_BACKEND: if len(self.pid_yank) == 0: self.__ask_terminate_or_cancel_backends( \ k, [process[current_pos]['pid']],) else: self.__ask_terminate_or_cancel_backends(k, self.pid_yank,) self.verbose_mode = old_verbose_mode curses.flushinp() return 0 # Move cursor if k == curses.KEY_DOWN or k == curses.KEY_UP: nb_nk = 0 known = True if k == curses.KEY_UP and current_pos > 0: if (self.lines[current_pos] - offset)\ < (self.start_line + 3): offset -= 1 self.__scroll_window(process, flag, indent, offset) self.__help_key_interactive() if current_pos < len(process): self.__refresh_line( process[current_pos], flag, indent, 'default', self.lines[current_pos] - offset) current_pos -= 1 if k == curses.KEY_DOWN and current_pos < (len(process) - 1): if (self.lines[current_pos] - offset) >= (self.maxy - 2): offset += 1 self.__scroll_window(process, flag, indent, offset) self.__help_key_interactive() if current_pos >= 0: self.__refresh_line( process[current_pos], flag, indent, 'default', self.lines[current_pos] - offset) current_pos += 1 self.__refresh_line( process[current_pos], flag, indent, 'cursor', self.lines[current_pos] - offset) curses.flushinp() continue # Add/remove a PID from the yank list if k == ord(' '): known = True if not self.pid_yank.count(process[current_pos]['pid']) > 0: self.pid_yank.append(process[current_pos]['pid']) else: self.pid_yank.remove(process[current_pos]['pid']) self.__refresh_line( process[current_pos], flag, indent, 'default', self.lines[current_pos] - offset) if current_pos < (len(process) - 1): current_pos += 1 if (self.lines[current_pos] - offset) >= (self.maxy - 1): offset += 1 self.__scroll_window(process, flag, indent, offset) self.__help_key_interactive() self.__refresh_line( process[current_pos], flag, indent, 'cursor', self.lines[current_pos] - offset) # Quit interactive mode if (k != -1 and not known) or k == curses.KEY_RESIZE: self.verbose_mode = old_verbose_mode curses.flushinp() return 0 curses.flushinp() if nb_nk > 3: self.verbose_mode = old_verbose_mode return 0 def poll(self, interval, flag, indent, process = None, disp_proc = None): """ Wrapper around polling """ if self.mode == 'activities': return self.__poll_activities( interval, flag, indent, process, disp_proc) elif self.mode == 'waiting' or self.mode == 'blocking': return self.__poll_waiting_blocking( interval, flag, indent, process, disp_proc) def __poll_activities(self, interval, flag, indent, process = None, \ disp_proc = None): """ Poll activities. """ # Keyboard interactions self.win.timeout(int(1000 * self.refresh_time * interval)) t_start = time.time() known = False do_refresh = False try: key = self.win.getch() except KeyboardInterrupt as err: raise err if key == ord('q'): curses.endwin() exit() # PAUSE mode if key == ord(' '): self.__pause() do_refresh = True # interactive mode if (key == curses.KEY_DOWN or key == curses.KEY_UP) and \ len(disp_proc) > 0: self.__interactive(disp_proc, flag, indent) known = False do_refresh = True # show waiting queries if (key == curses.KEY_F2 or key == ord('2')): self.mode = 'waiting' self.sort = 't' curses.flushinp() return self.__poll_waiting_blocking(0, flag, indent) # show blocking queries if (key == curses.KEY_F3 or key == ord('3')): self.mode = 'blocking' self.sort = 't' curses.flushinp() return self.__poll_waiting_blocking(0, flag, indent) # change verbosity if key == ord('v'): self.verbose_mode += 1 if self.verbose_mode > 3: self.verbose_mode = 1 do_refresh = True # turn off/on colors if key == ord('C'): if self.color is True: self.set_nocolor() else: self.set_color() do_refresh = True # sorts if key == ord('c') and (flag & PGTOP_FLAG_CPU) and self.sort != 'c': self.sort = 'c' known = True if key == ord('m') and (flag & PGTOP_FLAG_MEM) and self.sort != 'm': self.sort = 'm' known = True if key == ord('r') and (flag & PGTOP_FLAG_READ) and self.sort != 'r': self.sort = 'r' known = True if key == ord('w') and (flag & PGTOP_FLAG_WRITE) and self.sort != 'w': self.sort = 'w' known = True if key == ord('t') and self.sort != 't': self.sort = 't' known = True if key == ord('+') and self.refresh_time < 3: self.refresh_time += 1 do_refresh = True if key == ord('-') and self.refresh_time > 1: self.refresh_time -= 1 do_refresh = True # Refresh if key == ord('R'): known = True if key == ord('u'): self.__empty_pid_yank() known = True if key == ord('h'): self.__help_window() do_refresh = True if key == curses.KEY_RESIZE and \ self.uibuffer is not None and \ 'procs' in self.uibuffer: do_refresh = True if do_refresh is True and \ self.uibuffer is not None and \ type(self.uibuffer) is dict and \ 'procs' in self.uibuffer: self.check_window_size() self.refresh_window( self.uibuffer['procs'], self.uibuffer['extras'], self.uibuffer['flag'], self.uibuffer['indent'], self.uibuffer['io'], self.uibuffer['tps'], self.uibuffer['active_connections'], self.uibuffer['size_ev'], self.uibuffer['total_size']) curses.flushinp() t_end = time.time() if key > -1 and not known and \ (t_end - t_start) < (self.refresh_time * interval): return self.__poll_activities( ((self.refresh_time * interval) - \ (t_end - t_start))/self.refresh_time, flag, indent, process, disp_proc) # poll postgresql activity queries = self.data.pg_get_activities() self.pid = [] if self.is_local: # get resource usage for each process new_procs = self.data.sys_get_proc(queries, self.is_local) procs = [] read_bytes_delta = 0 write_bytes_delta = 0 read_count_delta = 0 write_count_delta = 0 for pid, new_proc in new_procs.items(): try: if pid in process: n_io_time = time.time() # Getting informations from the previous loop proc = process[pid] # Update old process with new informations proc.duration = new_proc.duration proc.state = new_proc.state proc.query = new_proc.query proc.appname = new_proc.appname proc.client = new_proc.client proc.wait = new_proc.wait proc.set_extra( 'io_wait', new_proc.get_extra('io_wait')) proc.set_extra( 'read_delta', (new_proc.get_extra('io_counters').read_bytes - proc.get_extra('io_counters').read_bytes) / (n_io_time - proc.get_extra('io_time'))) proc.set_extra( 'write_delta', (new_proc.get_extra('io_counters').write_bytes - proc.get_extra('io_counters').write_bytes) / (n_io_time - proc.get_extra('io_time'))) proc.set_extra( 'io_counters', new_proc.get_extra('io_counters')) proc.set_extra( 'io_time', n_io_time) # Global io counters read_bytes_delta += proc.get_extra('read_delta') write_bytes_delta += proc.get_extra('write_delta') else: # No previous information about this process proc = new_proc if not self.pid.count(pid): self.pid.append(pid) proc.set_extra( 'mem_percent', proc.get_extra('psutil_proc').memory_percent()) proc.set_extra( 'cpu_percent', proc.get_extra('psutil_proc').\ cpu_percent(interval=0)) new_procs[pid] = proc procs.append({ 'pid': pid, 'appname': proc.appname, 'database': proc.database, 'user':proc.user, 'client': proc.client, 'cpu': proc.get_extra('cpu_percent'), 'mem': proc.get_extra('mem_percent'), 'read': proc.get_extra('read_delta'), 'write': proc.get_extra('write_delta'), 'state': proc.state, 'query': proc.query, 'duration': self.data.get_duration(proc.duration), 'wait': proc.wait, 'io_wait': proc.get_extra('io_wait'), 'backend_type': proc.get_extra('backend_type') }) except psutil.NoSuchProcess: pass except psutil.AccessDenied: pass except Exception as err: raise err # store io counters if read_bytes_delta > 0: read_count_delta += int(read_bytes_delta/self.fs_blocksize) if write_bytes_delta > 0: write_count_delta += int(write_bytes_delta/self.fs_blocksize) self.data.set_global_io_counters( read_bytes_delta, write_bytes_delta, read_count_delta, write_count_delta) else: procs = [] new_procs = None for query in queries: if not self.pid.count(query['pid']): self.pid.append(query['pid']) procs.append({ 'pid': query['pid'], 'appname': query['application_name'], 'database': query['database'], 'user': query['user'], 'client': query['client'], 'state': query['state'], 'query': query['query'], 'duration': self.data.get_duration(query['duration']), 'wait': query['wait'] }) # return processes sorted by query duration if self.sort == 't': # TIME disp_procs = sorted( procs, key=lambda p: p['duration'], reverse=True) elif self.sort == 'c': # CPU disp_procs = sorted( procs, key=lambda p: p['cpu'], reverse=True) elif self.sort == 'm': # MEM disp_procs = sorted( procs, key=lambda p: p['mem'], reverse=True) elif self.sort == 'r': # READ disp_procs = sorted( procs, key=lambda p: p['read'], reverse=True) elif self.sort == 'w': # WRITE disp_procs = sorted( procs, key=lambda p: p['write'], reverse=True) else: disp_procs = sorted( procs, key=lambda p: p['duration'], reverse=True) # Store querie list if self.output is not None: self.__store_procs(procs) self.__check_pid_yank() return (disp_procs, new_procs) def __poll_waiting_blocking(self, interval, flag, indent, \ process = None, disp_proc = None): """ Poll waiting or blocking queries """ t_start = time.time() do_refresh = False known = False # Keyboard interactions self.win.timeout(int(1000 * self.refresh_time * interval)) try: k = self.win.getch() except KeyboardInterrupt as err: raise err if k == ord('q'): curses.endwin() exit() # PAUSE mode if k == ord(' '): self.__pause() do_refresh = True # interactive mode if (k == curses.KEY_DOWN or k == curses.KEY_UP) and \ len(disp_proc) > 0: self.__interactive(disp_proc, flag, indent) known = True # activities mode if (k == curses.KEY_F1 or k == ord('1')): self.mode = 'activities' curses.flushinp() queries = self.data.pg_get_activities() procs = self.data.sys_get_proc(queries, self.is_local) return self.__poll_activities(0, flag, indent, procs) # Waiting queries if (k == curses.KEY_F2 or k == ord('2')) and \ self.mode != 'waiting': self.mode = 'waiting' curses.flushinp() return self.__poll_waiting_blocking(0, flag, indent) # blocking queries if (k == curses.KEY_F3 or k == ord('3')) and \ self.mode != 'blocking': self.mode = 'blocking' curses.flushinp() return self.__poll_waiting_blocking(0, flag, indent) # change verbosity if k == ord('v'): self.verbose_mode += 1 if self.verbose_mode > 3: self.verbose_mode = 1 do_refresh = True # turnoff/on colors if k == ord('C'): if self.color is True: self.set_nocolor() else: self.set_color() do_refresh = True # sorts if k == ord('t') and self.sort != 't': self.sort = 't' known = True if k == ord('+') and self.refresh_time < 3: self.refresh_time += 1 if k == ord('-') and self.refresh_time > 1: self.refresh_time -= 1 if k == ord('h'): self.__help_window() do_refresh = True # Refresh if k == ord('R'): known = True if k == curses.KEY_RESIZE and self.uibuffer is not None and \ 'procs' in self.uibuffer: do_refresh = True if do_refresh is True and self.uibuffer is not None and \ 'procs' in self.uibuffer: self.check_window_size() self.refresh_window( self.uibuffer['procs'], self.uibuffer['extras'], self.uibuffer['flag'], self.uibuffer['indent'], self.uibuffer['io'], self.uibuffer['tps'], self.uibuffer['active_connections'], self.uibuffer['size_ev'], self.uibuffer['total_size']) curses.flushinp() t_end = time.time() if k > -1 and \ not known and \ (t_end - t_start) < (self.refresh_time * interval): return self.__poll_waiting_blocking( ((self.refresh_time * interval) -\ (t_end - t_start))/self.refresh_time, flag, indent, process, disp_proc) # poll postgresql activity if self.mode == 'waiting': queries = self.data.pg_get_waiting() else: queries = self.data.pg_get_blocking() new_procs = {} for query in queries: new_procs[query['pid']] = query new_procs[query['pid']][6] = \ self.data.get_duration(query['duration']) # return processes sorted by query duration disp_procs = sorted( queries, key=lambda q: q['duration'], reverse=True) return (disp_procs, new_procs) def print_string(self, lineno, colno, word, color = 0): return self.__print_string(lineno, colno, word, color) def __print_string(self, lineno, colno, word, color = 0): """ Print a string at position (lineno, colno) and returns its length. """ try: self.win.addstr(lineno, colno, word, color) except curses.error: pass return len(word) def __add_blank(self, line, offset = 0): """ Complete string with white spaces from the end of string to the end of line. """ line += " " * (self.maxx - (len(line) + offset)) return line def get_indent(self, flag): """ Returns identation for Query column. """ indent = '' res = [0] * self.max_ncol for _, val in PGTOP_COLS[self.mode].items(): if val['mandatory'] or \ (not val['mandatory'] and val['flag'] & flag): res[int(val['n'])] = val for val in res: if val is not 0: if val['name'] is not 'Query': indent += val['template_h'] % ' ' return indent def __print_cols_header(self, flag): """ Print columns headers """ line = '' disp = '' xpos = 0 res = [0] * self.max_ncol color = self.__get_color(C_GREEN) for _, val in PGTOP_COLS[self.mode].items(): if val['mandatory'] or \ (not val['mandatory'] and val['flag'] & flag): res[int(val['n'])] = val for val in res: if val is not 0: disp = val['template_h'] % val['name'] if ((val['name'] == "CPU%" and self.sort == 'c') or (val['name'] == "MEM%" and self.sort == 'm') or (val['name'] == "READ/s" and self.sort == 'r') or (val['name'] == "WRITE/s" and self.sort == 'w') or (val['name'] == "TIME+" and self.sort == 't')): color_highlight = self.__get_color(C_CYAN) else: color_highlight = color if val['name'] == "Query": disp += " " * (self.maxx - (len(line) + len(disp))) line += disp self.__print_string( self.lineno, xpos, disp, color_highlight|curses.A_REVERSE) xpos += len(disp) self.lineno += 1 def __print_header(self, pg_version, hostname, user, host, \ port, database, ios, tps, active_connections, size_ev, total_size): """ Print window header """ self.lineno = 0 colno = 0 version = " %s" % (pg_version) colno = self.__print_string( self.lineno, colno, version) colno += self.__print_string( self.lineno, colno, " - ") colno += self.__print_string( self.lineno, colno, hostname, curses.A_BOLD) colno += self.__print_string( self.lineno, colno, " - ") colno += self.__print_string( self.lineno, colno, user, self.__get_color(C_CYAN)) colno += self.__print_string( self.lineno, colno, "@") colno += self.__print_string( self.lineno, colno, host, self.__get_color(C_CYAN)) colno += self.__print_string( self.lineno, colno, ":") colno += self.__print_string( self.lineno, colno, port, self.__get_color(C_CYAN)) colno += self.__print_string( self.lineno, colno, "/") colno += self.__print_string( self.lineno, colno, database, self.__get_color(C_CYAN)) colno += self.__print_string( self.lineno, colno, " - Ref.: %ss" % (self.refresh_time,)) colno = 0 self.lineno += 1 colno += self.__print_string( self.lineno, colno, " Size: ") colno += self.__print_string( self.lineno, colno, "%8s" % (bytes2human(total_size),),) colno += self.__print_string( self.lineno, colno, " - %9s/s" % (bytes2human(size_ev),),) colno += self.__print_string( self.lineno, colno, " | TPS: ") colno += self.__print_string( self.lineno, colno, "%11s" % (tps,), self.__get_color(C_GREEN)|curses.A_BOLD) colno += self.__print_string( self.lineno, colno, " | Active Connections: ") colno += self.__print_string( self.lineno, colno, "%11s" % (active_connections,), self.__get_color(C_GREEN)|curses.A_BOLD) # If not local connection, don't get and display system informations if not self.is_local: return # Get memory & swap usage (mem_used_per, mem_used, mem_total, swap_used_per, \ swap_used, swap_total) = self.data.get_mem_swap() # Get load average (av1, av2, av3) = self.data.get_load_average() self.lineno += 1 line = " Mem.: %6s0%% - %9s/%-8s" % \ (mem_used_per, bytes2human(mem_used), \ bytes2human(mem_total)) colno_io = self.__print_string(self.lineno, 0, line) if (int(ios['read_count'])+int(ios['write_count'])) > self.max_iops: self.max_iops = (int(ios['read_count'])+int(ios['write_count'])) line_io = " | IO Max: %8s/s" % (self.max_iops,) colno = self.__print_string(self.lineno, colno_io, line_io) # swap usage line = " Swap: %6s0%% - %9s/%-8s" % \ (swap_used_per, bytes2human(swap_used), \ bytes2human(swap_total)) self.lineno += 1 colno = self.__print_string(self.lineno, 0, line) line_io = " | Read : %10s/s - %6s/s" % \ (bytes2human(ios['read_bytes']), int(ios['read_count']),) colno = self.__print_string(self.lineno, colno_io, line_io) # load average, uptime line = " Load: %.2f %.2f %.2f" % (av1, av2, av3) self.lineno += 1 colno = self.__print_string(self.lineno, 0, line) line_io = " | Write: %10s/s - %6s/s" % \ (bytes2human(ios['write_bytes']), int(ios['write_count']),) colno = self.__print_string(self.lineno, colno_io, line_io) def __help_window(self,): """ Display help window """ self.win.erase() self.lineno = 0 text = "pg_activity %s - (c) 2012-2019 Julien Tachoires" % \ (self.version) self.__print_string( self.lineno, 0, text, self.__get_color(C_GREEN)|curses.A_BOLD) self.lineno += 1 text = "Released under PostgreSQL License." self.__print_string( self.lineno, 0, text) self.lineno += 2 self.__display_help_key( self.lineno, 00, "Up/Down", "scroll process list") self.__display_help_key( self.lineno, 45, " C", "activate/deactivate colors") self.lineno += 1 self.__display_help_key( self.lineno, 00, " Space", "pause") self.__display_help_key( self.lineno, 45, " r", "sort by READ/s desc. (activities)") self.lineno += 1 self.__display_help_key( self.lineno, 00, " v", "change display mode") self.__display_help_key( self.lineno, 45, " w", "sort by WRITE/s desc. (activities)") self.lineno += 1 self.__display_help_key( self.lineno, 00, " q", "quit") self.__display_help_key( self.lineno, 45, " c", "sort by CPU% desc. (activities)") self.lineno += 1 self.__display_help_key( self.lineno, 00, " +", "increase refresh time (max:3)") self.__display_help_key( self.lineno, 45, " m", "sort by MEM% desc. (activities)") self.lineno += 1 self.__display_help_key( self.lineno, 00, " -", "decrease refresh time (min:1)") self.__display_help_key( self.lineno, 45, " t", "sort by TIME+ desc. (activities)") self.lineno += 1 self.__display_help_key( self.lineno, 00, " R", "force refresh") self.lineno += 1 self.__print_string( self.lineno, 0, "Mode") self.lineno += 1 self.__display_help_key( self.lineno, 00, " F1/1", "running queries") self.lineno += 1 self.__display_help_key( self.lineno, 00, " F2/2", "waiting queries") self.lineno += 1 self.__display_help_key( self.lineno, 00, " F3/3", "blocking queries") self.lineno += 2 self.__print_string( self.lineno, 0, "Press any key to exit.") self.win.timeout(-1) try: self.win.getch() except KeyboardInterrupt as err: raise err def __display_help_key(self, lineno, colno, key, help_msg): """ Display help key """ pos1 = self.__print_string( lineno, colno, key, self.__get_color(C_CYAN)|curses.A_BOLD) pos2 = self.__print_string( lineno, colno + pos1, ": %s" % (help_msg,)) return (colno + pos1 + pos2) def refresh_window(self, procs, extras, flag, indent, ios, \ tps, active_connections, size_ev, total_size): """ Refresh the window """ self.lines = [] (pg_version, hostname, user, host, port, dbname) = extras self.win.erase() self.__print_header( pg_version, hostname, user, host, port, dbname, ios, tps, active_connections, size_ev, total_size) self.lineno += 2 line_trunc = self.lineno self.__current_position() self.__print_cols_header(flag) for proc in procs: try: self.__refresh_line(proc, flag, indent, 'default') line_trunc += 1 self.lines.append(line_trunc) except curses.error: break for line in range(self.lineno, (self.maxy-1)): self.__print_string(line, 0, self.__add_blank(" ")) self.__change_mode_interactive() def __scroll_window(self, procs, flag, indent, offset = 0): """ Scroll the window """ self.lineno = (self.start_line + 2) pos = 0 for proc in procs: if pos >= offset and self.lineno < (self.maxy - 1): self.__refresh_line(proc, flag, indent, 'default') pos += 1 for line in range(self.lineno, (self.maxy-1)): self.__print_string(line, 0, self.__add_blank(" ")) def __refresh_line(self, process, flag, indent, \ typecolor = 'default', line = None): """ Refresh a line for activities mode """ if line is not None: l_lineno = line else: l_lineno = self.lineno if typecolor == 'default' and self.pid_yank.count(process['pid']) > 0: typecolor = 'yellow' colno = 0 colno += self.__print_string( l_lineno, colno, "%-6s " % (process['pid'],), self.line_colors['pid'][typecolor]) process['query'] = clean_str(process['query']) if flag & PGTOP_FLAG_DATABASE: colno += self.__print_string( l_lineno, colno, PGTOP_COLS[self.mode]['database']['template_h'] % \ (str(process['database'])[:16],), self.line_colors['database'][typecolor]) if self.mode == 'activities': if flag & PGTOP_FLAG_APPNAME: colno += self.__print_string( l_lineno, colno, "%16s " % (str(process['appname'])[:16],), self.line_colors['appname'][typecolor]) if flag & PGTOP_FLAG_USER: colno += self.__print_string( l_lineno, colno, "%16s " % (str(process['user'])[:16],), self.line_colors['user'][typecolor]) if flag & PGTOP_FLAG_CLIENT: colno += self.__print_string( l_lineno, colno, "%16s " % (str(process['client'])[:16],), self.line_colors['client'][typecolor]) if flag & PGTOP_FLAG_CPU: colno += self.__print_string( l_lineno, colno, "%6s " % (process['cpu'],), self.line_colors['cpu'][typecolor]) if flag & PGTOP_FLAG_MEM: colno += self.__print_string( l_lineno, colno, "%4s " % (round(process['mem'], 1),), self.line_colors['mem'][typecolor]) if flag & PGTOP_FLAG_READ: colno += self.__print_string( l_lineno, colno, "%8s " % (bytes2human(process['read']),), self.line_colors['read'][typecolor]) if flag & PGTOP_FLAG_WRITE: colno += self.__print_string( l_lineno, colno, "%8s " % (bytes2human(process['write']),), self.line_colors['write'][typecolor]) elif self.mode == 'waiting' or self.mode == 'blocking': if flag & PGTOP_FLAG_APPNAME: colno += self.__print_string( l_lineno, colno, "%16s " % (str(process['appname'])[:16],), self.line_colors['appname'][typecolor]) if flag & PGTOP_FLAG_RELATION: colno += self.__print_string( l_lineno, colno, "%9s " % (str(process['relation'])[:9],), self.line_colors['relation'][typecolor]) if flag & PGTOP_FLAG_TYPE: colno += self.__print_string( l_lineno, colno, "%16s " % (str(process['type'])[:16],), self.line_colors['type'][typecolor]) if flag & PGTOP_FLAG_MODE: if process['mode'] == 'ExclusiveLock' or \ process['mode'] == 'RowExclusiveLock' or \ process['mode'] == 'AccessExclusiveLock': colno += self.__print_string( l_lineno, colno, "%16s " % (str(process['mode'])[:16],), self.line_colors['mode_red'][typecolor]) else: colno += self.__print_string( l_lineno, colno, "%16s " % (str(process['mode'])[:16],), self.line_colors['mode_yellow'][typecolor]) if flag & PGTOP_FLAG_TIME: if process['duration'] >= 1 and process['duration'] < 60000: ctime = timedelta(seconds=float(process['duration'])) mic = '%.6d' % (ctime.microseconds) ctime = "%s:%s.%s" % (str((ctime.seconds // 60)).zfill(2), \ str((ctime.seconds % 60)).zfill(2), str(mic)[:2]) elif process['duration'] >= 60000: ctime = "%s h" % str(int(process['duration'] / 3600)) if process['duration'] < 1: colno += self.__print_string( l_lineno, colno, " %.6f " % (process['duration'],), self.line_colors['time_green'][typecolor]) elif process['duration'] >= 1 and process['duration'] < 3: colno += self.__print_string( l_lineno, colno, "%9s " % (ctime,), self.line_colors['time_yellow'][typecolor]) else: colno += self.__print_string( l_lineno, colno, "%9s " % (ctime,), self.line_colors['time_red'][typecolor]) if self.mode == 'activities' and flag & PGTOP_FLAG_WAIT: if process['wait']: colno += self.__print_string( l_lineno, colno, "%2s " % ('Y',), self.line_colors['wait_red'][typecolor]) else: colno += self.__print_string( l_lineno, colno, "%2s " % ('N',), self.line_colors['wait_green'][typecolor]) if self.mode == 'activities' and flag & PGTOP_FLAG_IOWAIT: if process['io_wait'] == 'Y': colno += self.__print_string( l_lineno, colno, "%4s " % ('Y',), self.line_colors['wait_red'][typecolor]) else: colno += self.__print_string( l_lineno, colno, "%4s " % ('N',), self.line_colors['wait_green'][typecolor]) state = MAP_STATES.get(process['state']) or process['state'] if state == 'active': color_state = 'state_green' elif state == 'idle in trans': color_state = 'state_yellow' elif state == 'idle in trans (a)': color_state = 'state_red' else: color_state = 'state_default' colno += self.__print_string( l_lineno, colno, " %17s " % state, self.line_colors[color_state][typecolor]) dif = self.maxx - len(indent) - 1 query = '' if 'backend_type' in process and \ process['backend_type'] == 'background worker': query += '\_ ' if self.verbose_mode == PGTOP_TRUNCATE: query += process['query'][:dif] colno += self.__print_string( l_lineno, colno, " %s" % (self.__add_blank(query, len(indent)+1),), self.line_colors['query'][typecolor]) elif self.verbose_mode == PGTOP_WRAP or \ self.verbose_mode == PGTOP_WRAP_NOINDENT: query += process['query'] query_wrote = '' offset = 0 if len(query) > dif and dif > 1: query_part = query[offset:dif] self.__print_string( l_lineno, colno, " %s" % (self.__add_blank(query_part, len(indent)+1),), self.line_colors['query'][typecolor]) query_wrote += query_part offset = len(query_wrote) if self.verbose_mode == PGTOP_WRAP_NOINDENT: dif = self.maxx p_indent = "" else: p_indent = indent while (len(query) - offset > 0): query_part = query[offset:(dif+offset)] l_lineno += 1 self.lineno += 1 self.__print_string( l_lineno, 0, "%s" % (self.__add_blank(p_indent + " " + \ query_part, len(indent)+1)), self.line_colors['query'][typecolor]) query_wrote += query_part offset = len(query_wrote) else: colno += self.__print_string( l_lineno, colno, " %s" % (self.__add_blank(query, len(indent)),), self.line_colors['query'][typecolor]) self.lineno += 1 def __clean_str_csv(self, string): # clean string for CSV format s = clean_str(string) s = s.replace('"', '\\"') return s def __store_procs(self, procs): # Store process list into CSV file with open(self.output, 'a') as f: if f.tell() == 0: # First line then write CSV header f.write("datetimeutc;pid;database;appname;user;client;cpu;" "memory;read;write;duration;wait;io_wait;state;" "query\n") for p in procs: f.write("\"{dt}\";\"{pid}\";\"{database}\";\"{appname}\";" "\"{user}\";\"{client}\";\"{cpu}\";\"{mem}\";" "\"{read}\";\"{write}\";\"{duration}\";\"{wait}\";" "\"{io_wait}\";\"{state}\";\"{query}\"\n".format( dt=dt.utcnow().strftime("%Y-%m-%dT%H:%m:%SZ"), pid=p.get('pid', 'N/A'), database=p.get('database', 'N/A'), appname=p.get('appname', 'N/A'), user=p.get('user', 'N/A'), client=p.get('client', 'N/A'), cpu=p.get('cpu', 'N/A'), mem=p.get('mem', 'N/A'), read=p.get('read', 'N/A'), write=p.get('write', 'N/A'), duration=p.get('duration', 'N/A'), wait=p.get('wait', 'N/A'), io_wait=p.get('io_wait', 'N/A'), state=p.get('state', 'N/A'), query=self.__clean_str_csv(p.get('query', 'N/A')) ) ) pg_activity-1.5.0/pgactivity/__init__.py000066400000000000000000000000001343700417500203250ustar00rootroot00000000000000pg_activity-1.5.0/setup.py000066400000000000000000000014331343700417500155560ustar00rootroot00000000000000import sys data_files = None for opt in sys.argv: if opt == '--with-man': data_files = [ ('/usr/share/man/man1', ['docs/man/pg_activity.1']) ] sys.argv.remove(opt) from setuptools import setup if sys.version_info < (2, 6): raise SystemExit('ERROR: pg_activity need at least python 2.6 to work.') setup( name = 'pg_activity', version = '1.5.0', author = 'Julien Tachoires', author_email = 'julmon@gmail.com', scripts = ['pg_activity'], packages = ['pgactivity'], url = 'https://github.com/julmon/pg_activity', license = 'LICENSE.txt', description = 'Command line tool for PostgreSQL server activity monitoring.', install_requires = [ "psutil >= 0.4.1", "psycopg2 >= 2.2.1", ], data_files = data_files, )