From aba4036ab69b70cc463221ef2a49b07b323ff4eb Mon Sep 17 00:00:00 2001 From: unclecode Date: Fri, 17 Oct 2025 22:43:06 +0800 Subject: [PATCH] Add demo and test scripts for monitor dashboard activity - Introduced a demo script (`demo_monitor_dashboard.py`) to showcase various monitoring features through simulated activity. - Implemented a test script (`test_monitor_demo.py`) to generate dashboard activity and verify monitor health and endpoint statistics. - Added a logo image to the static assets for branding purposes. --- deploy/docker/api.py | 33 +++- deploy/docker/server.py | 9 + deploy/docker/static/assets/crawl4ai-logo.jpg | Bin 0 -> 5920 bytes deploy/docker/static/assets/crawl4ai-logo.png | Bin 0 -> 1622 bytes deploy/docker/static/assets/logo.png | Bin 0 -> 11243 bytes deploy/docker/static/monitor/index.html | 182 ++++++++---------- deploy/docker/tests/demo_monitor_dashboard.py | 164 ++++++++++++++++ deploy/docker/tests/test_monitor_demo.py | 57 ++++++ 8 files changed, 338 insertions(+), 107 deletions(-) create mode 100644 deploy/docker/static/assets/crawl4ai-logo.jpg create mode 100644 deploy/docker/static/assets/crawl4ai-logo.png create mode 100644 deploy/docker/static/assets/logo.png create mode 100755 deploy/docker/tests/demo_monitor_dashboard.py create mode 100644 deploy/docker/tests/test_monitor_demo.py diff --git a/deploy/docker/api.py b/deploy/docker/api.py index 605b0c8a..64ac4a85 100644 --- a/deploy/docker/api.py +++ b/deploy/docker/api.py @@ -460,12 +460,22 @@ async def handle_crawl_request( hooks_config: Optional[dict] = None ) -> dict: """Handle non-streaming crawl requests with optional hooks.""" + # Track request start + request_id = f"req_{uuid4().hex[:8]}" + try: + from monitor import get_monitor + await get_monitor().track_request_start( + request_id, "/crawl", urls[0] if urls else "batch", browser_config + ) + except: + pass # Monitor not critical + start_mem_mb = _get_memory_mb() # <--- Get memory before start_time = time.time() mem_delta_mb = None peak_mem_mb = start_mem_mb hook_manager = None - + try: urls = [('https://' + url) if not url.startswith(('http://', 'https://')) and not url.startswith(("raw:", "raw://")) else url for url in urls] browser_config = BrowserConfig.load(browser_config) @@ -570,7 +580,16 @@ async def handle_crawl_request( "server_memory_delta_mb": mem_delta_mb, "server_peak_memory_mb": peak_mem_mb } - + + # Track request completion + try: + from monitor import get_monitor + await get_monitor().track_request_end( + request_id, success=True, pool_hit=True, status_code=200 + ) + except: + pass + # Add hooks information if hooks were used if hooks_config and hook_manager: from hook_manager import UserHookManager @@ -599,6 +618,16 @@ async def handle_crawl_request( except Exception as e: logger.error(f"Crawl error: {str(e)}", exc_info=True) + + # Track request error + try: + from monitor import get_monitor + await get_monitor().track_request_end( + request_id, success=False, error=str(e), status_code=500 + ) + except: + pass + if 'crawler' in locals() and crawler.ready: # Check if crawler was initialized and started # try: # await crawler.close() diff --git a/deploy/docker/server.py b/deploy/docker/server.py index efb1cecb..364f4457 100644 --- a/deploy/docker/server.py +++ b/deploy/docker/server.py @@ -174,6 +174,15 @@ app.mount( name="monitor_ui", ) +# ── static assets (logo, etc) ──────────────────────────────── +ASSETS_DIR = pathlib.Path(__file__).parent / "static" / "assets" +if ASSETS_DIR.exists(): + app.mount( + "/static/assets", + StaticFiles(directory=ASSETS_DIR), + name="assets", + ) + @app.get("/") async def root(): diff --git a/deploy/docker/static/assets/crawl4ai-logo.jpg b/deploy/docker/static/assets/crawl4ai-logo.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6a808c043126f1c691e7bbf81c766a4980b734cf GIT binary patch literal 5920 zcmb7IWmHvL*WM@SI&c8#JakBRN`rJsDi}1dl~q97#=0uqA4 zx4rki_xtmWcZ_eI^<$5*=UijW{XFZLbDrzD>#qPpQ&mG1fIt8M0{(#O1waXaL7{&Z zXmD_0;bLLI;aK=MIM}#^_=JQ6_yhz*#AFB}Vp3uP0t6+36iH4&K|x4zgNl-zij16s z{Ldy37#IV`!o$MCBPSvtBL9Eebq_#_3k(1QFbE3(C56C9A=iBX9e7CCUkmt;VdLOp zL7;FL_?8yT0}A_-%)bW5g2J$IAlIJ&LNE@BfFZz4heG~+|9?Js`S(kee5%JZUwOyg zw_s)M-jQ4por5)<5q~-4+_v~R?U`0dv(i$d2Ifvcyg2?iyTUdM3lJzfEO)!X0kL)7P$B^{6oe8yI20* zo!zCpE-eOOB5Snq{EggWvb_h|CzgZ01Xev;S6M}Tr9u;kHzDA8X}%>)n#frBr=zX` zUZ>$lC+k0|Bp4qWJFZtqPSJ|9%=k(%c4c~Zl+)d+-_acVDi{w2C8~N^Zx8E=d2Mr1 zFse31e+t}A;%ygL=IDPlhOXq#^i;S8G~R#6Tzp#SE%_&^hpeQ55G(lA+|_ z5BRG9ssn&PYGrUsB*RpcW<8<;bog&355mZf0H9DP$e@3rgk!<5Ay9yvLI4Lr%0x!# zh7<%5#RZXuU_r0M+F7@~j2aEq_((-E0R91u zSN=yg%h+nG>n1l;;2ti7$y)3+&(~L~rXBX8gN{;7_h3g`_It2vV9}Jz^KHmQg5$*j zg>Cm6*kPJ3<+SA&$g4XerG2BzJ|z4nu1hyLn;xT`SkEV*`a)DHCT;D-k)QYsKxZyn&MXb~){mex_fQ8E!(ZsGNw_{H$;%Ck@{Zi#yL+m~t z(-Y++E_KEEOW|ze^c>-Hyrv_SL&x)aIb3?RlpAKdYv((g>p_0`JslRw=#GRxAHoH0 z53KKFer_ZMs#!7eCBMCkg9=n(6B?h7R8(xGhQR-;<%BfqCJ4fkF{CUmLwMv22z?5p=fmKe~CX<~0KmY)V_ z4O^k@)_!kOE)Jp>$=pX%I_G^4$3YGWk^&GI6oQ9?^&dHaKw)qIi@ zQGAw%`Ob>cbFb@O{)E00L@Hw#>9uMg;>j-{K5w3HRdze&*eH}ltBzWCu!b1}iVRyZYTpElzz7n~Xx z8(-P0_D0rm)OF}GRdt*sz-OJlMOb2`TFGoC;fTPAmWWx$?BN3*N0Q^kYJo&Id#TwQ zFU()?xmgb}*jD>KsOp&V?DD?`xMv>=-$?0%>`YcC_K6*9yEAGwEF3GT`@Y;+Lgn?a zGTGK-a>Ll+mJFtMr?rEH3c30SAtkHrbRpYr{gsmo-thx-g}?f%Z}X$xGFz=B#{DX0 zR8Hxw+L!vc8F*yt;Vl3>UnI_i& z{>8-IwZe}&!rn2@jyD}y*vuEX3^bgNu67prCF=@Ae9ot<7avI`RYQ*P0n|Nd{*>Q* z)b^_kZ@I~vx&t}=qa3E>LRN@-tDW_e+__ClnuLa^6kO_R9W@%&627_Lc|Mufql`*w z*1$opTmvAQY^0zffPw&u`#<6VssW4?<*LWMW3>kt^!k{DmO&4}urvmp=UN{1wf? z?nl_ccvkoYUTsNPoJX3YSt@)=ftfC54<9u=KU9&GjuY1R5#uGVqrY3jq?z?nVTxT% zD3eHgav@hYJCCPTmjR{xY1@6(~#JU81nT}Zufi*keR zoh{O&yzZkiCHfCjonL)8kh-Qit0PkuF|T-PXXZ0q?j9kgWNM3sk|v={*gy)dFZdqu<0^K7gxQT zRx@T~nO0QRRGR14fEahp1gK^*<_`!}m&d2~J>DbC1Ijqemh z%XFCMTqbQVnok-{H%{Y}88v+xUG`OFq%00%)7_ul9fen12#C$UEkm8Tq&{o(77!gn zkB27|)1M0+7^Xb8((ITY`xF-u)OZumxR~CJ`5JJ+R`S%DQ(<^CsVkhCJi6PzU|NNG za@g>_6ay8` zwHRy%MR*y+Z&VP-7;yT1wlq3u?nvPI9+3S|2lHL5^&TJF?Y#<`r*f15=`CN&+Eh(6 z?NvEGxeN(6dQA|#_C4T`W*{kcb$yX!vmhBP_5lu^<(E?% z4qPQxX2_q1wdrC75*mztX6)f%@dG!pKigt`^FY_GPx9h5Q1xp_=MTcUStA79C|ACV z3%~rmA&n9!^QZe<&dYj3}0AAN|bAaf@)$?-$ZCNX`UmtUj~c;L?i#@QI}jm>5gpq%xm7O7mDr-w}$$oQW~(S|t~rsxml?h#(S z!Qyk2!Iac)&0w3@TbE1>i4{1Ihn5ssla*UNi{|wG^@(t_iN}U6c-4U;y=mRkU#`$w zfHvFLm6eC>HV15v|3uPWbZU6ia<6okkw0^CL^4UuX+w4aOW%zO_#0LNQRPW% zrb9>H9;2(>gud=o_kh|ZyC@YB+E?j1mzFmj9Ry>pf$Dgtdo8Qp1tp;+Ca4>bP4v*x zEBQn5?VtrOK5lf(d*{>=-nI`kuS2LSA-+irKtQ~;L!ifNd z6blN<%D?3iAY~TRSF}M0xc?E6?@V;9Mw<=j^4!{F&XAOiamNQ__RNkwB$;8@sIcNjH3Ufgkch!)9szL2gS_2Q{cjqVfj*}WZfv3fo_A{J?P zG&5CVlCSJ(JjFMM-JgMi>%!-J+wW)Ri)d$C$Yx8paQ5Hs6gaWL#5-`dUcpXg!$bBE z3@|&wk8wVXa>AnuL5)S-;(b6pc7;QtLkHahc{WP*Y|J~Z$@w3r%QXtJRnoFK=PW?R z91~2fGty~Q1|qt%o08*>Uu0HY8$yBo${)$q!GLY)@ecfy<~^a>x5b$z_TBRRIm>@* z?6VupID1clVk?kKRw#tt^^J!n_Iml8)1vAS;tYOVM!8rtI6}E^<^h_T+1qV*x0x0a) zEb(=|xnEx7n{Yg0HJHpck2KpeHEOYK^EMyfJpHkuvwr`})mA{#Z9kw!V=n0z9ZiN{ zllGg)8YTGF4>TtUmd=Y(D!HK7e3xBm|$aPFb5Wz$e7kFkj4;Se2pIQ`!Y zV_7K6VOz`QVA*A1rk|XxJ-26Houwbs7Go2l+xmK(+2pJJJ$^xz7#?!n%QCA-qk5P1 z!u*}~YZH{Yb@kSCP4sukTNt?w1{-SjCesiq;d=c?Jbp~Sh{sDJ-rN!Uv3u&Z{2(pG zGc=Ki2Pdd9qGL!SA!y-4ePI3E$8kKz&yP!;@EN?X0lc5urbf;8wG3kS+zPUh1;5_P zwtW=k7Mly&n}I?K$)G$9>(lRCd&{yv-oCn0<#pS(X~VWkc}+4x8UKDIx?d8lT^34+Es~xKSBkpW=>UC^-9mJr8Bk)D^8d&WDE9_q*CGe**BaX*q z{@dD2J07RL20*8=gZqnI9C4tx-0V@FDs2!Lc9f*WRo(d~{;LCW>NM`pUOZR_T(oz= z2U*c2nWOQJ|F%2m58i2ZQt+mNJuVL9-#bmptf&tN+PE|Qxzd>LbqKwG`&`5|kjNvF zfGSwqxIH0mgq#gR)1(W&dCxt>f_|Pw;&ZLw*&&q{ zQx7!;nB@Z<);hoSEqe~FmQ*ToHuzoiE|fND*Hi5_Gs-d~(;?(P2DwZ^Xnv$_E*K%CqMjY1jg*+3qjF=SrD8Iw*P|}T_f`9kFV-aw^oBAY$PIgLltseP4QQmH4ug65bfLC9+0oLwH#Qw1mQzlJ zLr7ek6|y_RJ^}yP02Nq~N#DjD97_;-&tCs$Ecw&aB2HfyFIURA zu`Z;q2NVcXD?xX7Hi`NJwb`80L`N2N9@tGh)@)kY5{{5MO|!BX!$kC@(=ooyuZ;22 zy1$&9$s4yzuejcau`+W~Y}QPYvm{U>b|0-`#@{b^|MI6Ho4V;G-A1O#1RSTk$FSyx zhWUv19yeW6)==v6)kqj14K|Fsn27kTk)xH0$HhPBC5981qX>}B&}To+UZQWhh;w2WPK;s0jI!8UUt-5CHRHa?8W~)1ZQxfu$v(kB!ys<(Ul?m0DO9 zDw!#TbQPf&?M+f|8Dp&EHEt6_^5E{?M?k@XF!Bs)dP_);dhr|H4?3;3vQbFFJ?Jzn z0Y8nn*oEn4>y-N>k7!RPnOi}GnS=+IQh9&(B<^fWl2BC`Yu#1#fr>iv0|%`h-}+}y zfsH-6m$<-+&ubvk*d7*?ku`H~Aa#I0+2-Dvs@nh{EJ>D4Z*b&*XmqK-Jk-&qBh92V z2Aq}EudoP|xPOUN0X58o%8tdZQ$!2U3zpVc#KRay-&~SbyiO^m=n^4TW-@k(hq%n< z;fUhgbvSB;&@HM6n-nLR=1ei<$-7OP)SF6D*zXKL$Y@DRtb(KxzAB9INg#RygQVeL|VX*Wly(A!iPMY@IRQ&s-K zxU?K#@jZ08esraSTIVKPfy+5LL;<44PQk3x`+F?m8y4*J>{mQ2Ho?m)8|`czc7yEF z4<1-J>Q)n$#T*HFPr#vJH!x7baXTInemK{lp0OSlFK$vX6_jQzI8&T?I4Q;==HaGs zuT+0EVV{?BR00-Xicisf4ItcLfc<(!M0l&TCP@m!uYWS0_3ksFcH<>pqCX(9$COeY zt5cFS_@IptkP$q3;o93}UpXMbb9iqaV6(YAZ=*I|#T^T0a*2^5A0gp)ycNp7g*%zd zb%K0{Ow{R#p<^!7#p+{6W%o2_rpI%78a|RvbU}on0}`z1aBTL9&cyWLWy%l*mpdST zQWu-aT1v$FYk>&x9J2@IQ_ipt5ve2@C9tYhov4@#-1jER+$K$0Y@@03$aq7oo3#>adFU zUmQ?AIn^`MyNp9^8p=ybONOPzYrgG9g|Fq0*(C@QBDqOr+y@%vtcwnE)O}twfAX^? zdhV?4ETEbN5_R*&y4S(Rn|G0O>+a}y~JU12>}FcPzs4AiaugUcoPT&g$FMBfPuuQTw96=z1kLO0f{Z; zl3wJryPprUr)STeJ*7;t+1=TH{_mS_{`u!$p|vJ5O(3|@^NnNJJ#Ab7h8z>rPb}k* z`R0L56nM@d@HEgMhvQXX94(i?xEzmTTF=nJh5dLtgR~z@j-WtcpsCF7%Gf(uKq)=o z{(Av%nITNqS8xj^AS4ip0*(Waz`&sC%bB%5yglGTm@x!kJ3u&M`Uv=C@DXqa52xX5 z-wyldv#?K`0@ths`ul-U2xD&pVQl*Q!FS$;ec~kSGiSk?8X%o^mCSA7+*i7s1mxbm zaOTbfQzg;eBfF2gtm0< zbc6&VVW58im^cy6p~K({C4gf?ZrciZ^eC820wW{7skq=PSHk)F8$e2kjt+!YzXkU6 z0P#35JnY7UM@4OEj$Uz#IwYw9Y9XjYA?lJ67M;65)peVy^CtE3<+K3mWRlvl%(!Hd z7Qonw6;z!!sk*vZbp8T$NlDfpKrJP8JnrI|u~`L#1fDzr=g$YKs(|iph*Kv`FqM_T zK6nV2Fab!X5&H0b;Ph!AnKVNmJpz|5h5hYbAQAz3{zYi*8lbruNF=~F-UOb14mfuX z;{17_NSOf7Q6L_tjz+0x&ZN5DNHs9T*v?(F!Xa7$)RmP~*RE6D>ZWRKqpq%|28
{Q2l+2>e`>wsg&uDgjw{}ZmNMHs>WvO@^XVQs)A;gfY#K-#YU>qQmVRos=kMe zZU2;(l*ZwlIaH0!hD38Kb!8=Wc{$aMCaPOq24wDBV=E0qf zyX>AA$gF3=7|xnyzE@7Tu*iBvUzT6r(V2x)GoGl8>78_ zRLw2abLJYK$wUsgOTc122@_O$;XUtw}b+sF`2_KHR!ugrZ(^Qd3OaS0`vfSgxZVp@{nCBZ!)Kyj4CD`&2Rc{|vODpxfd07>7 z-V%5^62-=_qQZdm-e>HS8d^fol7c#wqH6fVta(ctbyXGh?AcT;Z6;4#y+&P8;o3{a zcGNO@{{dCQRq7ct#tK41e|_(2~}&G z*&X#)Xic9^OA1=y5M!TzLDfHKlt`thqY;BuC=p@dLa_WLh`V(I~{7yO3*F1MM9UmIY_oGH}u)h`Kt6ix>Tg z$g>OL7PS;6!DLeL)Tvaz{Z7?=o2tE&vDcOrOvU>36;vHJsk&~n=$Buqr%a&^hpB}( z6jhG3jBzva*4bN5KEL;g)2|E$gCDQ`#|K0hfT^k;^HQ z#iT3%9z?)XlsXyL{)#yJBC~_McjTr}$6JDVfr6ehsoO(1FCBCADo~b>=Z7PxLJs=w zmp#{B7W`|>QDrPbegFpa6}Fd!K4c3=|N8oEgT5Df_uG#WC^W-o`4xT|nc$`RAG*CS UCSNaJPyhe`07*qoM6N<$f)VKA_5c6? literal 0 HcmV?d00001 diff --git a/deploy/docker/static/assets/logo.png b/deploy/docker/static/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..259118536616e6ac142775c6b9b9d02cd546eb9d GIT binary patch literal 11243 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

- 📊 Crawl4AI Monitor + Crawl4AI + Monitor GitHub stars @@ -90,7 +91,7 @@
@@ -170,85 +171,78 @@ - -
-
- - - - -
- -
- -
-
-

Active Requests (0)

- + +
+ +
+
+

📝 Requests (0 active)

+ +
+
+
+
No active requests
- -
-
-
No active requests
-
- -

Recent Completed

-
-
No completed requests
-
+

Recent Completed

+
+
No completed requests
+
- -
+ +
@@ -313,34 +307,14 @@ // ========== State Management ========== let autoRefresh = true; let refreshInterval; - const REFRESH_RATE = 5000; // 5 seconds + const REFRESH_RATE = 1000; // 1 second - // ========== Tab Switching ========== - document.querySelectorAll('.activity-tab').forEach(btn => { - btn.addEventListener('click', () => { - const tab = btn.dataset.tab; - - // Update tabs - document.querySelectorAll('.activity-tab').forEach(b => { - b.classList.remove('bg-dark', 'text-primary'); - }); - btn.classList.add('bg-dark', 'text-primary'); - - // Update content - document.querySelectorAll('.activity-content').forEach(c => c.classList.add('hidden')); - document.getElementById(`tab-${tab}`).classList.remove('hidden'); - - // Fetch specific data - if (tab === 'browsers') fetchBrowsers(); - if (tab === 'janitor') fetchJanitorLog(); - if (tab === 'errors') fetchErrors(); - }); - }); + // No more tabs - all sections visible at once! // ========== Auto-refresh Toggle ========== document.getElementById('auto-refresh-toggle').addEventListener('click', function() { autoRefresh = !autoRefresh; - this.textContent = autoRefresh ? 'ON ⚡5s' : 'OFF'; + this.textContent = autoRefresh ? 'ON ⚡1s' : 'OFF'; this.classList.toggle('bg-primary'); this.classList.toggle('bg-dark'); this.classList.toggle('text-dark'); @@ -367,6 +341,9 @@ await Promise.all([ fetchHealth(), fetchRequests(), + fetchBrowsers(), + fetchJanitorLog(), + fetchErrors(), fetchEndpointStats(), fetchTimeline() ]); @@ -475,29 +452,24 @@ const tbody = document.getElementById('browsers-table-body'); if (data.browsers.length === 0) { - tbody.innerHTML = 'No browsers'; + tbody.innerHTML = 'No browsers'; } else { tbody.innerHTML = data.browsers.map(b => { const typeIcon = b.type === 'permanent' ? '🔥' : b.type === 'hot' ? '♨️' : '❄️'; const typeColor = b.type === 'permanent' ? 'text-primary' : b.type === 'hot' ? 'text-accent' : 'text-light'; return ` - - ${typeIcon} ${b.type.toUpperCase()} - ${b.sig} - ${formatSeconds(b.age_seconds)} - ${formatSeconds(b.last_used_seconds)} ago - ${b.memory_mb} MB - ${b.hits} - + + ${typeIcon} + ${b.sig} + ${formatSeconds(b.age_seconds)} + ${formatSeconds(b.last_used_seconds)} + ${b.hits} + ${b.killable ? ` - - + ` : ` - + `} diff --git a/deploy/docker/tests/demo_monitor_dashboard.py b/deploy/docker/tests/demo_monitor_dashboard.py new file mode 100755 index 00000000..699988a5 --- /dev/null +++ b/deploy/docker/tests/demo_monitor_dashboard.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +""" +Monitor Dashboard Demo Script +Generates varied activity to showcase all monitoring features for video recording. +""" +import httpx +import asyncio +import time +from datetime import datetime + +BASE_URL = "http://localhost:11235" + +async def demo_dashboard(): + print("🎬 Monitor Dashboard Demo - Starting...\n") + print(f"📊 Dashboard: {BASE_URL}/dashboard") + print("=" * 60) + + async with httpx.AsyncClient(timeout=60.0) as client: + + # Phase 1: Simple requests (permanent browser) + print("\n🔷 Phase 1: Testing permanent browser pool") + print("-" * 60) + for i in range(5): + print(f" {i+1}/5 Request to /crawl (default config)...") + try: + r = await client.post( + f"{BASE_URL}/crawl", + json={"urls": [f"https://httpbin.org/html?req={i}"], "crawler_config": {}} + ) + print(f" ✅ Status: {r.status_code}, Time: {r.elapsed.total_seconds():.2f}s") + except Exception as e: + print(f" ❌ Error: {e}") + await asyncio.sleep(1) # Small delay between requests + + # Phase 2: Create variant browsers (different configs) + print("\n🔶 Phase 2: Testing cold→hot pool promotion") + print("-" * 60) + viewports = [ + {"width": 1920, "height": 1080}, + {"width": 1280, "height": 720}, + {"width": 800, "height": 600} + ] + + for idx, viewport in enumerate(viewports): + print(f" Viewport {viewport['width']}x{viewport['height']}:") + for i in range(4): # 4 requests each to trigger promotion at 3 + try: + r = await client.post( + f"{BASE_URL}/crawl", + json={ + "urls": [f"https://httpbin.org/json?v={idx}&r={i}"], + "browser_config": {"viewport": viewport}, + "crawler_config": {} + } + ) + print(f" {i+1}/4 ✅ {r.status_code} - Should see cold→hot after 3 uses") + except Exception as e: + print(f" {i+1}/4 ❌ {e}") + await asyncio.sleep(0.5) + + # Phase 3: Concurrent burst (stress pool) + print("\n🔷 Phase 3: Concurrent burst (10 parallel)") + print("-" * 60) + tasks = [] + for i in range(10): + tasks.append( + client.post( + f"{BASE_URL}/crawl", + json={"urls": [f"https://httpbin.org/delay/2?burst={i}"], "crawler_config": {}} + ) + ) + + print(" Sending 10 concurrent requests...") + start = time.time() + results = await asyncio.gather(*tasks, return_exceptions=True) + elapsed = time.time() - start + + successes = sum(1 for r in results if not isinstance(r, Exception) and r.status_code == 200) + print(f" ✅ {successes}/10 succeeded in {elapsed:.2f}s") + + # Phase 4: Multi-endpoint coverage + print("\n🔶 Phase 4: Testing multiple endpoints") + print("-" * 60) + endpoints = [ + ("/md", {"url": "https://httpbin.org/html", "f": "fit", "c": "0"}), + ("/screenshot", {"url": "https://httpbin.org/html"}), + ("/pdf", {"url": "https://httpbin.org/html"}), + ] + + for endpoint, payload in endpoints: + print(f" Testing {endpoint}...") + try: + if endpoint == "/md": + r = await client.post(f"{BASE_URL}{endpoint}", json=payload) + else: + r = await client.post(f"{BASE_URL}{endpoint}", json=payload) + print(f" ✅ {r.status_code}") + except Exception as e: + print(f" ❌ {e}") + await asyncio.sleep(1) + + # Phase 5: Intentional error (to populate errors tab) + print("\n🔷 Phase 5: Generating error examples") + print("-" * 60) + print(" Triggering invalid URL error...") + try: + r = await client.post( + f"{BASE_URL}/crawl", + json={"urls": ["invalid://bad-url"], "crawler_config": {}} + ) + print(f" Response: {r.status_code}") + except Exception as e: + print(f" ✅ Error captured: {type(e).__name__}") + + # Phase 6: Wait for janitor activity + print("\n🔶 Phase 6: Waiting for janitor cleanup...") + print("-" * 60) + print(" Idle for 40s to allow janitor to clean cold pool browsers...") + for i in range(40, 0, -10): + print(f" {i}s remaining... (Check dashboard for cleanup events)") + await asyncio.sleep(10) + + # Phase 7: Final stats check + print("\n🔷 Phase 7: Final dashboard state") + print("-" * 60) + + r = await client.get(f"{BASE_URL}/monitor/health") + health = r.json() + print(f" Memory: {health['container']['memory_percent']:.1f}%") + print(f" Browsers: Perm={health['pool']['permanent']['active']}, " + f"Hot={health['pool']['hot']['count']}, Cold={health['pool']['cold']['count']}") + + r = await client.get(f"{BASE_URL}/monitor/endpoints/stats") + stats = r.json() + print(f"\n Endpoint Stats:") + for endpoint, data in stats.items(): + print(f" {endpoint}: {data['count']} req, " + f"{data['avg_latency_ms']:.0f}ms avg, " + f"{data['success_rate_percent']:.1f}% success") + + r = await client.get(f"{BASE_URL}/monitor/browsers") + browsers = r.json() + print(f"\n Pool Efficiency:") + print(f" Total browsers: {browsers['summary']['total_count']}") + print(f" Memory usage: {browsers['summary']['total_memory_mb']} MB") + print(f" Reuse rate: {browsers['summary']['reuse_rate_percent']:.1f}%") + + print("\n" + "=" * 60) + print("✅ Demo complete! Dashboard is now populated with rich data.") + print(f"\n📹 Recording tip: Refresh {BASE_URL}/dashboard") + print(" You should see:") + print(" • Active & completed requests") + print(" • Browser pool (permanent + hot/cold)") + print(" • Janitor cleanup events") + print(" • Endpoint analytics") + print(" • Memory timeline") + +if __name__ == "__main__": + try: + asyncio.run(demo_dashboard()) + except KeyboardInterrupt: + print("\n\n⚠️ Demo interrupted by user") + except Exception as e: + print(f"\n\n❌ Demo failed: {e}") diff --git a/deploy/docker/tests/test_monitor_demo.py b/deploy/docker/tests/test_monitor_demo.py new file mode 100644 index 00000000..2dbff5b1 --- /dev/null +++ b/deploy/docker/tests/test_monitor_demo.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +"""Quick test to generate monitor dashboard activity""" +import httpx +import asyncio + +async def test_dashboard(): + async with httpx.AsyncClient(timeout=30.0) as client: + print("📊 Generating dashboard activity...") + + # Test 1: Simple crawl + print("\n1️⃣ Running simple crawl...") + r1 = await client.post( + "http://localhost:11235/crawl", + json={"urls": ["https://httpbin.org/html"], "crawler_config": {}} + ) + print(f" Status: {r1.status_code}") + + # Test 2: Multiple URLs + print("\n2️⃣ Running multi-URL crawl...") + r2 = await client.post( + "http://localhost:11235/crawl", + json={ + "urls": [ + "https://httpbin.org/html", + "https://httpbin.org/json" + ], + "crawler_config": {} + } + ) + print(f" Status: {r2.status_code}") + + # Test 3: Check monitor health + print("\n3️⃣ Checking monitor health...") + r3 = await client.get("http://localhost:11235/monitor/health") + health = r3.json() + print(f" Memory: {health['container']['memory_percent']}%") + print(f" Browsers: {health['pool']['permanent']['active']}") + + # Test 4: Check requests + print("\n4️⃣ Checking request log...") + r4 = await client.get("http://localhost:11235/monitor/requests") + reqs = r4.json() + print(f" Active: {len(reqs['active'])}") + print(f" Completed: {len(reqs['completed'])}") + + # Test 5: Check endpoint stats + print("\n5️⃣ Checking endpoint stats...") + r5 = await client.get("http://localhost:11235/monitor/endpoints/stats") + stats = r5.json() + for endpoint, data in stats.items(): + print(f" {endpoint}: {data['count']} requests, {data['avg_latency_ms']}ms avg") + + print("\n✅ Dashboard should now show activity!") + print(f"\n🌐 Open: http://localhost:11235/dashboard") + +if __name__ == "__main__": + asyncio.run(test_dashboard())