diff --git a/internal/services/observability/distinction_error_types.go b/internal/services/observability/distinction_error_types.go index b1a56e13..d33d562f 100644 --- a/internal/services/observability/distinction_error_types.go +++ b/internal/services/observability/distinction_error_types.go @@ -28,6 +28,7 @@ const ( GluonImapError GluonMessageError GluonOtherError + SMTPError EventLoopError // EventLoopError - should always be kept last when inserting new keys. ) @@ -37,6 +38,7 @@ var errorSchemaMap = map[DistinctionErrorTypeEnum]string{ //nolint:gochecknoglob EventLoopError: "bridge_event_loop_events_errors_users_total", GluonImapError: "bridge_gluon_imap_errors_users_total", GluonMessageError: "bridge_gluon_message_errors_users_total", + SMTPError: "bridge_smtp_errors_users_total", GluonOtherError: "bridge_gluon_other_errors_users_total", } diff --git a/internal/services/observability/heartbeat.go b/internal/services/observability/heartbeat.go index af26f7e6..78f119d2 100644 --- a/internal/services/observability/heartbeat.go +++ b/internal/services/observability/heartbeat.go @@ -44,6 +44,7 @@ func (d *distinctionUtility) resetHeartbeatData() { func (d *distinctionUtility) updateHeartbeatData(errType DistinctionErrorTypeEnum) { d.withUpdateHeartbeatDataLock(func() { + //nolint:exhaustive switch errType { case SyncError: d.heartbeatData.receivedSyncError = true diff --git a/internal/services/smtp/observabilitymetrics/metrics.go b/internal/services/smtp/observabilitymetrics/metrics.go new file mode 100644 index 00000000..1a4ccc5b --- /dev/null +++ b/internal/services/smtp/observabilitymetrics/metrics.go @@ -0,0 +1,90 @@ +// Copyright (c) 2024 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 observabilitymetrics + +import ( + "time" + + "github.com/ProtonMail/go-proton-api" +) + +const ( + smtpErrorsSchemaName = "bridge_smtp_errors_total" + smtpErrorsSchemaVersion = 1 + + smtpSendSuccessSchemaName = "bridge_smtp_send_success_total" + smtpSendSuccessSchemaVersion = 1 +) + +func generateSMTPErrorObservabilityMetric(errorType string) proton.ObservabilityMetric { + return proton.ObservabilityMetric{ + Name: smtpErrorsSchemaName, + Version: smtpErrorsSchemaVersion, + Timestamp: time.Now().Unix(), + Data: map[string]interface{}{ + "Value": 1, + "Labels": map[string]string{ + "errorType": errorType, + }, + }, + } +} + +func GenerateFailedGetParentID() proton.ObservabilityMetric { + return generateSMTPErrorObservabilityMetric("failedGetParentId") +} + +func GenerateUnsupportedMIMEType() proton.ObservabilityMetric { + return generateSMTPErrorObservabilityMetric("unsupportedMIMEType") +} + +func GenerateFailedCreateDraft() proton.ObservabilityMetric { + return generateSMTPErrorObservabilityMetric("failedToCreateDraft") +} + +func GenerateFailedCreateAttachments() proton.ObservabilityMetric { + return generateSMTPErrorObservabilityMetric("failedCreateAttachments") +} + +func GenerateFailedToGetRecipients() proton.ObservabilityMetric { + return generateSMTPErrorObservabilityMetric("failedGetRecipients") +} + +func GenerateFailedCreatePackages() proton.ObservabilityMetric { + return generateSMTPErrorObservabilityMetric("failedCreatePackages") +} + +func GenerateFailedSendDraft() proton.ObservabilityMetric { + return generateSMTPErrorObservabilityMetric("failedSendDraft") +} + +func GenerateFailedDeleteFromDrafts() proton.ObservabilityMetric { + return generateSMTPErrorObservabilityMetric("failedDeleteFromDrafts") +} + +func GenerateSMTPSendSuccess() proton.ObservabilityMetric { + return proton.ObservabilityMetric{ + Name: smtpSendSuccessSchemaName, + Version: smtpSendSuccessSchemaVersion, + Timestamp: time.Now().Unix(), + Data: map[string]interface{}{ + "Value": 1, + "Labels": map[string]string{}, + }, + } +} diff --git a/internal/services/smtp/service.go b/internal/services/smtp/service.go index e52406b4..e2f10649 100644 --- a/internal/services/smtp/service.go +++ b/internal/services/smtp/service.go @@ -29,6 +29,7 @@ import ( "github.com/ProtonMail/gluon/reporter" "github.com/ProtonMail/go-proton-api" bridgelogging "github.com/ProtonMail/proton-bridge/v3/internal/logging" + "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/userevents" @@ -63,6 +64,8 @@ type Service struct { addressMode usertypes.AddressMode serverManager ServerManager + + observabilitySender observability.Sender } func NewService( @@ -78,6 +81,7 @@ func NewService( mode usertypes.AddressMode, identityState *useridentity.State, serverManager ServerManager, + observabilitySender observability.Sender, ) *Service { subscriberName := fmt.Sprintf("smpt-%v", userID) @@ -103,6 +107,8 @@ func NewService( addressMode: mode, serverManager: serverManager, + + observabilitySender: observabilitySender, } } diff --git a/internal/services/smtp/smtp.go b/internal/services/smtp/smtp.go index 9cfa205c..99af3e65 100644 --- a/internal/services/smtp/smtp.go +++ b/internal/services/smtp/smtp.go @@ -35,7 +35,9 @@ import ( "github.com/ProtonMail/go-proton-api" "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/v3/internal/logging" + "github.com/ProtonMail/proton-bridge/v3/internal/services/observability" "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/usertypes" "github.com/ProtonMail/proton-bridge/v3/pkg/message" "github.com/ProtonMail/proton-bridge/v3/pkg/message/parser" @@ -166,6 +168,10 @@ func (s *Service) smtpSendMail(ctx context.Context, authID string, from string, // If the message was successfully sent, we can update the message ID in the record. s.log.Debug("Message sent successfully, signaling recorder") + + // Send SMTP success observability metric + s.observabilitySender.AddMetrics(observabilitymetrics.GenerateSMTPSendSuccess()) + s.recorder.SignalMessageSent(hash, srID, sent.ID) return nil @@ -196,7 +202,7 @@ func (s *Service) sendWithKey( } parentID, draftsToDelete, err := getParentID(ctx, s.client, authAddrID, addrMode, references) if err != nil { - // Sentry event has been removed; should be replaced with observability - BRIDGE-206. + s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedGetParentID()) s.log.WithError(err).Warn("Failed to get parent ID") } @@ -211,6 +217,7 @@ func (s *Service) sendWithKey( decBody = string(message.PlainBody) default: + s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateUnsupportedMIMEType()) return proton.Message{}, fmt.Errorf("unsupported MIME type: %v", message.MIMEType) } @@ -227,32 +234,38 @@ func (s *Service) sendWithKey( ExternalID: message.ExternalID, }) if err != nil { + s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedCreateDraft()) return proton.Message{}, fmt.Errorf("failed to create draft: %w", err) } attKeys, err := s.createAttachments(ctx, s.client, addrKR, draft.ID, message.Attachments) if err != nil { + s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedCreateAttachments()) return proton.Message{}, fmt.Errorf("failed to create attachments: %w", err) } recipients, err := s.getRecipients(ctx, s.client, userKR, settings, draft) if err != nil { + s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedToGetRecipients()) return proton.Message{}, fmt.Errorf("failed to get recipients: %w", err) } req, err := createSendReq(addrKR, message.MIMEBody, message.RichBody, message.PlainBody, recipients, attKeys) if err != nil { + s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedCreatePackages()) return proton.Message{}, fmt.Errorf("failed to create packages: %w", err) } res, err := s.client.SendDraft(ctx, draft.ID, req) if err != nil { + s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedSendDraft()) return proton.Message{}, fmt.Errorf("failed to send draft: %w", err) } // Only delete the drafts, if any, after message was successfully sent. if len(draftsToDelete) != 0 { if err := s.client.DeleteMessage(ctx, draftsToDelete...); err != nil { + s.observabilitySender.AddDistinctMetrics(observability.SMTPError, observabilitymetrics.GenerateFailedDeleteFromDrafts()) s.log.WithField("ids", draftsToDelete).WithError(err).Errorf("Failed to delete requested messages from Drafts") } } diff --git a/internal/user/user.go b/internal/user/user.go index 9431e281..6e1ce73f 100644 --- a/internal/user/user.go +++ b/internal/user/user.go @@ -265,6 +265,7 @@ func newImpl( addressMode, identityState.Clone(), smtpServerManager, + observabilityService, ) user.imapService = imapservice.NewService( diff --git a/tests/features/observability/all_metrics.feature b/tests/features/observability/all_metrics.feature index acb5ec7b..19ed40b0 100644 --- a/tests/features/observability/all_metrics.feature +++ b/tests/features/observability/all_metrics.feature @@ -35,4 +35,15 @@ Feature: Bridge send remote notification observability metrics And the user with username "[user:user1]" sends all possible sync message building success observability metrics Then it succeeds + Scenario: Test all possible SMTP error observability metrics + When the user logs in with username "[user:user1]" and password "password" + And the user with username "[user:user1]" sends all possible SMTP error observability metrics + Then it succeeds + + Scenario: Test SMTP send success observability metrics + When the user logs in with username "[user:user1]" and password "password" + And the user with username "[user:user1]" sends SMTP send success observability metric + Then it succeeds + + diff --git a/tests/observability_test.go b/tests/observability_test.go index 4de56ec8..2d7a3ca0 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" + smtpMetrics "github.com/ProtonMail/proton-bridge/v3/internal/services/smtp/observabilitymetrics" "github.com/ProtonMail/proton-bridge/v3/internal/services/syncservice/observabilitymetrics" ) @@ -57,6 +58,7 @@ func (s *scenario) userHeartbeatPermutationsObservability(username string) error // - bridge_gluon_message_errors_users_total_v1.schema.json // - bridge_gluon_other_errors_users_total_v1.schema.json // - bridge_event_loop_events_errors_users_total_v1.schema.json. +// - bridge_smtp_errors_users_total_v1.schema.json func (s *scenario) userDistinctionMetricsPermutationsObservability(username string) error { batch := proton.ObservabilityBatch{ Metrics: observability.GenerateAllUsedDistinctionMetricPermutations()} @@ -148,3 +150,37 @@ func (s *scenario) testGluonErrorObservabilityMetrics(username string) error { return err }) } + +// SMTPErrorObservabilityMetrics corresponds to bridge_smtp_errors_total_v1.schema.json. +func (s *scenario) SMTPErrorObservabilityMetrics(username string) error { + batch := proton.ObservabilityBatch{ + Metrics: []proton.ObservabilityMetric{ + smtpMetrics.GenerateFailedGetParentID(), + smtpMetrics.GenerateUnsupportedMIMEType(), + smtpMetrics.GenerateFailedCreateDraft(), + smtpMetrics.GenerateFailedCreateAttachments(), + smtpMetrics.GenerateFailedCreatePackages(), + smtpMetrics.GenerateFailedToGetRecipients(), + smtpMetrics.GenerateFailedSendDraft(), + smtpMetrics.GenerateFailedDeleteFromDrafts(), + }, + } + + 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) SMTPSendSuccessObservabilityMetric(username string) error { + batch := proton.ObservabilityBatch{ + Metrics: []proton.ObservabilityMetric{ + smtpMetrics.GenerateSMTPSendSuccess(), + }, + } + + 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 49ca6090..58bf1358 100644 --- a/tests/steps_test.go +++ b/tests/steps_test.go @@ -234,6 +234,10 @@ func (s *scenario) steps(ctx *godog.ScenarioContext) { ctx.Step(`^the user with username "([^"]*)" sends all possible event loop message events observability metrics$`, s.eventLoopFailureMessageEventsObservability) ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building failure observability metrics$`, s.syncFailureMessageBuiltObservability) ctx.Step(`^the user with username "([^"]*)" sends all possible sync message building success observability metrics$`, s.syncSuccessMessageBuiltObservability) + // 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) + // Gluon related metrics ctx.Step(`^the user with username "([^"]*)" sends all possible gluon error observability metrics$`, s.testGluonErrorObservabilityMetrics) }