From a12c7bfb44ad4e03f2c653d20f0cc050e01a2727 Mon Sep 17 00:00:00 2001 From: Martin Dimitrov Date: Sun, 15 Dec 2024 13:00:48 -0800 Subject: [PATCH] add common handler decorator and metrics for buzzer client --- .gitea/workflows/build-homeassistant.yaml | 7 +- bun.lockb | Bin 409296 -> 413960 bytes packages/doorman-api/package.json | 5 +- .../doorman-api/src/common/DoormanHandler.ts | 91 ++++++++++++++++++ .../src/common/DoormanHandlerContext.ts | 11 +++ packages/doorman-client/.env.example | 6 +- packages/doorman-client/package.json | 12 ++- .../src/functions/buzzer-activated.ts | 28 +++++- .../src/metrics/BuzzerActivatedMetrics.ts | 54 +++++++++++ .../doorman-client/src/types/TwilioContext.ts | 6 +- .../doorman-client/src/utils/DoormanUtils.ts | 19 ++-- .../doorman-client/src/utils/TwimlUtils.ts | 6 +- 12 files changed, 215 insertions(+), 30 deletions(-) create mode 100644 packages/doorman-api/src/common/DoormanHandler.ts create mode 100644 packages/doorman-api/src/common/DoormanHandlerContext.ts create mode 100644 packages/doorman-client/src/metrics/BuzzerActivatedMetrics.ts diff --git a/.gitea/workflows/build-homeassistant.yaml b/.gitea/workflows/build-homeassistant.yaml index 949640e..24b1942 100644 --- a/.gitea/workflows/build-homeassistant.yaml +++ b/.gitea/workflows/build-homeassistant.yaml @@ -15,7 +15,6 @@ jobs: token: ${{ github.token }} deploy-gitainer: needs: docker - runs-on: ubuntu-22.04 - steps: - - name: Call Gitainer stack webhooks - run: curl --request POST http://192.168.1.150:9080/api/stacks/doorman-homeassistant?pretty \ No newline at end of file + uses: martin/chromart-gitea-actions/.gitea/workflows/gitainer-deploy.yaml@main + with: + stack_name: doorman-homeassistant diff --git a/bun.lockb b/bun.lockb index e3ed26c6c14fade4a1bb395f31156b4855ec034c..ec7491bed9eeda5730b166a5af102336dc35c861 100755 GIT binary patch delta 40443 zcmeEv2UHcw)9=jXUbtgI6a)mbm_?!&L5maDRz=%1F+L&|B5l~Sv zteC|d)|}UzBl3PV(*vyk-S6%9!a47pw{tA3>etoX)zx8o68CJjUD7i9>CRQV%{+Fs zs>h^<4+6V9dNi(FQ?J6d;j>m(=oMD}$*Ny7#qu$&%>_C>&FEN0m;7vN%z+~1LXreQ z5`=_Vzzcv+#5mbUR7rwT$bXbr)OgS&uRgO}!b0j__X zqNYCpC;KdLvfBe*0(>>sr-R!a4NSgPY(d6{54g*T7oSEDnJiThA$@zf(`g} zaH`-4IHhlu+Z466-+-nRg*Ich%EyCKQ^(4QMQvTZnX1Lj5rl$3cm1U5?#Y5+1-%4# zDR2YVJ>+9WP2xCNw6b+gN8Un6b6=qHO5lZ|XMj^sBT*)CGjM9aC%K7LnfMKhRc#44 z<+fj{>fe_Lf&=s!z%9Z1h7ar&6dolEflj4{E)#@G;LX6vBNuS8AI0r0p_hYh3!U2A zFDfJ|7~OfWl_1zdFS}9+bG3wmtJHAyg<%mSGzKq22CLNq_27=sE6Y~JY~vHxsBXrr zRfCE`gTjM)hxCU|Zgm8wpf>}j<^;pOGAshm=aMKPoVF5U` zEODnAk^Y>w1E+Bujr1zuZ+8hoN${4?spGnGhgyRxk&23njvW*tv`AGQ^VX?DFc9f9 z6uYINK6z#F;_=NOQH^oCx$EE(bi*FC2dg0m4TT-ht--zas_7NLZJ^tOQ|PSIRX!4& zhRAKMU*bG0s%BJ3v><>F2pS{^Lx7{6_=GSB5q8k-aMt4%ecV2bG~?nul5wJ)sU zPzm5)lhm;{IHWhaOAsz)sO6MDs5+7iog7O7Cx?PVgTnixPc(;nheQqu3dbl8g+W;u z+(dz;z(bCxEo=o&6^eMBlH0g(V-|uCUQTbQ>D$2d}6VJ6u%_-Xnn=k-;fL z>1*mpn+8r5Y)~>{Zk$!)*#kPY$QqnRu|$Y4w?=rjOo@q*sMz8v(c+<{DRE9ja-5zxuLDmX26&(5e3 zFhQq!yr5H;mb{1f)6kBJ9vm@bfFPIyu!i0ehU8%Za0=BCWWc(VupXR7&}4AJ`+(Cd zZ}Lzrcr!T7(YN3zf!fRB`90 zYKZHAlLHk|FgZH+nOfjDaH>E6PJ{3EBQ-|w44f;ibT;I-FY%53FRgPcz zQf)<4M9`o?vHhY7zEO?B2S)V8NEKY(s!o>WyeQ|{u%k+!fm1hJ=X?S1$FgPeUG(;E%oeJy&PLA{r={1Br@67Fj2M+8X79t2Oxt%{ajgK8)$gyA{q1;!s z084Q4a6J;JKPQ1xnA?0)(>1$#(5Xe*Djo|v$~QfmpJmYiC+J#5v2vunZE=GjDid@t z7CN+BI3)=0CZ}uJ2<%=6%{(DKbki{FDmn@ zw_cRL+LtoBW+uuGC7pg5V6K*v0#5U87&wKe5-(@Jg_<7JZzxK^%0D=)CY>F-8AW9l zGypFNdk=6+a7!!(6b*Y3{+29NvM9n2*gZrXVA*`tCWvr`L1bNd5I(1?baIy;y4;vB&d;`j(Ruu72z3Cqo z(YxlrK0-(_)tiS_s{IS-h*pDyw#7xIQ+&Z`UDQSob`s$j41&VL;zCARtKkTV2?-7Y zZn9AebheQTI7a9e=g4u6rFFA%~NM4-Z2Z zx(Px^MC6dL!6Ad+mr}c8aG%~d(FwvJ_1r5gDXlvHp}N}0)v#0MDL93CCOCEF&rOx7 zDfca9-&EVh3gy&*1rLnqLuD@Fy1u;HEuW!N;0E>YTXV=DLFhw)gHW`Big&IcA1_s@ zz!aWsOgZ^WsY>x%oYcX#jPsF|)LMcfVh0382Z#0=7!yMMrA4(xW!2LL;N-avB0wJd zS5eEU!6P#`M%jo7(NW=HbZR`#(;L53r;G*ca55>LFl11Wa$46a=vGbMR=QGrU^P)$ zRJ5t*0iA}+0OY6of@`XNbONVVYvp2qDtZ%V)jzvhqOwSxgiiU*xc&+{O}-nPPjykF zxDh&fHa;O5`=zj$kil95v;y|RfQG~()i5E!O;ncZ{@}D4Y87;YPP2GOMDMU*4D1=O zr`fHQGmIt$j$UC4qV7E?KlVkB=E-qimQQFvHDm`(?Vg+ z_QzhqQB`NYG3LB|I5FvcgXj0(c_)n<_B_UJ^X{T8>W0>t*C+P;T+0J%9Wp2W>hh(0 zx>ME0cVi+h8r*vK9bmf9(Z#)w`KlW)&RYF6d1pwIS`~si{IbQ^DI{!6xctg=c6+8* zvccG+X2thY4<56Y2i4oc0vvSyvP%Qw_87mD?d8LdJ+{ZpZdj(U585oEokW#nSHH?^ zi$Ro(NrHe0o?w(Snwi99viHt->FNYQs0&OdCpI!kHWRUsL(|KN{w65`nv1GUH;L=y z*j;|oePCXSeTJV&tS*1v1%NqiSz;N{%Lt7EWt7DRCdp@xAk@y)ra_~y znaKf2I{{5Cj5IT*TB>Ocps5`f5NML(q197M8D-KPV{%->dQ!t=L1+q4l*MS1G!+^a z1@yGo-z2`0y)*p8hH@$P zv*fgce$pjiMl7K%z$Jl_qY2M$OQ%M0+$Jn!i#`MjMmCZviwp zy_Js?4@n(A0pVVHD0SrQ`aY6=Ataca%i>`#Bb1+@D4{bPUxad%`c5oXM>dT5n29u&<1*?={wp=t zGUl4Z!Sd&0eo_W7>Lx)>OfnhDu0j)K-}*j=K9K4u)vSQjQ;~dDqXMNuS&lv7Cq9?c zPWVYv)~GQu$cdA^j8G`{W^zV=$&d{#K=y6sW0<;D5ZcS><9wuNkf=W}M*U4Wf56Iis^ls=QqdeNj1MgGma4Mpf%6_TqBc`+}cjzJpJG zW!{{S(=PalMdZ&H{0wX-R$Dpyf{*k7QVZm>kP`#FjJvRKDT>(6WJrWISoS^YVSJH=B~XNoD)pgS~mAYgK0!p2_q6R*l?m;I#TY3cxQkTD9S ze$c43(0Z7pjnKTISyA+)htO!!BeG4sjJs7YRm1Vn^0FO==961U@jX~mbG1&;T0$#K zD~pr~jY4gd6Bn7JW_wkaMM}dgO1tJKT>(a&j!Ijbq|)iy5WsAfrb45oRAd>nl;);o zwD28Ut5_K3RaHvVT294Iy!m=1>5=+ZzH~oyuGE`@BS5xC%AQXjM zM5=L6bss|z^EQ+;IlYIEVFjcBIo;34_y7{EYigb?5t8wcngvTX=4M-HYKs$NyyVZf z{lq7-_Z>ee@rc%1%u;cWoCd7qZ(6@%F_$_)>xqKV32jWqRA??#jkZ*ZkL9$xep1p= zb)CXip`l4UCwt%XGqlMRgtl^el8_X9s^6m%LcSe`I{&q8Y;XSeZ@il0ztEEZSvZ#!r- zPR-@S=3aUz&FFL?zL3)%`iYI@&ky~i)RThHPH_oNNH(W(!+`T!XJ`~G9{*L6{P~fe z_(t}A94}6hKR@=9-knp!AW?rv70=`11@_R|o1|Z$(MU0q6Bn4IG-x!Opfxs0Z=tnT zwJ4LZ!3A}K=JvC3ArMN>L-ZHb1}g{~R1P~eAT)+>u0?ITp@rsZkLg}iTTV6}D_X|JC`WpSk{T3p z`YtpYE6P?v^7=i0W9LAlPE(uy2eiC=b#AC52Wst?3NslOLhF=k=+aEm0|=3`pZQ2#ZV7@Xl2BrElW`ifE_sFxjD{yb+9;NFZVR}Z zL4Kh~X(6;cFJD8eucT4y-0x^7H%;WM-#C1;oWg3v>e(jW~~q$&^SWKXq@fTX7W z4rzdb^?b-{6;tK3kAAxE59Kc(>lx}kLK78(0gnYCO7{I)R}TT(UzAh%2}L3u5QLxP z5e zvi&hT(z5*wuV2!(A=}SKs`^TOlA@d<4Ber5$-eD*;*HurI`qeqnXY;7yIM>S~SL@Fo~q zpi(1F9J)1M*%#R8u7729^sw#vEw`s4Owuf9w9ezih47t#)=1Il3aVJPYH2NtJ4{kj zXlkGncbIgGve|9}O8XsXh*B5&Ru}M{)iDQp;=9_%SbEXslpL*XXoKX0t8^M)>gldM zw03k_G`lK@Y>$QLE5(VT(odLJaA`L*FSUaHCbKu9$bL1#sH{#DY5>ORaJorq4vl}ZYjWB^F8byT5F{Y>Yn$|xT3K0&1BUg7BmfG-{_n7m!i{G*@U?U~oCP6QUcX=6&g(s)P-|_T_KbwXNUSoO(5}@1(@wh6G5}Xu~P3gTx0U7QAcF)Cot^wMY?7qa9~`XjGok z&r%FD8kz73+vB~Y$?0Q!bnl9=I2%N{J#J3(XyH0z8Z??(xK!$C(p|D-bxOdd1a3^z zAhVJab4=0@XaUe{lz8ldMt))Y-QHw)2(77{9^fOn;O4YC*in)+6dFZFU2N7sumHFx?^1TvA4`ln#ye8%BYE zrxa8dO2Eoe4;qD9?U)E?)Jn{iZC-jPKPk7y-t#Pqdj|J1XgO`Rbpy+=-A-_S9T0Vx z&{+6`(osn6;5Sw(%Y&EOQ=Nb)3xtM9Q-P4nIIo~L4niX*v5&!GV1=g|4HUO%iE9T< zot|*g2&E~KuukIo(yqL^lA%j+iVcNEglAXJXJ5iMF#8>VQ0p*}R6qi|m_4ib5Yroh9)(A2dFdu!=CSs_ii zt~S(tdC)<5_T4R&(12h^5xjsvEDyfKi2AKwp>Q%?oRcLBS;vp|1 z9`8^$V{ZDJbR8t8Cn9tMFtu;2@-AS%$04! z6=>8u&>ov4V^!^3j?=Ee2O74LK|Z?Js%&>H)Oim`8(1R%xIq+AP4!Dz7o;W7e2|7y zS%OKr4oz(i?PZ;+t7%xiqfF9pXw=bY&re=PDAX$!%ALbk&}acb1-OssRzs~05sEhH zM%Q3<+~7nyP#Ru{6wZHkHPyi^DMK$Dnz~wJ1vOh$6I&&n=xe+Wn08-E@YySOv9YeL zGppl)(rV%v6rFDb3WqMb7TZne0gj?+=t&C~wWpMRH8g|PK-uydmO=bUxsZJfiCV9o zqAR$n-G^I>m=C`|Q&&VRD`+P~tI)5N4Z<<8=~P_E8HZ7P6gBl$D|bogHx1a z!08}P>BGUT!KZ^$IWu|s-|?D%myjPP2WF!niQQ-`+IFULm-2Fnlb_4DPMq}RoUZ^U zzt(_LG3&wU_$#hs8>2*9c0Wois&vaXZcCi%=KNlk3E(;FI9gfHNG=&k4H3@jPAu=nn~0z*QJh zh1bDFHln>~iwg`vxJfX}juy*^dUgY-GOAwkQuA}Ne#P;>;#A37DjfVB=kK|Feol6u zc=~6ao{tw$y!?s;9a}gA{*b+BRJ*|lPUTDB2Jj-_bmZrhZprP6@^s>aTXFrLa0&J$ zjp$B_nj_DcpHqRQfKz%Io=%*)tpeAHQ@Rs4HKYnS<*Ulm^K)vDGslTjY&?w^PE?>b z&sc{W5T}BCIIjy%71iVE#3|j6bAOH#Cp>`b`8nbBIZm9!rt$cn5}NS@;-oj{I&rGF zB{)GHIG&%AeMjKLJ8|9_obvs`^{(J_5GT8C;1sd=o)C&ah~NhKIT;S*hJ(SWz-Z2g za(m)rH;nTbjuWR4Qv>P9&#B^3!0|6$n27)BR5S;i3S!`N{1vAH7a^S-S;Fo938%rf zmfQcCSN`h+%CL=RAg)V!?k$@BJu@RL|DKuuJv0A%W~P-02c4;DHT(C>j79R_Gcyf| zf6vVSa|92CpbHspg=04~M-uezwN6TZKCt91GO`wCro>oS)hUja*<@-)MR8#FVim zb^eW5sb0Ef;%XMwOV^Qw_Rc6q zEkXgLvALlDl0yMx6R?Lh2?NkL48X=P0O{;A0iOuy)(^mbwzeODHT?h-?hhb?b?y(K zQ-1*I1RP>gH~?cffC1qEj<7TWQVA$E0KiceHUL2A005^5IL7QF0N6zU7!d*B1Up7R zCIK}E0yxED2Lgy02;c?*XIRxi0ICcEFl7*cEOwQED+KsP0yxhmL;^^P1n``Ii_Cj4 z0I$IS77hk*nLQ!kF##>209<8rqW~mF0mvrcI%^UQpm8*SjnM#Zu+Id1BB0w40JqrM zApq740Z@1-fIF=7Pyn5V0!Sy|9+QRvFb)GSU>JZ0ERBFv0!qaIc*MeD0EET>I7Pq{ zW*-Z{E*8LuSO8DiF#<9Ps2K;~Ig5=05EBRB1_3Ww)n5Tr`4zyFUje*kR|&X6fbVbs zZ`p+50Fs6Scuv53<~;&{*9gjzscWYDsAJbMbsgB_5xPPwJV@7E{LJQ#gh}#9m}HMc zp0BJ)Jb=dW05--0$Y!4j_(VXr1Qe2^W9t%7$eIKog%e>Svd)Px>6EA|96zU6>p||1 zTHF_RHH%G(34d~VTiGL4EnCRTmb>?ljIcPj=yK~9i>9oyU((yUo#mX&L2ny9+Zef{ z^VUv1ZtrOLHY^C6dr|mq`5oMh&YAqs_Z+pmLRfg2mLFQTDCyNTGPq-_LKRbgpW1uZ z(kC$mooiewF}-D&gY{4LonM{2_w`(xw^7|)c9#G4{N{#X7hX-8duH%DmYt}xi=S&# zLOON4O-q+co^`i`HVx5vRl7mf!SAI#aX zqQkjSJ)2pTF>WmUd_AryH1`bo-J6~9YQ?p(iTcwK-@B{`uHWKZf1l0W9UnHIl+dE! zALnZ{9G*0&TFIR@_A#mP6L&Ut-+TU0pVM6}i~n)2RovrOUa5Xp*~U?Dufyk$r{>)W zm|Icbs$kOxDRBdbM+FQ@cv1PN&lm3tTk9@sTy$0K@IFTuKUr!w`sQXguM~^O&>X+> z&+9mr927s&8@E1l-LuH=-noE+p}ms|727;mpL)9S+Y28T4q4xS%J@pI!7FvQEe}{M zh-`YM)WvG{%iqK~*I4BkS>CB$iw)h{bu85B^7!xZ&LtYKZlmGeV|hX4Sr5-2ZRu!! zqfzAgy*b<3G`c@IqIj$&F7w!-@O?Z7L7P`W7FMU)<=Ghi1)a3vzGOy;}+k04(oem-`=3qIb~msdO9!u zXZxy7A9k|#PkOA__N2pF=WE?vx(3D$I}m5~o)sPg_X=P7KBN7E?k7eJyFbGBUFL74 z8b)6&yVN#q$Ad2^EqYw)P@>M8zKvgH%o>n-QTH~g!i^;to<;n8Vd&%E8n?7Mz2hn# zJmp5OV1D=Jbh_Am9*f-_KGAc}g%4e%@kxVPhA!^vU8J4cgP<>VyN+xwe_=z>Pxc>2 z%=y+Lu_?=P>R-C-zFOaw-`00+;5EMKN|sLUO?zCRxRtYWuPyt1PbU^y&TnJ-Bxc_*w%K1_ z^jT7>&y5T10yDyE-t8FUJ=(I%i!L{Y?YI<_(0a?lb;X_PbxX%%kKE{4=66q=>nIc% zxZu{y+pqqpUcu^N*I&oZA5hmzHz&tbJm-kZl{Q!19cwo3VISzerv4}6kaw|F=9U0QPBmvg;49^X5s?U&mTjr#d` zReUtN#;YwW-gjL&JjKGieCtrR%07~l#nkfOzmM@)_a>ivR{7ogG@zif@!s;YoqGhA z8T$5CnR^9lRC_%;Iyv@qgU}C_7eDT|D?Ha}TGTxtI3RI%1H{vRj(>6>Rg>~912@CgtQ2=aN8Uc?9C^Z^@JqsHRAbA3SQv^6L`!N6-PXsVx z41iMX7y+LMs5uru85TPhz?w+_ZV*t8RUHSQ(_{cs#sR3nt`cCJ0>F1X04FwKJb+XJ zo)b`+c_#q~oeE%K5`e1g2?2J~0JN9@pgNm70YD}J*#y*NO(p_}nGV1(2|z8@W)grZ zGXU%)z?F%U0bC& zCIgr~6F?xlLqIA4fii$bY=#UVbS{AR1T<$5`1OzSu(3j0v1R!(;fcFH1v4F(@>{bF;xfnoy z_L_i90@^PDFn}#x0w87;0K-xM16iA;0IIA8u#LCIx`!HULXm;x+)CQUN?5U^#Q$4#1cOVD@$ZE7=_a zQV9s$0bn(ou>(NpZUFBISjz%-0)C4pG6`tE3&2LUbQge_y#Nd;05-EW zDFCXZ1K3HxRwkwbxI#ckDuC^5D*;LS0NA7f*vW#@0C?>OaD;#qX0;o@V*-Zk29U-w z2uMBvz-bSFJuGq$fW{dBE)tN=%IyX4iGZ2Lv2q zuKNHO4+EIJ55N(2hk#T90`~(r%4X~b5PAf_djgKJfCB*Regm-b0Du$hH369fw9f!= ziY?6m5OWlO;UIuBtj$3HRWbqWBp{24hX7n5Amk8$^K2^tNyh-#90qWa1sw+9bsWGE z0xmPFBLE%~Fysh;t1N?nz$5mAfY7r5TATpz zgv~txz%C0wHUUprlasoR;xo3C*mL%o*bCO?6xd6)me?yMo(6l(Ium=twi0{Gq%&ae zSP-%IEREO)W_1?qBMUpLYsDHm=&bRXkv=6MOIOj!5Ihmjvf0usT`g(w!0>ST#MqJD z`Jihh)?lt5b;S)4lgS|Co9+agenIz&vP#iW^tp0y|IH~@pLOv%F^A3gs$+)Vmf=qa zkm+c}svrNeHYS$-O?Ms(%ZzN4r~GxAl!c|tI_gqT;1vI_?6E zO`X3MO1{qySekhl9I*FMli*twcKd-&pYp1xSwmfck2%VNr-ac3S-s+Bl?(;(He_M) zBc63FjzU#k3s!HcO}8P3RkJbMZScoic!kKHGlhqRh6RV>Qt~$5j#FZLCS{hbnciF% zTr*`}DYMc#*45X%MqcZ$i2ure7XfsU;s1efDI?{7rPpZOef)o*0sk3a@~r!Rc!q{n zD{%XndQlMlogKSnRzLoGj(X7Tk!U30@1xKV`M`QWO2j`u^gmF9Ai{ahyzB2kvfiJBZ1+catn7n#}av7dI#wmuXz;5 z=ry15lmW+Rj*%mbV`CHy59H~+ATpq1EH|V_)Km~TF%Fy}U&LOpz{r{Ep#B`&%I#_Ziv&jSHhLk33~Pcc`Fz^J4V?*t6%F>C+^!bn;=I5VZbz?y zP3AKq6&U`}Cj`P2j_u(X-aiy(a%>+ksy7~Q)C;pYcmN=c1-#TN+~?CNgJbx#5SU>(TOG*WOJ z1x6m?jYy#z$BuCfZ@DT3fe9ygUSG&GNNJ9r^EQQC8W_#_>l|wa*%lbB z8NYL^Ipm_il=$D^U<-gJKy=*XSWC#K6hshiaSR_c3uicX8yNo4S24m_5G_LYxLq5_ zS2=c{W7Hi)w6;7jL;cjKwjf$C=y=Et+d(EnT3jA+tUY9U0g{f#9P0qtk7G|b))80$ z$Nu10Ct$t!K!3`y&eZ;3fK)l17|EF~pb(Bd2ZksKzkui+CpunoyRMMC04odrieud% z&qRiD;IBE>9Wpsv0sIZedO#+}Dp362a_%fl+z=K=**r>SxBW{*dqUIt)B-IE}w34w`dt01Tr!X2CN@02{(F2^jnl z27+Svf>em-9R%!GV6>VR<`_Ag%vaVT92*QQ7Nm@SOAbasjRVo@SCnJXkg1>Pu;SPd z$lu8W9L0c9i-&@!rF2+xyJ3*2QM7>Ba4ZHgttGU8l^~4zpFGK=QgGM;B#|6B&M|vn zRNt?lQyg>T1r7(61k4}26vswD9tioH#ZX@;0@Oc%c$NN*@Q>u7W}p zZa5y;4vtmjSQ4LCxW~==En0*0_MXpcLhTXCIh6UlolZm zZa4)pwSo>$j!lKEy{=uGW78;|4@?s2N)q&}d- zhl4U?cLl*9;n*zb+N-L*9GeY{78RP$^*A;MGA$~E`EiUvruS|8g8Ku*KOq^U=U9A0 zZa5dx@Q)A2Jn zb;$}4bt6sPcHkHS!b*^h9`UES+n$4~Ak(Ro=4l6xt%kgnO2N^QV{0Jq0o$OLn{t-#DU)|+GT+W_K=9_0w(;C5)UIo5|`JAi%QSYM9q1oi;fSnyDe?SlM} z+l6r~1=wS5*NXf%&$`VHgQ zLCA$LooK4XaO@DbqZt&-vBQubWAxK(i33KBJA(R!ryLy44S%BqV04V&*ip!{I5v`F znZRh4EC#1>M7|yaVOu3E15W@(l^h36flS9Jj-7x!6>|J?2xA~n=98dwu!N)a8YIRx#4Zd%OF!B3plof?>?-pSb;ZYm6*pHGZ!bipE4Rmw@}7{ z=(Y*nCP@NK0MYG{+br>o89f#{^~NmNkN`B1t$kSNZ_O$= zwTIXN)DhGf)CKels4J)&s5__!3;JYM+={NSLqK!^-4_(fx_vXV#q+W!Z_QHT>8f=S zs-!Dbx-y*#ng*HHv=saIhTeUo+%L>HuV0s08^81w}62k0s28K@P) zN#C7)106-AGC{{c$3Z9bLW<>kGbdda}dK?6W^5kMb5Oh<>!0?h@@2hj%;tw3!+bQMw`yaA{oC=m1$sF4v@z>Oi8;HtBc z&HHFp(xxi(YM|<%8lWn0pd@?#(X5sYeO^L)`eGp3$6JGJm@7hEQ`xOU&4%?|5bdlV zfatrRYarTG(HEIdpf{kmpm(76 zpbwyrAo?Eg1?VNHE9|>~egU;XgMJ3lcYZk_y0;;MbRaX3-U4^e3=qsg79hzY#rd;Y z2VG~{PV>|b@dVWd znLu9b##b}Hc-l76*~|cL0irvV!$5R{@mJ78WWNET@2E0C$3gU!SQ=3|mw%H1eKJB3^!bmir4m{col7Ni> zb`$BhK)-{IfPMoV1|0xpfHr{WGdvkYCxPkg!FRJ^RqG?!=+M&&YTgWVB!*= z_n?oUN1$UCxB%V*VJ~PDXcA}|h<3Jgj_zYncdz77H`YR@m%G(LGJOVW0u4dgbg!Wi zlKnySC7R}A0mubGw8tn6dI@{-ozg!+rai?I&>w_@4ubYEUo-uB`*#qj*w^51K$P_; zXdV;w`c@JpuLUh;o%Q+(1^np>7e`SKIa*nhHIQ!c@#u>$X06rl|!wk)>13c zI;G51a7x<`POYYLsG;Ow8r`#@R*?Y_ftpS&qeAwAw91LowobFx@cedEIHhZ8dEGHNZMuAxlSBF#z4uSJ6LQo$NV`Y{mYA&e-0#7eDtU+xBqFRlL3up3RnJ$2&YUm z5UFs|$%YD}){re-E9Q6lGIX-N0!{-^D=!T)HHx}Wi6F+GQXo};aO74%g%g%Pd{nVk zkmiWinjh0{!1O64fwj+cu5In)%K(3(g(6d3`ShuqJ z%JFnpls4viaJuC~3s)g>1Epc%Rm2@c4-416hQ31~5B5r`g=F90D_N~Gu}={(3X zXeP)XIz?|9XgG+Rr^}Tn5Z$O90A35^3^@$E3&;)R3UUF(cgBBohsGU52^~QlKd+_yueMM+90|YPG!*@7plM$M0ar-fPMn`f&#hT&O+|X`v7o1kUyv%mkFaZ%1ormjUhJ$(Jdpwn}7&w0cs9v18NOw1!@WMVC5?4i^o&@ zLO_`)vNw1zs28X^=oe5|P&ZHzs3)igh&qU{zMwv!P*6CC#(sbBexNRpDOMDV!O%_U zgh=o~pkQDF!6Rs_lM)1?o}r7&XwVSQP*4mg4ipQTiQo}Ug**u~5;Oud5fl#^2O0|+ z0~!TN1ksJ?1n|+IG02+)J|46f_yq96v>J_rG8xJg&YZg z&|g(j0_8BIQDp*1tBj&DA3Dv2_2BD3YeB0(E6d`)elQ3FQ3RGlUjYh&yc&EB%c`iy zF9bpi2Gj>s5>YMAP&J`0t?Wpc+Uk34R$wBZ}4rFl9uM!gk$5K|S^`eWjtCzEz zi*TL!!N61ddCo-+F1yb^3GR|-pr4|=U6SBZe_oFib4y)biY=Q$W%Hl>OgJdKXjJKm z&Ogr}fMvh{pLbj*JMAY!m#rPy?}gFnT%KJVvrK{y+HZ_zEiGE=aFr*m@(fnP2Wu>3 z)rwVd7bo3YnwPSNbwgGM?Pp7SmG~xEma+bjXYhc{fB{~CUQc$~&z{OY&nonrGA}95 zt~9$vK4`y_n)xB2%t9w$jk?__FJq^3zfAmILgTl zDeBA;#<0Ru(HQ8DX0iltoIyFJgY z6`KYFPwj_Iy}!=-^??0vck<$|{q*U~`U=EdWYAuHtc?_SD{AT#Jo3JxEySXDsjad&e=nI1)Flcl)Jg{0u zN}6im;p{04VQpsWZOuG|QEZ?LOYK)?OMV*gd~LJx6Z0&Wusc-c2KIHDK2Y4p0;cPI z#P4jxbbw0m?ePWOR?`0DoM_2%0D8JCQ(mA-I9}q5O##c-7d0PUgl0%71?TFdO=|bm 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 Dw@LQ- diff --git a/packages/doorman-api/package.json b/packages/doorman-api/package.json index 329afb2..cd0da9e 100644 --- a/packages/doorman-api/package.json +++ b/packages/doorman-api/package.json @@ -12,14 +12,15 @@ }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.609.0", - "@twilio-labs/serverless-runtime-types": "^4.0.1", "@twilio/runtime-handler": "1.3.0", "discord.js": "^14.16.3", + "prom-client": "^15.1.3", "twilio": "^3.56" }, "devDependencies": { "twilio-run": "^3.5.4", - "concurrently": "^9.1.0" + "concurrently": "^9.1.0", + "@twilio-labs/serverless-runtime-types": "^3.0.0" }, "engines": { "node": "18" diff --git a/packages/doorman-api/src/common/DoormanHandler.ts b/packages/doorman-api/src/common/DoormanHandler.ts new file mode 100644 index 0000000..a813a33 --- /dev/null +++ b/packages/doorman-api/src/common/DoormanHandler.ts @@ -0,0 +1,91 @@ +import { ServerlessCallback, ServerlessFunctionSignature } from "@twilio-labs/serverless-runtime-types/types"; +import { PrometheusContentType, Registry, Pushgateway, Summary } from "prom-client"; +import { DoormanLambdaContext } from "./DoormanHandlerContext"; + +export type BaseEvent = { request: { cookies: {}; headers: {}; }; } + +export type DoormanLambda = ( + context: Parameters>[0], + event: Parameters>[1], + callback: Parameters>[2], + metricsRegistry: Registry, +) => void; + +export enum CommonMetrics { + RUNTIME = "FunctionRuntime", +}; + +/** + * A decorator for twilio handlers. It provides a metrics registry and + * should implement timeout and cleanup jobs based on lambda timeout + * @param handler + */ +export function withMetrics( + functionName: string, + handler: DoormanLambda +): ServerlessFunctionSignature { + return async (context, event, callback) => { + console.log("[CommonHandler] creating metrics registry"); + const metricsRegistry = new Registry(); + const pushGateway = new Pushgateway(context.PUSHGATEWAY_URL, {}, metricsRegistry); + + metricsRegistry.registerMetric(new Summary({ + name: CommonMetrics.RUNTIME, + help: "Runtime of the function", + })); + + const summaryTimer = (metricsRegistry.getSingleMetric(CommonMetrics.RUNTIME) as Summary).startTimer(); + + const startTime = Date.now(); + console.log(`[CommonHandler] started handler at ${startTime}`); + + const handlerResponsePromise: Promise> = new Promise(async (resolve, reject) => { + // intercept the callbackResult + let callbackResult: Parameters | undefined; + const tempCallback: ServerlessCallback = (err, payload) => { + callbackResult = [err, payload]; + } + + await handler(context, event, tempCallback, metricsRegistry); + + if (!callbackResult) { + reject("No callback was given"); + } + resolve(callbackResult as Parameters); + }); + + console.time("[CommonHandler] nested handler time"); + + const result = await handlerResponsePromise; + + console.timeEnd("[CommonHandler] nested handler time"); + + const endTime = Date.now(); + const remainingTime = 10000 - (endTime - startTime); + + console.log(`[CommonHandler] there is ${remainingTime} ms left to send metrics`); + + let metricsTimeout = setTimeout(() => { + console.log("[CommonHandler] cutting it too close, abandoning metrics"); + callback(...result); + }, remainingTime - 250); + + summaryTimer(); + + console.log("[CommonHandler] attempting to push metrics..."); + try { + await pushGateway.push({ + jobName: functionName, + groupings: { + stage: context.STAGE, + }, + }); + console.log("[CommonHandler] pushed metrics successfully"); + } catch (e: any) { + console.log("[CommonHandler] failed to push metrics, quietly discarding them", e); + } + + clearTimeout(metricsTimeout); + callback(...result); + }; +}; diff --git a/packages/doorman-api/src/common/DoormanHandlerContext.ts b/packages/doorman-api/src/common/DoormanHandlerContext.ts new file mode 100644 index 0000000..ba9c64e --- /dev/null +++ b/packages/doorman-api/src/common/DoormanHandlerContext.ts @@ -0,0 +1,11 @@ +import { EnvironmentVariables } from "@twilio-labs/serverless-runtime-types/types"; + +export enum Stage { + DEV = "dev", + PROD = "prod", +}; + +export interface DoormanLambdaContext extends EnvironmentVariables { + PUSHGATEWAY_URL: string; + STAGE: string; +}; diff --git a/packages/doorman-client/.env.example b/packages/doorman-client/.env.example index 03505de..fd99292 100644 --- a/packages/doorman-client/.env.example +++ b/packages/doorman-client/.env.example @@ -2,4 +2,8 @@ DOORMAN_URL=https://doorman.chromart.cc # twilio auth ACCOUNT_SID= -AUTH_TOKEN= \ No newline at end of file +AUTH_TOKEN= + +# metrics +PUSHGATEWAY_URL=https://metrics.chromart.cc +STAGE=prod \ No newline at end of file diff --git a/packages/doorman-client/package.json b/packages/doorman-client/package.json index 3e2e755..1c957ab 100644 --- a/packages/doorman-client/package.json +++ b/packages/doorman-client/package.json @@ -11,21 +11,23 @@ "deploy": "twilio-run deploy --load-system-env --env .env.example --service-name buzzer --environment=prod --override-existing-project" }, "dependencies": { - "@twilio-labs/serverless-runtime-types": "^3.0.0", "@twilio/runtime-handler": "1.3.0", - "node-fetch": "2", - "twilio": "^3.56" + "node-fetch": "^2.7.0", + "prom-client": "^15.1.3", + "prometheus-remote-write": "^0.5.0", + "twilio": "^3.84.1" }, "devDependencies": { "@types/bun": "latest", "concurrently": "^9.1.0", - "twilio-run": "^3.5.4" + "twilio-run": "^3.5.4", + "@twilio-labs/serverless-runtime-types": "^3.0.0" }, "engines": { "node": "18" }, "type": "commonjs", "peerDependencies": { - "typescript": "^5.0.0" + "typescript": "^5.2.2" } } \ No newline at end of file diff --git a/packages/doorman-client/src/functions/buzzer-activated.ts b/packages/doorman-client/src/functions/buzzer-activated.ts index 3b589ab..a401178 100644 --- a/packages/doorman-client/src/functions/buzzer-activated.ts +++ b/packages/doorman-client/src/functions/buzzer-activated.ts @@ -15,8 +15,14 @@ import VoiceResponse from 'twilio/lib/twiml/VoiceResponse'; import { DoorStatus } from '../../../doorman-api/src/types/DoorStatus'; import { StatusResponse } from '../../../doorman-api/src/functions/api/door/status'; import { InfoResponseClient } from '../../../doorman-api/src/functions/api/door/info'; +import { withMetrics } from '../../../doorman-api/src/common/DoormanHandler'; +import { Counter, Summary } from 'prom-client'; +import { BuzzerActivatedMetrics, registerMetrics } from '../metrics/BuzzerActivatedMetrics'; -export const handler: ServerlessFunctionSignature = async function(context, event, callback) { +export const handler: ServerlessFunctionSignature = withMetrics('buzzer-activated', async function(context, event, callback, metricsRegistry) { + // metrics + registerMetrics(metricsRegistry); + let invokeId = `[${randomUUID()}]`; let configString = event.config; let config: InfoResponseClient | undefined; @@ -37,6 +43,7 @@ export const handler: ServerlessFunctionSignature "")); + await notifyDiscord(context, msgs, config.discordUsers, config.discordUsers.map(() => ""), metricsRegistry); let discordLock = false; let intervals: Timer[] = []; @@ -55,10 +62,14 @@ export const handler: ServerlessFunctionSignature((resolve, reject) => { intervals.push(setInterval(() => { + (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.POLL_ATTEMPTS) as Counter).inc({ door: config.door }, 1); + const recordPollLatency = (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.POLL_LATENCY) as Summary).startTimer({ door: config.door }); + fetch(context.DOORMAN_URL + `/api/door/status?door=${config.door}`) .then(res => res.json()) .then(async (rawBody) => { let body = rawBody as StatusResponse; + recordPollLatency(); if (body?.status === DoorStatus.OPEN) { clearInterval(intervals[0]); const twiml = doorOpenTwiml(config); @@ -68,7 +79,9 @@ export const handler: ServerlessFunctionSignature((resolve, reject) => { timeouts.push(setTimeout(async () => { + (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.RESULT_NOTIFICATION_FATE_UNKNOWN) as Counter).inc({ door: config.door }, 1); + (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.DIAL_THROUGH) as Counter).inc({ door: config.door }, 1); + const twiml = dialFallbackTwiml(config); console.error( invokeId, "UngracefulFallbackPromise: Cutting it too close to timeout! Skipping notifying users and calling fallback" @@ -119,4 +137,4 @@ export const handler: ServerlessFunctionSignature clearTimeout(timeout)); intervals.forEach(interval => clearInterval(interval)); callback(null, twiml); -}; +}); diff --git a/packages/doorman-client/src/metrics/BuzzerActivatedMetrics.ts b/packages/doorman-client/src/metrics/BuzzerActivatedMetrics.ts new file mode 100644 index 0000000..2ca24a0 --- /dev/null +++ b/packages/doorman-client/src/metrics/BuzzerActivatedMetrics.ts @@ -0,0 +1,54 @@ +import { Counter, Registry, Summary } from "prom-client"; + +export enum BuzzerActivatedMetrics { + CALL_REJECTED = "CallRejected", + API_UNLOCK = "ApiUnlocked", + DIAL_THROUGH = "DialThrough", + RESULT_NOTIFICATION_FATE_UNKNOWN = "ResultNotificationFateUnknown", + POLL_ATTEMPTS = "PollAttempts", + POLL_LATENCY = "PollLatency", + NOTIFY_LATENCY = "NotifyLatency", +} + +export const registerMetrics = (metricsRegistry: Registry) => { + metricsRegistry.registerMetric(new Counter({ + name: BuzzerActivatedMetrics.CALL_REJECTED, + help: "A call is rejected because the dialer / door is not registered", + labelNames: ["From"], + })); + + metricsRegistry.registerMetric(new Counter({ + name: BuzzerActivatedMetrics.API_UNLOCK, + help: "Door was unlocked with the API", + labelNames: ["door"], + })); + + metricsRegistry.registerMetric(new Counter({ + name: BuzzerActivatedMetrics.DIAL_THROUGH, + help: "Dialed through to fallback numbers", + labelNames: ["door"], + })); + + metricsRegistry.registerMetric(new Counter({ + name: BuzzerActivatedMetrics.RESULT_NOTIFICATION_FATE_UNKNOWN, + help: "Discord result notification may or may not have been delivered due to time constraints", + labelNames: ["door"], + })); + + metricsRegistry.registerMetric(new Counter({ + name: BuzzerActivatedMetrics.POLL_ATTEMPTS, + help: "Number of times the door status was polled", + labelNames: ["door"], + })); + + metricsRegistry.registerMetric(new Summary({ + name: BuzzerActivatedMetrics.POLL_LATENCY, + help: "Latency for the door status poll", + labelNames: ["door"], + })); + + metricsRegistry.registerMetric(new Summary({ + name: BuzzerActivatedMetrics.NOTIFY_LATENCY, + help: "Latency for notify api calls", + })); +} diff --git a/packages/doorman-client/src/types/TwilioContext.ts b/packages/doorman-client/src/types/TwilioContext.ts index 674ded7..78ff976 100644 --- a/packages/doorman-client/src/types/TwilioContext.ts +++ b/packages/doorman-client/src/types/TwilioContext.ts @@ -1,5 +1,5 @@ -import { EnvironmentVariables } from "@twilio-labs/serverless-runtime-types/types"; +import { DoormanLambdaContext } from "../../../doorman-api/src/common/DoormanHandlerContext"; -export interface TwilioContext extends EnvironmentVariables { +export interface TwilioContext extends DoormanLambdaContext { DOORMAN_URL: string; -} +}; diff --git a/packages/doorman-client/src/utils/DoormanUtils.ts b/packages/doorman-client/src/utils/DoormanUtils.ts index 9b6aa42..d03e638 100644 --- a/packages/doorman-client/src/utils/DoormanUtils.ts +++ b/packages/doorman-client/src/utils/DoormanUtils.ts @@ -1,8 +1,10 @@ +import { register, Registry, Summary } from "prom-client"; +import { InfoResponseClient } from "../../../doorman-api/src/functions/api/door/info"; import { TwilioContext } from "../types/TwilioContext"; -import { DoorConfig } from "../types/DoorConfig"; import { lambdaFastHttp } from "./LambdaUtils"; +import { BuzzerActivatedMetrics } from "../metrics/BuzzerActivatedMetrics"; -export async function getConfig(context: TwilioContext, buzzer: string): Promise { +export async function getConfig(context: TwilioContext, buzzer: string): Promise { return await fetch(context.DOORMAN_URL + `/api/door/info?buzzer=${buzzer}`) .then(res => res.json()) .catch(err => { @@ -10,12 +12,15 @@ export async function getConfig(context: TwilioContext, buzzer: string): Promise }); } -export function notifyDiscord(context: TwilioContext, msg: string[], u: string[], optionalJsonStr: string[]){ - return lambdaFastHttp(context.DOORMAN_URL + +export async function notifyDiscord(context: TwilioContext, msg: string[], u: string[], optionalJsonStr: string[], metricsRegistry: Registry){ + const endTimer = (metricsRegistry.getSingleMetric(BuzzerActivatedMetrics.NOTIFY_LATENCY) as Summary).startTimer(); + const res = await lambdaFastHttp(context.DOORMAN_URL + `/api/door/notify?discordUser=${encodeURIComponent(JSON.stringify(u))}&msg=${encodeURIComponent(JSON.stringify(msg))}&json=${encodeURIComponent(JSON.stringify(optionalJsonStr))}`, - ).catch(err => console.log(err)) + ).catch(err => console.log(err)); + endTimer(); + return res; } -export async function notifyAllDiscord(context: TwilioContext, config: DoorConfig, msg: string, optionalJsonStr: string = "") { - return notifyDiscord(context, config.discordUsers.map(() => msg), config.discordUsers, config.discordUsers.map(() => optionalJsonStr)); +export async function notifyAllDiscord(context: TwilioContext, config: InfoResponseClient, msg: string, metricsRegistry: Registry, optionalJsonStr: string = "") { + return notifyDiscord(context, config.discordUsers.map(() => msg), config.discordUsers, config.discordUsers.map(() => optionalJsonStr), metricsRegistry); } \ No newline at end of file diff --git a/packages/doorman-client/src/utils/TwimlUtils.ts b/packages/doorman-client/src/utils/TwimlUtils.ts index 9bfdc95..2cb09fc 100644 --- a/packages/doorman-client/src/utils/TwimlUtils.ts +++ b/packages/doorman-client/src/utils/TwimlUtils.ts @@ -1,7 +1,7 @@ import VoiceResponse from 'twilio/lib/twiml/VoiceResponse'; -import { DoorConfig } from '../types/DoorConfig'; +import { InfoResponseClient } from '../../../doorman-api/src/functions/api/door/info'; -export function doorOpenTwiml(config: DoorConfig): VoiceResponse { +export function doorOpenTwiml(config: InfoResponseClient): VoiceResponse { const twiml = new Twilio.twiml.VoiceResponse(); twiml.play('https://buzzer-2439-prod.twil.io/buzzing_up_boosted.mp3'); @@ -12,7 +12,7 @@ export function doorOpenTwiml(config: DoorConfig): VoiceResponse { return twiml; } -export function dialFallbackTwiml(config: DoorConfig): VoiceResponse { +export function dialFallbackTwiml(config: InfoResponseClient): VoiceResponse { const twiml = new Twilio.twiml.VoiceResponse(); let dial = twiml.dial({