Commit cebf2398 authored by Stan Hu's avatar Stan Hu

Improve logging in e-mail receiver processing

Previously there was little visibility in Service Desk and
reply-by-email processing. Now we add more JSON logging when mails are
successfully processed and when they fail.

To do this, we needed to refactor:

1. `Email::Receiver` and `Email::ServiceDeskReceiver` to make it
possible to extract the metadata from the mail.

2. `EmailReceiverWorker` so that`ServiceDeskEmailReceiverWorker` can
better reuse the code.

Relates to https://gitlab.com/gitlab-org/gitlab/-/issues/118594

Changelog: added
parent 61318cf1
...@@ -9,21 +9,80 @@ class EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker ...@@ -9,21 +9,80 @@ class EmailReceiverWorker # rubocop:disable Scalability/IdempotentWorker
urgency :high urgency :high
weight 2 weight 2
attr_accessor :raw
def perform(raw) def perform(raw)
return unless Gitlab::IncomingEmail.enabled? return unless should_perform?
begin @raw = raw
Gitlab::Email::Receiver.new(raw).execute execute_receiver
rescue StandardError => e
handle_failure(raw, e)
end end
def should_perform?
Gitlab::IncomingEmail.enabled?
end end
private private
def handle_failure(raw, error) def execute_receiver
Gitlab::AppLogger.warn("Email can not be processed: #{error}\n\n#{raw}") receiver.execute
log_success
rescue StandardError => e
log_error(e)
handle_failure(e)
end
def receiver
@receiver ||= Gitlab::Email::Receiver.new(raw)
end
def logger
Sidekiq.logger
end
def log_success
logger.info(build_message('Successfully processed message', receiver.mail_metadata))
end
def log_error(error)
payload =
case error
# Unparsable e-mails don't have metadata we can use
when Gitlab::Email::EmailUnparsableError, Gitlab::Email::EmptyEmailError
{}
else
mail_metadata
end
# We don't need the backtrace and more details if the e-mail couldn't be processed
if error.is_a?(Gitlab::Email::ProcessingError)
payload['exception.class'] = error.class.name
else
Gitlab::ExceptionLogFormatter.format!(error, payload)
Gitlab::ErrorTracking.track_exception(error)
end
logger.error(build_message('Error processing message', payload))
end
def build_message(message, params = {})
{
class: self.class.name,
Labkit::Correlation::CorrelationId::LOG_KEY => Labkit::Correlation::CorrelationId.current_id,
message: message
}.merge(params)
end
def mail_metadata
receiver.mail_metadata
rescue StandardError => e
# We should never get here as long as we check EmailUnparsableError, but
# let's be defensive in case we did something wrong.
Gitlab::ErrorTracking.track_exception(e)
{}
end
def handle_failure(error)
return unless raw.present? return unless raw.present?
can_retry = false can_retry = false
......
...@@ -5,13 +5,11 @@ class ServiceDeskEmailReceiverWorker < EmailReceiverWorker # rubocop:disable Sca ...@@ -5,13 +5,11 @@ class ServiceDeskEmailReceiverWorker < EmailReceiverWorker # rubocop:disable Sca
sidekiq_options retry: 3 sidekiq_options retry: 3
def perform(raw) def should_perform?
return unless ::Gitlab::ServiceDeskEmail.enabled? ::Gitlab::ServiceDeskEmail.enabled?
begin
Gitlab::Email::ServiceDeskReceiver.new(raw).execute
rescue StandardError => e
handle_failure(raw, e)
end end
def receiver
@receiver ||= Gitlab::Email::ServiceDeskReceiver.new(raw)
end end
end end
---
title: Improve logging in e-mail receiver processing
merge_request: 60692
author:
type: added
...@@ -6,6 +6,8 @@ require_dependency 'gitlab/email/handler' ...@@ -6,6 +6,8 @@ require_dependency 'gitlab/email/handler'
module Gitlab module Gitlab
module Email module Email
class Receiver class Receiver
include Gitlab::Utils::StrongMemoize
def initialize(raw) def initialize(raw)
@raw = raw @raw = raw
end end
...@@ -13,11 +15,7 @@ module Gitlab ...@@ -13,11 +15,7 @@ module Gitlab
def execute def execute
raise EmptyEmailError if @raw.blank? raise EmptyEmailError if @raw.blank?
mail = build_mail ignore_auto_reply!
ignore_auto_reply!(mail)
handler = find_handler(mail)
raise UnknownIncomingEmail unless handler raise UnknownIncomingEmail unless handler
...@@ -26,13 +24,33 @@ module Gitlab ...@@ -26,13 +24,33 @@ module Gitlab
end end
end end
def mail_metadata
{
message_id: mail.message_id,
from_address: mail.from,
to_address: mail.to,
mail_key: mail_key,
references: Array(mail.references),
delivered_to: delivered_to.map(&:value),
envelope_to: envelope_to.map(&:value),
x_envelope_to: x_envelope_to.map(&:value)
}
end
private private
def find_handler(mail) def handler
mail_key = extract_mail_key(mail) strong_memoize(:handler) { find_handler }
end
def find_handler
Handler.for(mail, mail_key) Handler.for(mail, mail_key)
end end
def mail
strong_memoize(:mail) { build_mail }
end
def build_mail def build_mail
Mail::Message.new(@raw) Mail::Message.new(@raw)
rescue Encoding::UndefinedConversionError, rescue Encoding::UndefinedConversionError,
...@@ -40,22 +58,24 @@ module Gitlab ...@@ -40,22 +58,24 @@ module Gitlab
raise EmailUnparsableError, e raise EmailUnparsableError, e
end end
def extract_mail_key(mail) def mail_key
key_from_to_header(mail) || key_from_additional_headers(mail) strong_memoize(:mail_key) do
key_from_to_header || key_from_additional_headers
end
end end
def key_from_to_header(mail) def key_from_to_header
mail.to.find do |address| mail.to.find do |address|
key = Gitlab::IncomingEmail.key_from_address(address) key = Gitlab::IncomingEmail.key_from_address(address)
break key if key break key if key
end end
end end
def key_from_additional_headers(mail) def key_from_additional_headers
find_key_from_references(mail) || find_key_from_references ||
find_key_from_delivered_to_header(mail) || find_key_from_delivered_to_header ||
find_key_from_envelope_to_header(mail) || find_key_from_envelope_to_header ||
find_key_from_x_envelope_to_header(mail) find_key_from_x_envelope_to_header
end end
def ensure_references_array(references) def ensure_references_array(references)
...@@ -71,41 +91,53 @@ module Gitlab ...@@ -71,41 +91,53 @@ module Gitlab
end end
end end
def find_key_from_references(mail) def find_key_from_references
ensure_references_array(mail.references).find do |mail_id| ensure_references_array(mail.references).find do |mail_id|
key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id) key = Gitlab::IncomingEmail.key_from_fallback_message_id(mail_id)
break key if key break key if key
end end
end end
def find_key_from_delivered_to_header(mail) def delivered_to
Array(mail[:delivered_to]).find do |header| Array(mail[:delivered_to])
end
def envelope_to
Array(mail[:envelope_to])
end
def x_envelope_to
Array(mail[:x_envelope_to])
end
def find_key_from_delivered_to_header
delivered_to.find do |header|
key = Gitlab::IncomingEmail.key_from_address(header.value) key = Gitlab::IncomingEmail.key_from_address(header.value)
break key if key break key if key
end end
end end
def find_key_from_envelope_to_header(mail) def find_key_from_envelope_to_header
Array(mail[:envelope_to]).find do |header| envelope_to.find do |header|
key = Gitlab::IncomingEmail.key_from_address(header.value) key = Gitlab::IncomingEmail.key_from_address(header.value)
break key if key break key if key
end end
end end
def find_key_from_x_envelope_to_header(mail) def find_key_from_x_envelope_to_header
Array(mail[:x_envelope_to]).find do |header| x_envelope_to.find do |header|
key = Gitlab::IncomingEmail.key_from_address(header.value) key = Gitlab::IncomingEmail.key_from_address(header.value)
break key if key break key if key
end end
end end
def ignore_auto_reply!(mail) def ignore_auto_reply!
if auto_submitted?(mail) || auto_replied?(mail) if auto_submitted? || auto_replied?
raise AutoGeneratedEmailError raise AutoGeneratedEmailError
end end
end end
def auto_submitted?(mail) def auto_submitted?
# Mail::Header#[] is case-insensitive # Mail::Header#[] is case-insensitive
auto_submitted = mail.header['Auto-Submitted']&.value auto_submitted = mail.header['Auto-Submitted']&.value
...@@ -114,7 +146,7 @@ module Gitlab ...@@ -114,7 +146,7 @@ module Gitlab
auto_submitted && auto_submitted != 'no' auto_submitted && auto_submitted != 'no'
end end
def auto_replied?(mail) def auto_replied?
autoreply = mail.header['X-Autoreply']&.value autoreply = mail.header['X-Autoreply']&.value
autoreply && autoreply == 'yes' autoreply && autoreply == 'yes'
......
...@@ -5,14 +5,19 @@ module Gitlab ...@@ -5,14 +5,19 @@ module Gitlab
class ServiceDeskReceiver < Receiver class ServiceDeskReceiver < Receiver
private private
def find_handler(mail) def find_handler
key = service_desk_key(mail) return unless service_desk_key
return unless key
Gitlab::Email::Handler::ServiceDeskHandler.new(mail, nil, service_desk_key: key) Gitlab::Email::Handler::ServiceDeskHandler.new(mail, nil, service_desk_key: service_desk_key)
end end
def service_desk_key(mail) def service_desk_key
strong_memoize(:service_desk_key) do
find_service_desk_key
end
end
def find_service_desk_key
mail.to.find do |address| mail.to.find do |address|
key = ::Gitlab::ServiceDeskEmail.key_from_address(address) key = ::Gitlab::ServiceDeskEmail.key_from_address(address)
break key if key break key if key
......
...@@ -15,6 +15,7 @@ RSpec.describe Gitlab::Email::Receiver do ...@@ -15,6 +15,7 @@ RSpec.describe Gitlab::Email::Receiver do
context 'when the email contains a valid email address in a header' do context 'when the email contains a valid email address in a header' do
let(:handler) { double(:handler) } let(:handler) { double(:handler) }
let(:metadata) { receiver.mail_metadata }
before do before do
allow(handler).to receive(:execute) allow(handler).to receive(:execute)
...@@ -22,24 +23,38 @@ RSpec.describe Gitlab::Email::Receiver do ...@@ -22,24 +23,38 @@ RSpec.describe Gitlab::Email::Receiver do
allow(handler).to receive(:metrics_event) allow(handler).to receive(:metrics_event)
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.example.com") stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.example.com")
expect(receiver.mail_metadata.keys).to match_array(%i(message_id from_address to_address mail_key references delivered_to envelope_to x_envelope_to))
end end
context 'when in a Delivered-To header' do context 'when in a Delivered-To header' do
let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') } let(:email_raw) { fixture_file('emails/forwarded_new_issue.eml') }
it_behaves_like 'correctly finds the mail key' it_behaves_like 'correctly finds the mail key'
it 'parses the metadata' do
expect(metadata[:delivered_to]). to eq(["incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com", "support@example.com"])
end
end end
context 'when in an Envelope-To header' do context 'when in an Envelope-To header' do
let(:email_raw) { fixture_file('emails/envelope_to_header.eml') } let(:email_raw) { fixture_file('emails/envelope_to_header.eml') }
it_behaves_like 'correctly finds the mail key' it_behaves_like 'correctly finds the mail key'
it 'parses the metadata' do
expect(metadata[:envelope_to]). to eq(["incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com"])
end
end end
context 'when in an X-Envelope-To header' do context 'when in an X-Envelope-To header' do
let(:email_raw) { fixture_file('emails/x_envelope_to_header.eml') } let(:email_raw) { fixture_file('emails/x_envelope_to_header.eml') }
it_behaves_like 'correctly finds the mail key' it_behaves_like 'correctly finds the mail key'
it 'parses the metadata' do
expect(metadata[:x_envelope_to]). to eq(["incoming+gitlabhq/gitlabhq+auth_token@appmail.example.com"])
end
end end
context 'when enclosed with angle brackets in an Envelope-To header' do context 'when enclosed with angle brackets in an Envelope-To header' do
......
...@@ -13,6 +13,7 @@ RSpec.describe EmailReceiverWorker, :mailer do ...@@ -13,6 +13,7 @@ RSpec.describe EmailReceiverWorker, :mailer do
it "calls the email receiver" do it "calls the email receiver" do
expect(Gitlab::Email::Receiver).to receive(:new).with(raw_message).and_call_original expect(Gitlab::Email::Receiver).to receive(:new).with(raw_message).and_call_original
expect_any_instance_of(Gitlab::Email::Receiver).to receive(:execute) expect_any_instance_of(Gitlab::Email::Receiver).to receive(:execute)
expect(Sidekiq.logger).to receive(:info).with(hash_including(message: "Successfully processed message")).and_call_original
described_class.new.perform(raw_message) described_class.new.perform(raw_message)
end end
...@@ -20,10 +21,11 @@ RSpec.describe EmailReceiverWorker, :mailer do ...@@ -20,10 +21,11 @@ RSpec.describe EmailReceiverWorker, :mailer do
context "when an error occurs" do context "when an error occurs" do
before do before do
allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(error) allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(error)
expect(Sidekiq.logger).to receive(:error).with(hash_including('exception.class' => error.class.name)).and_call_original
end end
context 'when the error is Gitlab::Email::EmptyEmailError' do context 'when the error is Gitlab::Email::EmptyEmailError' do
let(:error) { Gitlab::Email::EmptyEmailError } let(:error) { Gitlab::Email::EmptyEmailError.new }
it 'sends out a rejection email' do it 'sends out a rejection email' do
perform_enqueued_jobs do perform_enqueued_jobs do
...@@ -38,7 +40,7 @@ RSpec.describe EmailReceiverWorker, :mailer do ...@@ -38,7 +40,7 @@ RSpec.describe EmailReceiverWorker, :mailer do
end end
context 'when the error is Gitlab::Email::AutoGeneratedEmailError' do context 'when the error is Gitlab::Email::AutoGeneratedEmailError' do
let(:error) { Gitlab::Email::AutoGeneratedEmailError } let(:error) { Gitlab::Email::AutoGeneratedEmailError.new }
it 'does not send out any rejection email' do it 'does not send out any rejection email' do
perform_enqueued_jobs do perform_enqueued_jobs do
...@@ -63,6 +65,21 @@ RSpec.describe EmailReceiverWorker, :mailer do ...@@ -63,6 +65,21 @@ RSpec.describe EmailReceiverWorker, :mailer do
expect(email.body.parts.last.to_s).to include("Could not deal with that") expect(email.body.parts.last.to_s).to include("Could not deal with that")
end end
end end
context 'when the error is ActiveRecord::StatementTimeout' do
let(:error) { ActiveRecord::StatementTimeout.new("Statement timeout") }
it 'does not report the error to the sender' do
expect(Gitlab::ErrorTracking).to receive(:track_exception).with(error).and_call_original
perform_enqueued_jobs do
described_class.new.perform(raw_message)
end
email = ActionMailer::Base.deliveries.last
expect(email).to be_nil
end
end
end end
end end
......
...@@ -9,11 +9,12 @@ RSpec.describe ServiceDeskEmailReceiverWorker, :mailer do ...@@ -9,11 +9,12 @@ RSpec.describe ServiceDeskEmailReceiverWorker, :mailer do
context 'when service_desk_email config is enabled' do context 'when service_desk_email config is enabled' do
before do before do
stub_service_desk_email_setting(enabled: true, address: 'foo') stub_service_desk_email_setting(enabled: true, address: 'support+%{key}@example.com')
end end
it 'does not ignore the email' do it 'does not ignore the email' do
expect(Gitlab::Email::ServiceDeskReceiver).to receive(:new) expect(Gitlab::Email::ServiceDeskReceiver).to receive(:new).and_call_original
expect(Sidekiq.logger).to receive(:error).with(hash_including('exception.class' => Gitlab::Email::ProjectNotFound.to_s)).and_call_original
worker.perform(email) worker.perform(email)
end end
...@@ -23,6 +24,7 @@ RSpec.describe ServiceDeskEmailReceiverWorker, :mailer do ...@@ -23,6 +24,7 @@ RSpec.describe ServiceDeskEmailReceiverWorker, :mailer do
allow_next_instance_of(Gitlab::Email::ServiceDeskReceiver) do |receiver| allow_next_instance_of(Gitlab::Email::ServiceDeskReceiver) do |receiver|
allow(receiver).to receive(:find_handler).and_return(nil) allow(receiver).to receive(:find_handler).and_return(nil)
end end
expect(Sidekiq.logger).to receive(:error).with(hash_including('exception.class' => Gitlab::Email::UnknownIncomingEmail.to_s)).and_call_original
end end
it 'sends a rejection email' do it 'sends a rejection email' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment