feat(BRIDGE-363): Observability metrics for IMAP connections; minor unleash service refactor;

This commit is contained in:
Atanas Janeshliev
2025-05-12 13:35:52 +02:00
parent a305ee1113
commit 89da7335b6
26 changed files with 334 additions and 64 deletions

2
go.mod
View File

@ -7,7 +7,7 @@ toolchain go1.24.2
require ( require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.2.0 github.com/Masterminds/semver/v3 v3.2.0
github.com/ProtonMail/gluon v0.17.1-0.20250324123053-2abce471ad71 github.com/ProtonMail/gluon v0.17.1-0.20250516132429-a4b2de331311
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba
github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton

28
go.sum
View File

@ -36,8 +36,14 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I= github.com/ProtonMail/bcrypt v0.0.0-20210511135022-227b4adcab57/go.mod h1:HecWFHognK8GfRDGnFQbW/LiV7A3MX3gZVs45vk5h8I=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug=
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo= github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/gluon v0.17.1-0.20250324123053-2abce471ad71 h1:UC8SLrS6QbBeOUM8FJugyNoeV5gRGoQCwNePAMxuM20= github.com/ProtonMail/gluon v0.17.1-0.20250513141309-843796a505bc h1:2oppv7H5ZeFnRDohTbLZW5A8I1ylhoX2QEi3RtKxrLE=
github.com/ProtonMail/gluon v0.17.1-0.20250324123053-2abce471ad71/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= github.com/ProtonMail/gluon v0.17.1-0.20250513141309-843796a505bc/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20250514104052-2f93fdfc4850 h1:OFMVeakcDS9nHW5kQ/CuBXri84iPBqPgZFHz5Xs/8jo=
github.com/ProtonMail/gluon v0.17.1-0.20250514104052-2f93fdfc4850/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20250515084749-4afe6a076ac4 h1:L1JeVS2op3VIcPKctS493+qOBFGhr488mMkYVSLr9eY=
github.com/ProtonMail/gluon v0.17.1-0.20250515084749-4afe6a076ac4/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/gluon v0.17.1-0.20250516132429-a4b2de331311 h1:8oEkpmF8PD7GyCQjmTto+4yhz4vE1tTT2djL2BgJcBI=
github.com/ProtonMail/gluon v0.17.1-0.20250516132429-a4b2de331311/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8=
github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= github.com/ProtonMail/go-crypto v0.0.0-20230321155629-9a39f2531310/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE=
github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE= github.com/ProtonMail/go-crypto v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE=
github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0= github.com/ProtonMail/go-crypto v1.1.4-proton/go.mod h1:zNoyBJW3p/yVWiHNZgfTF9VsjwqYof5YY0M9kt2QaX0=
@ -45,14 +51,6 @@ github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423 h1:p8nBDx
github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-message v0.13.1-0.20240919135104-3bc88e6a9423/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ekTTXpdwKYF8eBlsYsDVoggDAuAjoK66k=
github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c h1:dxnbB+ov77BDj1LC35fKZ14hLoTpU6OTpZySwxarVx0=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250217140732-2e531f21de4c/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409092940-13ddc20a05a1 h1:u3G9UB8prOnzOneOf0JFCIVnMRLiK4QgEpPQVu9Y8Q4=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409092940-13ddc20a05a1/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409131808-0bbc8e7c32db h1:mOtbY5BB2eNr2QmbZhFn5EnsJcimTntPB6akN2r+AuE=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250409131808-0bbc8e7c32db/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250410050801-92de6e7c8517 h1:70JoDgXxfil4hbDoYGF98rMd47Rld6wXWyFAw4uFOTY=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250410050801-92de6e7c8517/go.mod h1:RYgagBFkA3zFrSt7/vviFFwjZxBo6pGzcTwFsLwsnyc=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba h1:DFBngZ7u/f69flRFzPp6Ipo6PKEyflJlA5OCh52yDB4= github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba h1:DFBngZ7u/f69flRFzPp6Ipo6PKEyflJlA5OCh52yDB4=
github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba/go.mod h1:eXIoLyIHxvPo8Kd9e1ygYIrAwbeWJhLi3vgSz2crlK4= github.com/ProtonMail/go-proton-api v0.4.1-0.20250417134000-e624a080f7ba/go.mod h1:eXIoLyIHxvPo8Kd9e1ygYIrAwbeWJhLi3vgSz2crlK4=
github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8= github.com/ProtonMail/go-smtp v0.0.0-20231109081432-2b3d50599865 h1:EP1gnxLL5Z7xBSymE9nSTM27nRYINuvssAtDmG0suD8=
@ -506,8 +504,6 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -565,8 +561,6 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -583,8 +577,6 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -625,8 +617,6 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@ -648,8 +638,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -746,7 +746,7 @@ func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric)
bridge.observabilityService.AddMetrics(metric) bridge.observabilityService.AddMetrics(metric)
} }
func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) { func (bridge *Bridge) PushDistinctObservabilityMetrics(errType observability.DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) {
bridge.observabilityService.AddDistinctMetrics(errType, metrics...) bridge.observabilityService.AddDistinctMetrics(errType, metrics...)
} }

View File

@ -25,7 +25,7 @@ func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySende
func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder } func (m *MockObservabilitySender) EXPECT() *MockObservabilitySenderRecorder { return m.recorder }
func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) { func (m *MockObservabilitySender) AddDistinctMetrics(errType observability.DistinctionMetricTypeEnum, _ ...proton.ObservabilityMetric) {
m.ctrl.T.Helper() m.ctrl.T.Helper()
m.ctrl.Call(m, "AddDistinctMetrics", errType) m.ctrl.Call(m, "AddDistinctMetrics", errType)
} }
@ -35,7 +35,18 @@ func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetr
m.ctrl.Call(m, "AddMetrics", metrics) m.ctrl.Call(m, "AddMetrics", metrics)
} }
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionErrorTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call { func (m *MockObservabilitySender) AddTimeLimitedMetric(metricType observability.DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "AddTimeLimitedMetric", metricType, metric)
}
func (m *MockObservabilitySender) GetEmailClient() string {
m.ctrl.T.Helper()
m.ctrl.Call(m, "GetEmailClient")
return ""
}
func (mr *MockObservabilitySenderRecorder) AddDistinctMetrics(errType observability.DistinctionMetricTypeEnum, _ ...proton.ObservabilityMetric) *gomock.Call {
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, return mr.mock.ctrl.RecordCallWithMethodType(mr.mock,
"AddDistinctMetrics", "AddDistinctMetrics",
@ -47,3 +58,13 @@ func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.Observab
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddMetrics", reflect.TypeOf((*MockObservabilitySender)(nil).AddMetrics), metrics)
} }
func (mr *MockObservabilitySenderRecorder) AddTimeLimitedMetric(metricType observability.DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddTimeLimitedMetric", reflect.TypeOf((*MockObservabilitySender)(nil).AddTimeLimitedMetric), metricType, metric)
}
func (mr *MockObservabilitySenderRecorder) GetEmailClient() {
mr.mock.ctrl.T.Helper()
mr.mock.ctrl.Call(mr.mock, "GetEmailClient", reflect.TypeOf((*MockObservabilitySender)(nil).GetEmailClient))
}

View File

@ -551,7 +551,7 @@ func (bridge *Bridge) addUserWithVault(
syncSettingsPath, syncSettingsPath,
isNew, isNew,
bridge.notificationStore, bridge.notificationStore,
bridge.unleashService.GetFlagValue, bridge.unleashService,
) )
if err != nil { if err != nil {
return fmt.Errorf("failed to create user: %w", err) return fmt.Errorf("failed to create user: %w", err)

View File

@ -96,7 +96,7 @@ func TestTLSSignedCertTrustedPublicKey(t *testing.T) {
_, dialer, _, checker, _ := createClientWithPinningDialer("") _, dialer, _, checker, _ := createClientWithPinningDialer("")
copyTrustedPins(checker) copyTrustedPins(checker)
checker.trustedPins = append(checker.trustedPins, `pin-sha256="hgraU1+uoS6kjiJaH5G+BiqQoyiIml1Nat+2FiUAcII="`) checker.trustedPins = append(checker.trustedPins, `pin-sha256="FlvTPG/nIMKtOj9nelnEjujwSZ5EDyfiKYxZgbXREls="`)
_, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443") _, err := dialer.DialTLSContext(context.Background(), "tcp", "rsa4096.badssl.com:443")
r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA") r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA")
} }

View File

@ -36,6 +36,9 @@ type IMAPServerManager interface {
RemoveIMAPUser(ctx context.Context, deleteData bool, provider GluonIDProvider, addrID ...string) error RemoveIMAPUser(ctx context.Context, deleteData bool, provider GluonIDProvider, addrID ...string) error
LogRemoteLabelIDs(ctx context.Context, provider GluonIDProvider, addrID ...string) error LogRemoteLabelIDs(ctx context.Context, provider GluonIDProvider, addrID ...string) error
GetOpenIMAPSessionCount() int
GetRollingIMAPConnectionCount() int
} }
type NullIMAPServerManager struct{} type NullIMAPServerManager struct{}
@ -67,6 +70,14 @@ func (n NullIMAPServerManager) LogRemoteLabelIDs(
return nil return nil
} }
func (n NullIMAPServerManager) GetOpenIMAPSessionCount() int {
return 0
}
func (n NullIMAPServerManager) GetRollingIMAPConnectionCount() int {
return 0
}
func NewNullIMAPServerManager() *NullIMAPServerManager { func NewNullIMAPServerManager() *NullIMAPServerManager {
return &NullIMAPServerManager{} return &NullIMAPServerManager{}
} }

View File

@ -24,6 +24,7 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/ProtonMail/gluon" "github.com/ProtonMail/gluon"
@ -40,6 +41,12 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const (
rollingCounterNewConnectionThreshold = 300
rollingCounterNumberOfBuckets = 6
rollingCounterBucketRotationInterval = time.Second * 10
)
var logIMAP = logrus.WithField("pkg", "server/imap") //nolint:gochecknoglobals var logIMAP = logrus.WithField("pkg", "server/imap") //nolint:gochecknoglobals
type IMAPSettingsProvider interface { type IMAPSettingsProvider interface {
@ -126,6 +133,7 @@ func newIMAPServer(
gluon.WithUIDValidityGenerator(uidValidityGenerator), gluon.WithUIDValidityGenerator(uidValidityGenerator),
gluon.WithPanicHandler(panicHandler), gluon.WithPanicHandler(panicHandler),
gluon.WithObservabilitySender(observability.NewAdapter(observabilitySender), int(observability.GluonImapError), int(observability.GluonMessageError), int(observability.GluonOtherError)), gluon.WithObservabilitySender(observability.NewAdapter(observabilitySender), int(observability.GluonImapError), int(observability.GluonMessageError), int(observability.GluonOtherError)),
gluon.WithConnectionRollingCounter(rollingCounterNewConnectionThreshold, rollingCounterNumberOfBuckets, rollingCounterBucketRotationInterval),
} }
if disableIMAPAuthenticate { if disableIMAPAuthenticate {

View File

@ -200,6 +200,14 @@ func (sm *Service) RemoveSMTPAccount(ctx context.Context, service *bridgesmtp.Se
return err return err
} }
func (sm *Service) GetOpenIMAPSessionCount() int {
return sm.imapServer.GetOpenSessionCount()
}
func (sm *Service) GetRollingIMAPConnectionCount() int {
return sm.imapServer.GetRollingIMAPConnectionCount()
}
func (sm *Service) run(ctx context.Context, subscription events.Subscription) { func (sm *Service) run(ctx context.Context, subscription events.Subscription) {
eventSub := subscription.Add() eventSub := subscription.Add()
defer subscription.Remove(eventSub) defer subscription.Remove(eventSub)

View File

@ -44,7 +44,7 @@ type Service struct {
store *Store store *Store
getFlagValueFn unleash.GetFlagValueFn featureFlagValueProvider unleash.FeatureFlagValueProvider
observabilitySender observability.Sender observabilitySender observability.Sender
} }
@ -52,7 +52,7 @@ type Service struct {
const bitfieldRegexPattern = `^\\\d+` const bitfieldRegexPattern = `^\\\d+`
func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store, func NewService(userID string, service userevents.Subscribable, eventPublisher events.EventPublisher, store *Store,
getFlagFn unleash.GetFlagValueFn, observabilitySender observability.Sender) *Service { featureFlagValueProvider unleash.FeatureFlagValueProvider, observabilitySender observability.Sender) *Service {
return &Service{ return &Service{
userID: userID, userID: userID,
@ -68,8 +68,8 @@ func NewService(userID string, service userevents.Subscribable, eventPublisher e
store: store, store: store,
getFlagValueFn: getFlagFn, featureFlagValueProvider: featureFlagValueProvider,
observabilitySender: observabilitySender, observabilitySender: observabilitySender,
} }
} }
@ -102,7 +102,7 @@ func (s *Service) run(ctx context.Context) {
} }
func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEvents []proton.NotificationEvent) error { func (s *Service) HandleNotificationEvents(ctx context.Context, notificationEvents []proton.NotificationEvent) error {
if s.getFlagValueFn(unleash.EventLoopNotificationDisabled) { if s.featureFlagValueProvider.GetFlagValue(unleash.EventLoopNotificationDisabled) {
s.log.Info("Received notification events. Skipping as kill switch is enabled.") s.log.Info("Received notification events. Skipping as kill switch is enabled.")
return nil return nil
} }

View File

@ -19,6 +19,7 @@ package observability
import ( import (
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability/gluonmetrics"
) )
type Adapter struct { type Adapter struct {
@ -88,6 +89,15 @@ func (adapter *Adapter) AddDistinctMetrics(errType interface{}, metrics ...map[s
} }
if len(typedMetrics) > 0 { if len(typedMetrics) > 0 {
adapter.sender.AddDistinctMetrics(DistinctionErrorTypeEnum(errTypeInt), typedMetrics...) adapter.sender.AddDistinctMetrics(DistinctionMetricTypeEnum(errTypeInt), typedMetrics...)
} }
} }
func (adapter *Adapter) AddIMAPConnectionsExceededThresholdMetric(totalOpenIMAPConnections, newIMAPConnections int) {
metric := gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold(
adapter.sender.GetEmailClient(),
BucketIMAPConnections(totalOpenIMAPConnections),
BucketIMAPConnections(newIMAPConnections))
adapter.sender.AddTimeLimitedMetric(NewIMAPConnectionsExceedThreshold, metric)
}

View File

@ -19,21 +19,22 @@ package observability
import "time" import "time"
// DistinctionErrorTypeEnum - maps to the specific error schema for which we // DistinctionMetricTypeEnum - used to distinct specific metrics which we want to limit over some interval.
// want to send a user update. // Most enums are tied to a specific error schema for which we also send a specific distinction user update.
type DistinctionErrorTypeEnum int type DistinctionMetricTypeEnum int
const ( const (
SyncError DistinctionErrorTypeEnum = iota SyncError DistinctionMetricTypeEnum = iota
GluonImapError GluonImapError
GluonMessageError GluonMessageError
GluonOtherError GluonOtherError
SMTPError SMTPError
EventLoopError // EventLoopError - should always be kept last when inserting new keys. EventLoopError // EventLoopError - should always be kept last when inserting new keys.
NewIMAPConnectionsExceedThreshold
) )
// errorSchemaMap - maps between the DistinctionErrorTypeEnum and the relevant schema name. // errorSchemaMap - maps between some DistinctionMetricTypeEnum and the relevant schema name.
var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglobals var errorSchemaMap = map[DistinctionMetricTypeEnum]string{ //nolint:gochecknoglobals
SyncError: "bridge_sync_errors_users_total", SyncError: "bridge_sync_errors_users_total",
EventLoopError: "bridge_event_loop_events_errors_users_total", EventLoopError: "bridge_event_loop_events_errors_users_total",
GluonImapError: "bridge_gluon_imap_errors_users_total", GluonImapError: "bridge_gluon_imap_errors_users_total",
@ -43,9 +44,9 @@ var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglob
} }
// createLastSentMap - needs to be updated whenever we make changes to the enum. // createLastSentMap - needs to be updated whenever we make changes to the enum.
func createLastSentMap() map[DistinctionErrorTypeEnum]time.Time { func createLastSentMap() map[DistinctionMetricTypeEnum]time.Time {
registerTime := time.Now().Add(-updateInterval) registerTime := time.Now().Add(-updateInterval)
lastSentMap := make(map[DistinctionErrorTypeEnum]time.Time) lastSentMap := make(map[DistinctionMetricTypeEnum]time.Time)
for errType := SyncError; errType <= EventLoopError; errType++ { for errType := SyncError; errType <= EventLoopError; errType++ {
lastSentMap[errType] = registerTime lastSentMap[errType] = registerTime

View File

@ -40,7 +40,7 @@ type distinctionUtility struct {
panicHandler async.PanicHandler panicHandler async.PanicHandler
lastSentMap map[DistinctionErrorTypeEnum]time.Time // Ensures we don't step over the limit of one user update every 5 mins. lastSentMap map[DistinctionMetricTypeEnum]time.Time // Ensures we don't step over the limit of one user update every 5 mins.
observabilitySender observabilitySender observabilitySender observabilitySender
settingsGetter settingsGetter settingsGetter settingsGetter
@ -87,7 +87,7 @@ func (d *distinctionUtility) setSettingsGetter(getter settingsGetter) {
// checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric // checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric
// within the last 5 minutes. // within the last 5 minutes.
func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeEnum) bool { func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionMetricTypeEnum) bool {
curTime := time.Now() curTime := time.Now()
val, ok := d.lastSentMap[key] val, ok := d.lastSentMap[key]
if !ok { if !ok {
@ -107,7 +107,7 @@ func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeE
// and the relevant settings. In the future this will need to be expanded to support multiple // and the relevant settings. In the future this will need to be expanded to support multiple
// versions of the metric if we ever decide to change them. // versions of the metric if we ever decide to change them.
func (d *distinctionUtility) generateUserMetric( func (d *distinctionUtility) generateUserMetric(
metricType DistinctionErrorTypeEnum, metricType DistinctionMetricTypeEnum,
) proton.ObservabilityMetric { ) proton.ObservabilityMetric {
schemaName, ok := errorSchemaMap[metricType] schemaName, ok := errorSchemaMap[metricType]
if !ok { if !ok {
@ -138,7 +138,7 @@ func generateUserMetric(schemaName, plan, mailClient, dohEnabled, betaAccess str
} }
} }
func (d *distinctionUtility) generateDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) []proton.ObservabilityMetric { func (d *distinctionUtility) generateDistinctMetrics(errType DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) []proton.ObservabilityMetric {
d.updateHeartbeatData(errType) d.updateHeartbeatData(errType)
if d.checkAndUpdateLastSentMap(errType) { if d.checkAndUpdateLastSentMap(errType) {

View File

@ -0,0 +1,45 @@
// Copyright (c) 2025 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Proton Mail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package gluonmetrics
import (
"time"
"github.com/ProtonMail/go-proton-api"
)
const (
newIMAPConnectionThresholdExceededSchemaName = "bridge_imap_recently_opened_connections_total"
newIMAPConnectionThresholdExceededVersion = 1
)
func GenerateNewOpenedIMAPConnectionsExceedThreshold(emailClient, totalOpenIMAPConnectionCount, newlyOpenedIMAPConnectionCount string) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: newIMAPConnectionThresholdExceededSchemaName,
Version: newIMAPConnectionThresholdExceededVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"mailClient": emailClient,
"numberOfOpenIMAPConnectionsBuckets": totalOpenIMAPConnectionCount,
"numberOfRecentlyOpenedIMAPConnectionsBuckets": newlyOpenedIMAPConnectionCount,
},
},
}
}

View File

@ -42,7 +42,7 @@ func (d *distinctionUtility) resetHeartbeatData() {
d.heartbeatData.receivedGluonError = false d.heartbeatData.receivedGluonError = false
} }
func (d *distinctionUtility) updateHeartbeatData(errType DistinctionErrorTypeEnum) { func (d *distinctionUtility) updateHeartbeatData(errType DistinctionMetricTypeEnum) {
d.withUpdateHeartbeatDataLock(func() { d.withUpdateHeartbeatDataLock(func() {
//nolint:exhaustive //nolint:exhaustive
switch errType { switch errType {

View File

@ -45,7 +45,9 @@ type client struct {
// so we can easily pass them down to relevant components. // so we can easily pass them down to relevant components.
type Sender interface { type Sender interface {
AddMetrics(metrics ...proton.ObservabilityMetric) AddMetrics(metrics ...proton.ObservabilityMetric)
AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) AddDistinctMetrics(errType DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric)
AddTimeLimitedMetric(metricType DistinctionMetricTypeEnum, metric proton.ObservabilityMetric)
GetEmailClient() string
} }
type Service struct { type Service struct {
@ -325,11 +327,25 @@ func (s *Service) AddMetrics(metrics ...proton.ObservabilityMetric) {
// what number of events come from what number of users. // what number of events come from what number of users.
// As the binning interval is what allows us to do this we // As the binning interval is what allows us to do this we
// should not send these if there are no logged-in users at that moment. // should not send these if there are no logged-in users at that moment.
func (s *Service) AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) { func (s *Service) AddDistinctMetrics(errType DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) {
metrics = s.distinctionUtility.generateDistinctMetrics(errType, metrics...) metrics = s.distinctionUtility.generateDistinctMetrics(errType, metrics...)
s.addMetricsIfClients(metrics...) s.addMetricsIfClients(metrics...)
} }
// AddTimeLimitedMetric - schedules a metric to be sent if a metric of the same type has not been sent within some interval.
// The interval is defined in the distinction utility.
func (s *Service) AddTimeLimitedMetric(metricType DistinctionMetricTypeEnum, metric proton.ObservabilityMetric) {
if !s.distinctionUtility.checkAndUpdateLastSentMap(metricType) {
return
}
s.addMetricsIfClients(metric)
}
func (s *Service) GetEmailClient() string {
return s.distinctionUtility.getEmailClientUserAgent()
}
// ModifyHeartbeatInterval - should only be used for testing. Resets the heartbeat ticker. // ModifyHeartbeatInterval - should only be used for testing. Resets the heartbeat ticker.
func (s *Service) ModifyHeartbeatInterval(duration time.Duration) { func (s *Service) ModifyHeartbeatInterval(duration time.Duration) {
s.distinctionUtility.heartbeatTicker.Reset(duration) s.distinctionUtility.heartbeatTicker.Reset(duration)

View File

@ -66,3 +66,30 @@ func getEnabled(value bool) string {
} }
return "enabled" return "enabled"
} }
func BucketIMAPConnections(val int) string {
switch {
case val < 10:
return "<10"
case val < 25:
return "10-24"
case val < 50:
return "25-49"
case val < 100:
return "50-99"
case val < 200:
return "100-199"
case val < 300:
return "200-299"
case val < 500:
return "300-499"
case val < 1000:
return "500-999"
case val < 2000:
return "1000-1999"
case val < 3000:
return "2000-2999"
default:
return "3000+"
}
}

View File

@ -21,6 +21,7 @@ import (
"time" "time"
"github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
) )
const ( const (
@ -29,6 +30,9 @@ const (
smtpSendSuccessSchemaName = "bridge_smtp_send_success_total" smtpSendSuccessSchemaName = "bridge_smtp_send_success_total"
smtpSendSuccessSchemaVersion = 1 smtpSendSuccessSchemaVersion = 1
smtpSubmissionRequestSchemaName = "bridge_smtp_send_request_total"
smtpSubmissionRequestSchemaVersion = 1
) )
func generateSMTPErrorObservabilityMetric(errorType string) proton.ObservabilityMetric { func generateSMTPErrorObservabilityMetric(errorType string) proton.ObservabilityMetric {
@ -88,3 +92,19 @@ func GenerateSMTPSendSuccess() proton.ObservabilityMetric {
}, },
} }
} }
func GenerateSMTPSubmissionRequest(emailClient string, numberOfOpenIMAPConnections, numberOfRecentlyOpenedIMAPConnections int) proton.ObservabilityMetric {
return proton.ObservabilityMetric{
Name: smtpSubmissionRequestSchemaName,
Version: smtpSubmissionRequestSchemaVersion,
Timestamp: time.Now().Unix(),
Data: map[string]interface{}{
"Value": 1,
"Labels": map[string]string{
"numberOfOpenIMAPConnections": observability.BucketIMAPConnections(numberOfOpenIMAPConnections),
"numberOfRecentlyOpenedIMAPConnections": observability.BucketIMAPConnections(numberOfRecentlyOpenedIMAPConnections),
"mailClient": emailClient,
},
},
}
}

View File

@ -32,13 +32,24 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability" "github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks" "github.com/ProtonMail/proton-bridge/v3/internal/services/orderedtasks"
"github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder" "github.com/ProtonMail/proton-bridge/v3/internal/services/sendrecorder"
"github.com/ProtonMail/proton-bridge/v3/internal/services/smtp/observabilitymetrics"
"github.com/ProtonMail/proton-bridge/v3/internal/services/userevents" "github.com/ProtonMail/proton-bridge/v3/internal/services/userevents"
"github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity" "github.com/ProtonMail/proton-bridge/v3/internal/services/useridentity"
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
"github.com/ProtonMail/proton-bridge/v3/internal/usertypes" "github.com/ProtonMail/proton-bridge/v3/internal/usertypes"
"github.com/ProtonMail/proton-bridge/v3/pkg/cpc" "github.com/ProtonMail/proton-bridge/v3/pkg/cpc"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const (
newlyOpenedIMAPConnectionsThreshold = 300
)
type imapSessionCountProvider interface {
GetOpenIMAPSessionCount() int
GetRollingIMAPConnectionCount() int
}
type Service struct { type Service struct {
userID string userID string
panicHandler async.PanicHandler panicHandler async.PanicHandler
@ -59,6 +70,9 @@ type Service struct {
serverManager ServerManager serverManager ServerManager
observabilitySender observability.Sender observabilitySender observability.Sender
imapSessionCountProvider imapSessionCountProvider
featureFlagValueProvider unleash.FeatureFlagValueProvider
} }
func NewService( func NewService(
@ -74,6 +88,8 @@ func NewService(
identityState *useridentity.State, identityState *useridentity.State,
serverManager ServerManager, serverManager ServerManager,
observabilitySender observability.Sender, observabilitySender observability.Sender,
imapSessionCountProvider imapSessionCountProvider,
featureFlagValueProvider unleash.FeatureFlagValueProvider,
) *Service { ) *Service {
subscriberName := fmt.Sprintf("smpt-%v", userID) subscriberName := fmt.Sprintf("smpt-%v", userID)
@ -99,7 +115,9 @@ func NewService(
addressMode: mode, addressMode: mode,
serverManager: serverManager, serverManager: serverManager,
observabilitySender: observabilitySender, imapSessionCountProvider: imapSessionCountProvider,
observabilitySender: observabilitySender,
featureFlagValueProvider: featureFlagValueProvider,
} }
} }
@ -207,7 +225,6 @@ func (s *Service) run(ctx context.Context) {
switch r := request.Value().(type) { switch r := request.Value().(type) {
case *sendMailReq: case *sendMailReq:
s.log.Debug("Received send mail request")
err := s.sendMail(ctx, r) err := s.sendMail(ctx, r)
request.Reply(ctx, nil, err) request.Reply(ctx, nil, err)
@ -252,15 +269,38 @@ type sendMailReq struct {
func (s *Service) sendMail(ctx context.Context, req *sendMailReq) error { func (s *Service) sendMail(ctx context.Context, req *sendMailReq) error {
defer async.HandlePanic(s.panicHandler) defer async.HandlePanic(s.panicHandler)
openSessionCount := s.imapSessionCountProvider.GetOpenIMAPSessionCount()
newlyOpenedSessions := s.imapSessionCountProvider.GetRollingIMAPConnectionCount()
log := s.log.WithFields(logrus.Fields{
"newlyOpenedIMAPConnectionsCount": newlyOpenedSessions,
"openIMAPConnectionsCount": openSessionCount,
})
log.Debug("Received send mail request")
// Send SMTP send request metric to observability.
s.observabilitySender.AddMetrics(observabilitymetrics.GenerateSMTPSubmissionRequest(s.observabilitySender.GetEmailClient(), openSessionCount, newlyOpenedSessions))
// Send report to sentry if kill switch is disabled & number of newly opened IMAP connections exceed threshold.
if !s.featureFlagValueProvider.GetFlagValue(unleash.SMTPSubmissionRequestSentryReportDisabled) && newlyOpenedSessions >= newlyOpenedIMAPConnectionsThreshold {
if err := s.reporter.ReportMessageWithContext("SMTP Send Mail Request - newly opened IMAP connections exceed threshold", reporter.Context{
"newlyOpenedIMAPConnectionsCount": newlyOpenedSessions,
"openIMAPConnectionsCount": openSessionCount,
"emailClient": s.observabilitySender.GetEmailClient(),
}); err != nil {
s.log.WithError(err).Error("Failed to submit report to sentry (SMTP Send Mail Request)")
}
}
start := time.Now() start := time.Now()
defer func() { defer func() {
end := time.Now() end := time.Now()
s.log.Debugf("Send mail request finished in %v", end.Sub(start)) log.Debugf("Send mail request finished in %v", end.Sub(start))
}() }()
if err := s.smtpSendMail(ctx, req.authID, req.from, req.to, req.r); err != nil { if err := s.smtpSendMail(ctx, req.authID, req.from, req.to, req.r); err != nil {
if apiErr := new(proton.APIError); errors.As(err, &apiErr) { if apiErr := new(proton.APIError); errors.As(err, &apiErr) {
s.log.WithError(apiErr).WithField("Details", apiErr.DetailsToString()).Error("failed to send message") log.WithError(apiErr).WithField("Details", apiErr.DetailsToString()).Error("failed to send message")
} }
return err return err

View File

@ -37,15 +37,29 @@ var pollJitter = 2 * time.Minute //nolint:gochecknoglobals
const filename = "unleash_flags" const filename = "unleash_flags"
const ( const (
EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled" EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled"
IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled" IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled"
UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled" UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled"
UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled" UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled"
SMTPSubmissionRequestSentryReportDisabled = "InboxBridgeSmtpSubmissionRequestSentryReportDisabled"
) )
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error) type FeatureFlagValueProvider interface {
type GetFlagValueFn func(key string) bool GetFlagValue(key string) bool
}
// NullUnleashService - mock of the unleash service. Should be used for testing.
type NullUnleashService struct{}
func (n NullUnleashService) GetFlagValue(_ string) bool {
return false
}
func NewNullUnleashService() *NullUnleashService {
return &NullUnleashService{}
}
type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error)
type Service struct { type Service struct {
panicHandler async.PanicHandler panicHandler async.PanicHandler
timer *proton.Ticker timer *proton.Ticker

View File

@ -110,7 +110,7 @@ func New(
syncConfigDir string, syncConfigDir string,
isNew bool, isNew bool,
notificationStore *notifications.Store, notificationStore *notifications.Store,
getFlagValFn unleash.GetFlagValueFn, featureFlagValueProvider unleash.FeatureFlagValueProvider,
) (*User, error) { ) (*User, error) {
user, err := newImpl( user, err := newImpl(
ctx, ctx,
@ -130,7 +130,7 @@ func New(
syncConfigDir, syncConfigDir,
isNew, isNew,
notificationStore, notificationStore,
getFlagValFn, featureFlagValueProvider,
) )
if err != nil { if err != nil {
// Cleanup any pending resources on error // Cleanup any pending resources on error
@ -163,7 +163,7 @@ func newImpl(
syncConfigDir string, syncConfigDir string,
isNew bool, isNew bool,
notificationStore *notifications.Store, notificationStore *notifications.Store,
getFlagValueFn unleash.GetFlagValueFn, featureFlagValueProvider unleash.FeatureFlagValueProvider,
) (*User, error) { ) (*User, error) {
logrus.WithField("userID", apiUser.ID).Info("Creating new user") logrus.WithField("userID", apiUser.ID).Info("Creating new user")
@ -262,6 +262,8 @@ func newImpl(
identityState.Clone(), identityState.Clone(),
smtpServerManager, smtpServerManager,
observabilityService, observabilityService,
imapServerManager,
featureFlagValueProvider,
) )
user.imapService = imapservice.NewService( user.imapService = imapservice.NewService(
@ -284,7 +286,7 @@ func newImpl(
observabilityService, observabilityService,
) )
user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, getFlagValueFn, observabilityService) user.notificationService = notifications.NewService(user.id, user.eventService, user, notificationStore, featureFlagValueProvider, observabilityService)
// When we receive an auth object, we update it in the vault. // When we receive an auth object, we update it in the vault.
// This will be used to authorize the user on the next run. // This will be used to authorize the user on the next run.

View File

@ -33,6 +33,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability" "github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/services/smtp" "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp"
"github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks" "github.com/ProtonMail/proton-bridge/v3/internal/telemetry/mocks"
"github.com/ProtonMail/proton-bridge/v3/internal/unleash"
"github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/internal/vault"
"github.com/ProtonMail/proton-bridge/v3/tests" "github.com/ProtonMail/proton-bridge/v3/tests"
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
@ -150,6 +151,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
nullEventSubscription := events.NewNullSubscription() nullEventSubscription := events.NewNullSubscription()
nullIMAPServerManager := imapservice.NewNullIMAPServerManager() nullIMAPServerManager := imapservice.NewNullIMAPServerManager()
nullSMTPServerManager := smtp.NewNullServerManager() nullSMTPServerManager := smtp.NewNullServerManager()
nullUnleashService := unleash.NewNullUnleashService()
user, err := New( user, err := New(
ctx, ctx,
@ -171,9 +173,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
notifications.NewStore(func() (string, error) { notifications.NewStore(func() (string, error) {
return "", nil return "", nil
}), }),
func(_ string) bool { nullUnleashService,
return false
},
) )
require.NoError(tb, err) require.NoError(tb, err)
defer user.Close() defer user.Close()

View File

@ -45,5 +45,9 @@ Feature: Bridge send remote notification observability metrics
And the user with username "[user:user1]" sends SMTP send success observability metric And the user with username "[user:user1]" sends SMTP send success observability metric
Then it succeeds Then it succeeds
Scenario: Test SMTP send request observability metric
When the user logs in with username "[user:user1]" and password "password"
And the user with username "[user:user1]" sends an SMTP send request observability metric
Then it succeeds

View File

@ -9,3 +9,8 @@ Feature: Bridge send remote notification observability metrics
When the user logs in with username "[user:user1]" and password "password" When the user logs in with username "[user:user1]" and password "password"
And the user with username "[user:user1]" sends all possible gluon error observability metrics And the user with username "[user:user1]" sends all possible gluon error observability metrics
Then it succeeds Then it succeeds
Scenario: Test newly opened IMAP connections in Gluon exceed threshold metric
When the user logs in with username "[user:user1]" and password "password"
And the user with username "[user:user1]" sends a Gluon metric indicating that the number of newly opened IMAP connections within some interval have exceed a threshold value
Then it succeeds

View File

@ -25,6 +25,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/evtloopmsgevents"
"github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents" "github.com/ProtonMail/proton-bridge/v3/internal/services/imapservice/observabilitymetrics/syncmsgevents"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability" "github.com/ProtonMail/proton-bridge/v3/internal/services/observability"
"github.com/ProtonMail/proton-bridge/v3/internal/services/observability/gluonmetrics"
smtpMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp/observabilitymetrics" smtpMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp/observabilitymetrics"
"github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics" "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics"
) )
@ -188,3 +189,48 @@ func (s *scenario) SMTPSendSuccessObservabilityMetric(username string) error {
return err return err
}) })
} }
func (s *scenario) SMTPSendRequestObservabilityMetric(username string) error {
batch := proton.ObservabilityBatch{
Metrics: []proton.ObservabilityMetric{
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 1, 10),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 10, 25),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 30, 45),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 50, 75),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 100, 150),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 200, 250),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 300, 450),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 500, 750),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 1000, 1500),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 1900, 2500),
smtpMetrics.GenerateSMTPSubmissionRequest("outlook", 3000, 3500),
},
}
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
err := c.SendObservabilityBatch(ctx, batch)
return err
})
}
func (s *scenario) GluonNewlyOpenedIMAPConnectionsExceedThreshold(username string) error {
batch := proton.ObservabilityBatch{
Metrics: []proton.ObservabilityMetric{
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(1), observability.BucketIMAPConnections(10)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(10), observability.BucketIMAPConnections(25)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(30), observability.BucketIMAPConnections(45)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(50), observability.BucketIMAPConnections(75)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(100), observability.BucketIMAPConnections(150)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(200), observability.BucketIMAPConnections(250)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(300), observability.BucketIMAPConnections(450)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(500), observability.BucketIMAPConnections(750)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(1000), observability.BucketIMAPConnections(1500)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(1900), observability.BucketIMAPConnections(2500)),
gluonmetrics.GenerateNewOpenedIMAPConnectionsExceedThreshold("outlook", observability.BucketIMAPConnections(3000), observability.BucketIMAPConnections(3500)),
},
}
return s.t.withClientPass(context.Background(), username, s.t.getUserByName(username).userPass, func(ctx context.Context, c *proton.Client) error {
err := c.SendObservabilityBatch(ctx, batch)
return err
})
}

View File

@ -242,7 +242,11 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) {
// SMTP metrics // SMTP metrics
ctx.Step(`^the user with username "([^"]*)" sends all possible SMTP error observability metrics$`, s.SMTPErrorObservabilityMetrics) ctx.Step(`^the user with username "([^"]*)" sends all possible SMTP error observability metrics$`, s.SMTPErrorObservabilityMetrics)
ctx.Step(`^the user with username "([^"]*)" sends SMTP send success observability metric$`, s.SMTPSendSuccessObservabilityMetric) ctx.Step(`^the user with username "([^"]*)" sends SMTP send success observability metric$`, s.SMTPSendSuccessObservabilityMetric)
// SMTP submission metric
ctx.Step(`^the user with username "([^"]*)" sends an SMTP send request observability metric$`, s.SMTPSendRequestObservabilityMetric)
// Gluon related metrics // Gluon related metrics
ctx.Step(`^the user with username "([^"]*)" sends all possible gluon error observability metrics$`, s.testGluonErrorObservabilityMetrics) ctx.Step(`^the user with username "([^"]*)" sends all possible gluon error observability metrics$`, s.testGluonErrorObservabilityMetrics)
// Gluon metric - on newly opened IMAP connections exceeding threshold.
ctx.Step(`^the user with username "([^"]*)" sends a Gluon metric indicating that the number of newly opened IMAP connections within some interval have exceed a threshold value$`, s.GluonNewlyOpenedIMAPConnectionsExceedThreshold)
} }