From 89da7335b658b073245a4ddccc28424bab51230e Mon Sep 17 00:00:00 2001 From: Atanas Janeshliev Date: Mon, 12 May 2025 13:35:52 +0200 Subject: [PATCH] feat(BRIDGE-363): Observability metrics for IMAP connections; minor unleash service refactor; --- go.mod | 2 +- go.sum | 28 ++++------- internal/bridge/bridge.go | 2 +- internal/bridge/mocks/observability_mocks.go | 25 +++++++++- internal/bridge/user.go | 2 +- internal/dialer/dialer_pinning_test.go | 2 +- .../services/imapservice/server_manager.go | 11 +++++ internal/services/imapsmtpserver/imap.go | 8 ++++ internal/services/imapsmtpserver/service.go | 8 ++++ internal/services/notifications/service.go | 10 ++-- internal/services/observability/adapter.go | 12 ++++- .../observability/distinction_error_types.go | 17 +++---- .../observability/distinction_utility.go | 8 ++-- .../observability/gluonmetrics/metrics.go | 45 +++++++++++++++++ internal/services/observability/heartbeat.go | 2 +- internal/services/observability/service.go | 20 +++++++- internal/services/observability/utils.go | 27 +++++++++++ .../smtp/observabilitymetrics/metrics.go | 20 ++++++++ internal/services/smtp/service.go | 48 +++++++++++++++++-- internal/unleash/service.go | 26 +++++++--- internal/user/user.go | 10 ++-- internal/user/user_test.go | 6 +-- .../observability/all_metrics.feature | 4 ++ .../observability/gluon_metrics.feature | 5 ++ tests/observability_test.go | 46 ++++++++++++++++++ tests/steps_test.go | 4 ++ 26 files changed, 334 insertions(+), 64 deletions(-) create mode 100644 internal/services/observability/gluonmetrics/metrics.go diff --git a/go.mod b/go.mod index b2c6434d..30ae1d2e 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.24.2 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 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-proton-api v0.4.1-0.20250417134000-e624a080f7ba github.com/ProtonMail/gopenpgp/v2 v2.8.2-proton diff --git a/go.sum b/go.sum index b9aea9c6..170a199b 100644 --- a/go.sum +++ b/go.sum @@ -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-20211005172633-e235017c1baf h1:yc9daCCYUefEs69zUkSzubzjBbL+cmOXgnmt9Fyd9ug= 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.20250324123053-2abce471ad71/go.mod h1:0/c03TzZPNiSgY5UDJK1iRDkjlDPwWugxTT6et2qDu8= +github.com/ProtonMail/gluon v0.17.1-0.20250513141309-843796a505bc h1:2oppv7H5ZeFnRDohTbLZW5A8I1ylhoX2QEi3RtKxrLE= +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 v1.1.4-proton h1:KIo9uNlk3vzlwI7o5VjhiEjI4Ld1TDixOMnoNZyfpFE= 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-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-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/go.mod h1:eXIoLyIHxvPo8Kd9e1ygYIrAwbeWJhLi3vgSz2crlK4= 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.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 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/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= 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.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= 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/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 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-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.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/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= 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.8.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/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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.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.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/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/bridge/bridge.go b/internal/bridge/bridge.go index f5cab833..9099ceeb 100644 --- a/internal/bridge/bridge.go +++ b/internal/bridge/bridge.go @@ -746,7 +746,7 @@ func (bridge *Bridge) PushObservabilityMetric(metric proton.ObservabilityMetric) 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...) } diff --git a/internal/bridge/mocks/observability_mocks.go b/internal/bridge/mocks/observability_mocks.go index 3f733cc5..643b532f 100644 --- a/internal/bridge/mocks/observability_mocks.go +++ b/internal/bridge/mocks/observability_mocks.go @@ -25,7 +25,7 @@ func NewMockObservabilitySender(ctrl *gomock.Controller) *MockObservabilitySende 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.Call(m, "AddDistinctMetrics", errType) } @@ -35,7 +35,18 @@ func (m *MockObservabilitySender) AddMetrics(metrics ...proton.ObservabilityMetr 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() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddDistinctMetrics", @@ -47,3 +58,13 @@ func (mr *MockObservabilitySenderRecorder) AddMetrics(metrics ...proton.Observab mr.mock.ctrl.T.Helper() 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)) +} diff --git a/internal/bridge/user.go b/internal/bridge/user.go index b76abfc9..2947f082 100644 --- a/internal/bridge/user.go +++ b/internal/bridge/user.go @@ -551,7 +551,7 @@ func (bridge *Bridge) addUserWithVault( syncSettingsPath, isNew, bridge.notificationStore, - bridge.unleashService.GetFlagValue, + bridge.unleashService, ) if err != nil { return fmt.Errorf("failed to create user: %w", err) diff --git a/internal/dialer/dialer_pinning_test.go b/internal/dialer/dialer_pinning_test.go index 21111090..e74b81ad 100644 --- a/internal/dialer/dialer_pinning_test.go +++ b/internal/dialer/dialer_pinning_test.go @@ -96,7 +96,7 @@ func TestTLSSignedCertTrustedPublicKey(t *testing.T) { _, dialer, _, checker, _ := createClientWithPinningDialer("") 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") r.NoError(t, err, "expected dial to succeed because public key is known and cert is signed by CA") } diff --git a/internal/services/imapservice/server_manager.go b/internal/services/imapservice/server_manager.go index 656638f0..ad2dbd33 100644 --- a/internal/services/imapservice/server_manager.go +++ b/internal/services/imapservice/server_manager.go @@ -36,6 +36,9 @@ type IMAPServerManager interface { RemoveIMAPUser(ctx context.Context, deleteData bool, provider GluonIDProvider, addrID ...string) error LogRemoteLabelIDs(ctx context.Context, provider GluonIDProvider, addrID ...string) error + + GetOpenIMAPSessionCount() int + GetRollingIMAPConnectionCount() int } type NullIMAPServerManager struct{} @@ -67,6 +70,14 @@ func (n NullIMAPServerManager) LogRemoteLabelIDs( return nil } +func (n NullIMAPServerManager) GetOpenIMAPSessionCount() int { + return 0 +} + +func (n NullIMAPServerManager) GetRollingIMAPConnectionCount() int { + return 0 +} + func NewNullIMAPServerManager() *NullIMAPServerManager { return &NullIMAPServerManager{} } diff --git a/internal/services/imapsmtpserver/imap.go b/internal/services/imapsmtpserver/imap.go index f5bd0eec..0916db2c 100644 --- a/internal/services/imapsmtpserver/imap.go +++ b/internal/services/imapsmtpserver/imap.go @@ -24,6 +24,7 @@ import ( "io" "os" "path/filepath" + "time" "github.com/Masterminds/semver/v3" "github.com/ProtonMail/gluon" @@ -40,6 +41,12 @@ import ( "github.com/sirupsen/logrus" ) +const ( + rollingCounterNewConnectionThreshold = 300 + rollingCounterNumberOfBuckets = 6 + rollingCounterBucketRotationInterval = time.Second * 10 +) + var logIMAP = logrus.WithField("pkg", "server/imap") //nolint:gochecknoglobals type IMAPSettingsProvider interface { @@ -126,6 +133,7 @@ func newIMAPServer( gluon.WithUIDValidityGenerator(uidValidityGenerator), gluon.WithPanicHandler(panicHandler), gluon.WithObservabilitySender(observability.NewAdapter(observabilitySender), int(observability.GluonImapError), int(observability.GluonMessageError), int(observability.GluonOtherError)), + gluon.WithConnectionRollingCounter(rollingCounterNewConnectionThreshold, rollingCounterNumberOfBuckets, rollingCounterBucketRotationInterval), } if disableIMAPAuthenticate { diff --git a/internal/services/imapsmtpserver/service.go b/internal/services/imapsmtpserver/service.go index 2a024835..d2c0e95d 100644 --- a/internal/services/imapsmtpserver/service.go +++ b/internal/services/imapsmtpserver/service.go @@ -200,6 +200,14 @@ func (sm *Service) RemoveSMTPAccount(ctx context.Context, service *bridgesmtp.Se 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) { eventSub := subscription.Add() defer subscription.Remove(eventSub) diff --git a/internal/services/notifications/service.go b/internal/services/notifications/service.go index 7354e26f..0325e9bc 100644 --- a/internal/services/notifications/service.go +++ b/internal/services/notifications/service.go @@ -44,7 +44,7 @@ type Service struct { store *Store - getFlagValueFn unleash.GetFlagValueFn + featureFlagValueProvider unleash.FeatureFlagValueProvider observabilitySender observability.Sender } @@ -52,7 +52,7 @@ type Service struct { const bitfieldRegexPattern = `^\\\d+` 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{ userID: userID, @@ -68,8 +68,8 @@ func NewService(userID string, service userevents.Subscribable, eventPublisher e store: store, - getFlagValueFn: getFlagFn, - observabilitySender: observabilitySender, + featureFlagValueProvider: featureFlagValueProvider, + observabilitySender: observabilitySender, } } @@ -102,7 +102,7 @@ func (s *Service) run(ctx context.Context) { } 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.") return nil } diff --git a/internal/services/observability/adapter.go b/internal/services/observability/adapter.go index bd186a7f..7f5d69df 100644 --- a/internal/services/observability/adapter.go +++ b/internal/services/observability/adapter.go @@ -19,6 +19,7 @@ package observability import ( "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/proton-bridge/v3/internal/services/observability/gluonmetrics" ) type Adapter struct { @@ -88,6 +89,15 @@ func (adapter *Adapter) AddDistinctMetrics(errType interface{}, metrics ...map[s } 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) +} diff --git a/internal/services/observability/distinction_error_types.go b/internal/services/observability/distinction_error_types.go index 64d761a3..5f419685 100644 --- a/internal/services/observability/distinction_error_types.go +++ b/internal/services/observability/distinction_error_types.go @@ -19,21 +19,22 @@ package observability import "time" -// DistinctionErrorTypeEnum - maps to the specific error schema for which we -// want to send a user update. -type DistinctionErrorTypeEnum int +// DistinctionMetricTypeEnum - used to distinct specific metrics which we want to limit over some interval. +// Most enums are tied to a specific error schema for which we also send a specific distinction user update. +type DistinctionMetricTypeEnum int const ( - SyncError DistinctionErrorTypeEnum = iota + SyncError DistinctionMetricTypeEnum = iota GluonImapError GluonMessageError GluonOtherError SMTPError EventLoopError // EventLoopError - should always be kept last when inserting new keys. + NewIMAPConnectionsExceedThreshold ) -// errorSchemaMap - maps between the DistinctionErrorTypeEnum and the relevant schema name. -var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglobals +// errorSchemaMap - maps between some DistinctionMetricTypeEnum and the relevant schema name. +var errorSchemaMap = map[DistinctionMetricTypeEnum]string{ //nolint:gochecknoglobals SyncError: "bridge_sync_errors_users_total", EventLoopError: "bridge_event_loop_events_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. -func createLastSentMap() map[DistinctionErrorTypeEnum]time.Time { +func createLastSentMap() map[DistinctionMetricTypeEnum]time.Time { registerTime := time.Now().Add(-updateInterval) - lastSentMap := make(map[DistinctionErrorTypeEnum]time.Time) + lastSentMap := make(map[DistinctionMetricTypeEnum]time.Time) for errType := SyncError; errType <= EventLoopError; errType++ { lastSentMap[errType] = registerTime diff --git a/internal/services/observability/distinction_utility.go b/internal/services/observability/distinction_utility.go index ddafe87e..cb9aa732 100644 --- a/internal/services/observability/distinction_utility.go +++ b/internal/services/observability/distinction_utility.go @@ -40,7 +40,7 @@ type distinctionUtility struct { 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 settingsGetter settingsGetter @@ -87,7 +87,7 @@ func (d *distinctionUtility) setSettingsGetter(getter settingsGetter) { // checkAndUpdateLastSentMap - checks whether we have sent a relevant user update metric // within the last 5 minutes. -func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionErrorTypeEnum) bool { +func (d *distinctionUtility) checkAndUpdateLastSentMap(key DistinctionMetricTypeEnum) bool { curTime := time.Now() val, ok := d.lastSentMap[key] 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 // versions of the metric if we ever decide to change them. func (d *distinctionUtility) generateUserMetric( - metricType DistinctionErrorTypeEnum, + metricType DistinctionMetricTypeEnum, ) proton.ObservabilityMetric { schemaName, ok := errorSchemaMap[metricType] 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) if d.checkAndUpdateLastSentMap(errType) { diff --git a/internal/services/observability/gluonmetrics/metrics.go b/internal/services/observability/gluonmetrics/metrics.go new file mode 100644 index 00000000..c1fc7ccc --- /dev/null +++ b/internal/services/observability/gluonmetrics/metrics.go @@ -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 . + +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, + }, + }, + } +} diff --git a/internal/services/observability/heartbeat.go b/internal/services/observability/heartbeat.go index cfb97a3d..8fe57383 100644 --- a/internal/services/observability/heartbeat.go +++ b/internal/services/observability/heartbeat.go @@ -42,7 +42,7 @@ func (d *distinctionUtility) resetHeartbeatData() { d.heartbeatData.receivedGluonError = false } -func (d *distinctionUtility) updateHeartbeatData(errType DistinctionErrorTypeEnum) { +func (d *distinctionUtility) updateHeartbeatData(errType DistinctionMetricTypeEnum) { d.withUpdateHeartbeatDataLock(func() { //nolint:exhaustive switch errType { diff --git a/internal/services/observability/service.go b/internal/services/observability/service.go index 59e0f1f8..b56cdb62 100644 --- a/internal/services/observability/service.go +++ b/internal/services/observability/service.go @@ -45,7 +45,9 @@ type client struct { // so we can easily pass them down to relevant components. type Sender interface { 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 { @@ -325,11 +327,25 @@ func (s *Service) AddMetrics(metrics ...proton.ObservabilityMetric) { // what number of events come from what number of users. // 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. -func (s *Service) AddDistinctMetrics(errType DistinctionErrorTypeEnum, metrics ...proton.ObservabilityMetric) { +func (s *Service) AddDistinctMetrics(errType DistinctionMetricTypeEnum, metrics ...proton.ObservabilityMetric) { metrics = s.distinctionUtility.generateDistinctMetrics(errType, 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. func (s *Service) ModifyHeartbeatInterval(duration time.Duration) { s.distinctionUtility.heartbeatTicker.Reset(duration) diff --git a/internal/services/observability/utils.go b/internal/services/observability/utils.go index 8ee8fb72..431e5b84 100644 --- a/internal/services/observability/utils.go +++ b/internal/services/observability/utils.go @@ -66,3 +66,30 @@ func getEnabled(value bool) string { } 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+" + } +} diff --git a/internal/services/smtp/observabilitymetrics/metrics.go b/internal/services/smtp/observabilitymetrics/metrics.go index 16424889..2df13636 100644 --- a/internal/services/smtp/observabilitymetrics/metrics.go +++ b/internal/services/smtp/observabilitymetrics/metrics.go @@ -21,6 +21,7 @@ import ( "time" "github.com/ProtonMail/go-proton-api" + "github.com/ProtonMail/proton-bridge/v3/internal/services/observability" ) const ( @@ -29,6 +30,9 @@ const ( smtpSendSuccessSchemaName = "bridge_smtp_send_success_total" smtpSendSuccessSchemaVersion = 1 + + smtpSubmissionRequestSchemaName = "bridge_smtp_send_request_total" + smtpSubmissionRequestSchemaVersion = 1 ) 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, + }, + }, + } +} diff --git a/internal/services/smtp/service.go b/internal/services/smtp/service.go index 51092250..2da4401b 100644 --- a/internal/services/smtp/service.go +++ b/internal/services/smtp/service.go @@ -32,13 +32,24 @@ import ( "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/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/useridentity" + "github.com/ProtonMail/proton-bridge/v3/internal/unleash" "github.com/ProtonMail/proton-bridge/v3/internal/usertypes" "github.com/ProtonMail/proton-bridge/v3/pkg/cpc" "github.com/sirupsen/logrus" ) +const ( + newlyOpenedIMAPConnectionsThreshold = 300 +) + +type imapSessionCountProvider interface { + GetOpenIMAPSessionCount() int + GetRollingIMAPConnectionCount() int +} + type Service struct { userID string panicHandler async.PanicHandler @@ -59,6 +70,9 @@ type Service struct { serverManager ServerManager observabilitySender observability.Sender + + imapSessionCountProvider imapSessionCountProvider + featureFlagValueProvider unleash.FeatureFlagValueProvider } func NewService( @@ -74,6 +88,8 @@ func NewService( identityState *useridentity.State, serverManager ServerManager, observabilitySender observability.Sender, + imapSessionCountProvider imapSessionCountProvider, + featureFlagValueProvider unleash.FeatureFlagValueProvider, ) *Service { subscriberName := fmt.Sprintf("smpt-%v", userID) @@ -99,7 +115,9 @@ func NewService( addressMode: mode, 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) { case *sendMailReq: - s.log.Debug("Received send mail request") err := s.sendMail(ctx, r) request.Reply(ctx, nil, err) @@ -252,15 +269,38 @@ type sendMailReq struct { func (s *Service) sendMail(ctx context.Context, req *sendMailReq) error { 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() defer func() { 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 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 diff --git a/internal/unleash/service.go b/internal/unleash/service.go index 691a3125..7c74ceda 100644 --- a/internal/unleash/service.go +++ b/internal/unleash/service.go @@ -37,15 +37,29 @@ var pollJitter = 2 * time.Minute //nolint:gochecknoglobals const filename = "unleash_flags" const ( - EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled" - IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled" - UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled" - UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled" + EventLoopNotificationDisabled = "InboxBridgeEventLoopNotificationDisabled" + IMAPAuthenticateCommandDisabled = "InboxBridgeImapAuthenticateCommandDisabled" + UserRemovalGluonDataCleanupDisabled = "InboxBridgeUserRemovalGluonDataCleanupDisabled" + UpdateUseNewVersionFileStructureDisabled = "InboxBridgeUpdateWithOsFilterDisabled" + SMTPSubmissionRequestSentryReportDisabled = "InboxBridgeSmtpSubmissionRequestSentryReportDisabled" ) -type requestFeaturesFn func(ctx context.Context) (proton.FeatureFlagResult, error) -type GetFlagValueFn func(key string) bool +type FeatureFlagValueProvider interface { + 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 { panicHandler async.PanicHandler timer *proton.Ticker diff --git a/internal/user/user.go b/internal/user/user.go index 938870cc..702abf1a 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -110,7 +110,7 @@ func New( syncConfigDir string, isNew bool, notificationStore *notifications.Store, - getFlagValFn unleash.GetFlagValueFn, + featureFlagValueProvider unleash.FeatureFlagValueProvider, ) (*User, error) { user, err := newImpl( ctx, @@ -130,7 +130,7 @@ func New( syncConfigDir, isNew, notificationStore, - getFlagValFn, + featureFlagValueProvider, ) if err != nil { // Cleanup any pending resources on error @@ -163,7 +163,7 @@ func newImpl( syncConfigDir string, isNew bool, notificationStore *notifications.Store, - getFlagValueFn unleash.GetFlagValueFn, + featureFlagValueProvider unleash.FeatureFlagValueProvider, ) (*User, error) { logrus.WithField("userID", apiUser.ID).Info("Creating new user") @@ -262,6 +262,8 @@ func newImpl( identityState.Clone(), smtpServerManager, observabilityService, + imapServerManager, + featureFlagValueProvider, ) user.imapService = imapservice.NewService( @@ -284,7 +286,7 @@ func newImpl( 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. // This will be used to authorize the user on the next run. diff --git a/internal/user/user_test.go b/internal/user/user_test.go index a5cf056d..dbdc7765 100644 --- a/internal/user/user_test.go +++ b/internal/user/user_test.go @@ -33,6 +33,7 @@ import ( "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/telemetry/mocks" + "github.com/ProtonMail/proton-bridge/v3/internal/unleash" "github.com/ProtonMail/proton-bridge/v3/internal/vault" "github.com/ProtonMail/proton-bridge/v3/tests" "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() nullIMAPServerManager := imapservice.NewNullIMAPServerManager() nullSMTPServerManager := smtp.NewNullServerManager() + nullUnleashService := unleash.NewNullUnleashService() user, err := New( ctx, @@ -171,9 +173,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma notifications.NewStore(func() (string, error) { return "", nil }), - func(_ string) bool { - return false - }, + nullUnleashService, ) require.NoError(tb, err) defer user.Close() diff --git a/tests/features/observability/all_metrics.feature b/tests/features/observability/all_metrics.feature index 19ed40b0..cdce3fea 100644 --- a/tests/features/observability/all_metrics.feature +++ b/tests/features/observability/all_metrics.feature @@ -45,5 +45,9 @@ Feature: Bridge send remote notification observability metrics And the user with username "[user:user1]" sends SMTP send success observability metric 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 diff --git a/tests/features/observability/gluon_metrics.feature b/tests/features/observability/gluon_metrics.feature index d10d063e..29ebdecd 100644 --- a/tests/features/observability/gluon_metrics.feature +++ b/tests/features/observability/gluon_metrics.feature @@ -9,3 +9,8 @@ Feature: Bridge send remote notification observability metrics 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 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 diff --git a/tests/observability_test.go b/tests/observability_test.go index 57aaa495..50ab5ad2 100644 --- a/tests/observability_test.go +++ b/tests/observability_test.go @@ -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/syncmsgevents" "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" "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics" ) @@ -188,3 +189,48 @@ func (s *scenario) SMTPSendSuccessObservabilityMetric(username string) error { 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 + }) +} diff --git a/tests/steps_test.go b/tests/steps_test.go index 41c2fbac..dfcbcec6 100644 --- a/tests/steps_test.go +++ b/tests/steps_test.go @@ -242,7 +242,11 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) { // SMTP metrics 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) + // SMTP submission metric + ctx.Step(`^the user with username "([^"]*)" sends an SMTP send request observability metric$`, s.SMTPSendRequestObservabilityMetric) // Gluon related metrics 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) }