Skip to content

Instantly share code, notes, and snippets.

@manish-in-java
Created July 1, 2021 11:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save manish-in-java/e589dd5f867b8dd90cfc9187b4fbcd9c to your computer and use it in GitHub Desktop.
Save manish-in-java/e589dd5f867b8dd90cfc9187b4fbcd9c to your computer and use it in GitHub Desktop.
A Logback appender that can send log messages over email, using Amazon Web Services Simple Email Service (AWS SES).
/*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in the
* Software without restriction, including without limitation the rights to use, copy,
* modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
* and to permit persons to whom the Software is furnished to do so, subject to the
* following conditions:
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
* PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
* CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
* OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
package com.example.logging;
import ch.qos.logback.classic.ClassicConstants;
import ch.qos.logback.classic.PatternLayout;
import ch.qos.logback.classic.boolex.OnErrorEvaluator;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Layout;
import ch.qos.logback.core.boolex.EventEvaluator;
import ch.qos.logback.core.helpers.CyclicBuffer;
import ch.qos.logback.core.net.SMTPAppenderBase;
import ch.qos.logback.core.pattern.PatternLayoutBase;
import ch.qos.logback.core.spi.CyclicBufferTracker;
import com.amazonaws.regions.Regions;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailService;
import com.amazonaws.services.simpleemail.AmazonSimpleEmailServiceClientBuilder;
import com.amazonaws.services.simpleemail.model.*;
import edu.emory.mathcs.backport.java.util.Collections;
import java.util.Collection;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* <p>
* Provides support for sending log events to one or more email addresses
* using the Amazon Simple Email Service (SES). Functionally, this is identical
* to the classic Logback {@code SMTPAppender}, with the only difference that
* whereas the later uses core SMTP for sending log events, this appender
* uses the Amazon SES SDK for integrating with the SES API directly.
* </p>
*
* <p>
* This appender does not support the SMTP protocol - if Amazon SES needs
* to be invoked using SMTP credentials, use the standard Logback
* {@code SMTPAppender} instead. Use this appender if and only if SES can be
* invoked with the AWS SES SDK using IAM credentials.
* </p>
*
* <p>
* AWS IAM credentials must be supplied to the appender either through the IAM
* instance profile with which the application is running, or by configuring the
* credentials locally on the server. There is no way to configure the IAM
* credentials on the appender itself, as doing so poses the risk of exposing
* the credentials publicly.
* </p>
*
* <p>
* Examples of configuring this appender are given below.
* </p>
*
* <h3>Example 1 - Basic <small>(logs errors only)</small></h3>
* <pre>{@code
* <appender class="com.example.logging.AmazonSESAppender" name="sesAppender">
* <region>us-east-1</region>
* <from>monitoring@example.com</from>
* <to>monitoring@example.com</to>
* </appender>
* }</pre>
*
* <h3>Example 2 - Custom subject <small>(logs errors only)</small></h3>
* <pre>{@code
* <appender class="com.example.logging.AmazonSESAppender" name="sesAppender">
* <region>us-east-1</region>
* <from>monitoring@example.com</from>
* <to>monitoring@example.com</to>
* <subject>Application Error Report</subject>
* </appender>
* }</pre>
*
* <h3>Example 3 - Multiple recipients <small>(logs errors only)</small></h3>
* <pre>{@code
* <appender class="com.example.logging.AmazonSESAppender" name="sesAppender">
* <region>us-east-1</region>
* <from>monitoring@example.com</from>
* <to>monitoring@example.com</to>
* <to>support@example.com</to>
* <subject>Application Error Report</subject>
* </appender>
* }</pre>
*
* <h3>Example 4 - Synchronous (real-time) reporting <small>(logs errors only)</small></h3>
* <pre>{@code
* <appender class="com.example.logging.AmazonSESAppender" name="sesAppender">
* <region>us-east-1</region>
* <from>monitoring@example.com</from>
* <to>monitoring@example.com</to>
* <to>support@example.com</to>
* <subject>Application Error Report</subject>
* <asynchronousSending>false</asynchronousSending>
* </appender>
*
* <h3>Example 5 - Report 20 events at a time <small>(logs errors only)</small></h3>
* <pre>{@code
* <appender class="com.example.logging.AmazonSESAppender" name="sesAppender">
* <region>us-east-1</region>
* <from>monitoring@example.com</from>
* <to>monitoring@example.com</to>
* <to>support@example.com</to>
* <subject>Application Error Report</subject>
* <asynchronousSending>false</asynchronousSending>
* <cyclicBufferTracker class="ch.qos.logback.core.spi.CyclicBufferTracker">
* <bufferSize>20</bufferSize>
* </cyclicBufferTracker>
* </appender>
*
* <h3>Example 6 - Custom log level</h3>
* <pre>{@code
* <appender class="com.example.logging.AmazonSESAppender" name="sesAppender">
* <region>us-east-1</region>
* <from>monitoring@example.com</from>
* <to>monitoring@example.com</to>
* <to>support@example.com</to>
* <subject>Application Error Report</subject>
* <asynchronousSending>false</asynchronousSending>
* <cyclicBufferTracker class="ch.qos.logback.core.spi.CyclicBufferTracker">
* <bufferSize>20</bufferSize>
* </cyclicBufferTracker>
* <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
* <level>INFO</level>
* </filter>
* </appender>
* }</pre>
*/
public class AmazonSESAppender extends SMTPAppenderBase<ILoggingEvent>
{
private static final String DEFAULT_SUBJECT_PATTERN = "%logger{20} - %m";
private boolean includeCallerData = false;
private Collection<String> recipients = Collections.emptySet();
private Regions region = Regions.US_EAST_1;
private AmazonSimpleEmailService sesClient;
/**
* Creates an appender with a default {@link EventEvaluator} that gets
* triggered only for log events with level {@code Error} or higher.
*/
public AmazonSESAppender()
{
this(null);
}
/**
* Creates an appender using an {@link EventEvaluator} that can be used to
* determine if a particular log event must be appended to this appender.
*
* @param eventEvaluator The {@link EventEvaluator} to use for appending
* log events to this appender. If {@code null}, a default evaluator that
* gets triggered for log events with level {@code Error} or higher, is
* set forcibly.
*/
public AmazonSESAppender(final EventEvaluator<ILoggingEvent> eventEvaluator)
{
super();
if (eventEvaluator != null)
{
this.eventEvaluator = eventEvaluator;
}
else
{
this.eventEvaluator = new OnErrorEvaluator();
this.eventEvaluator.setContext(getContext());
this.eventEvaluator.setName("onError");
this.eventEvaluator.start();
}
}
/**
* Gets whether logs should include information about the caller that
* generated the logs.
*
* @param includeCallerData Whether logs should include information about
* the caller that generated the logs.
*/
public void setIncludeCallerData(final boolean includeCallerData)
{
this.includeCallerData = includeCallerData;
}
/**
* Sets the AWS region from which this appender should send emails.
*
* @param region A unique AWS region code like {@code us-east-1}.
*/
public void setRegion(final String region)
{
if (region != null && !region.isBlank())
{
this.region = Regions.fromName(region);
}
}
/**
* Creates an AWS SES API client for sending emails.
*
* @throws IllegalStateException if {@code from} or {@code region} is
* {@code null}, or if there is no recipient.
*/
@Override
public void start()
{
// Ensure that the AWS region is specified.
if (region == null)
{
throw new IllegalStateException("Region must not be blank.");
}
// Ensure that the sender is specified.
if (getFrom() == null || getFrom().isBlank())
{
throw new IllegalStateException("From must not be blank.");
}
// Ensure that the recipient(s) is(are) specified.
if (getToList().isEmpty())
{
throw new IllegalStateException("To must not be blank.");
}
recipients = getToList().stream()
.map(PatternLayoutBase::getPattern)
.collect(Collectors.toList());
sesClient = AmazonSimpleEmailServiceClientBuilder.standard()
.withRegion(region)
.build();
if (cbTracker == null)
{
cbTracker = new CyclicBufferTracker<>();
}
subjectLayout = this.makeSubjectLayout(getSubject());
this.started = true;
}
/**
* Gets whether the caller has requested the internal buffer to be cleared
* as part of a log event.
*
* @param event A log event.
*/
@Override
protected boolean eventMarksEndOfLife(final ILoggingEvent event)
{
return event.getMarker() != null
&& event.getMarker().contains(ClassicConstants.FINALIZE_SESSION_MARKER);
}
/**
* Adds log events being tracked in a {@link CyclicBuffer} to a
* {@link StringBuffer} in preparation for including them in an email.
*
* @param buffer A {@link CyclicBuffer} maintained by this appender for
* tracking log events yet to be appended.
* @param output A {@link StringBuffer} to which log event information
* should be added.
*/
@Override
protected void fillBuffer(final CyclicBuffer<ILoggingEvent> buffer, final StringBuffer output)
{
int len = buffer.length();
for (int i = 0; i < len; i++)
{
output.append(layout.doLayout(buffer.get()));
}
}
/**
* Gets a layout for an email recipient.
*
* @param recipient Email address for a recipient to whom this appender
* should send emails.
*
* @return A layout appropriate for the recipient of emails to be sent from
* this appender.
*/
@Override
protected PatternLayout makeNewToPatternLayout(final String recipient)
{
final PatternLayout patternLayout = new PatternLayout();
patternLayout.setPattern(recipient);
return patternLayout;
}
/**
* Gets a layout for the email subject, as appropriate for this appender.
* If the {@code subject} set for this appender is {@code null} or blank,
* a default layout pattern is used.
*
* @return A layout appropriate for the subject of emails to be sent from
* this appender.
*
* @see #DEFAULT_SUBJECT_PATTERN
*/
@Override
protected Layout<ILoggingEvent> makeSubjectLayout(String subject)
{
if (subject == null || subject.isBlank())
{
subject = DEFAULT_SUBJECT_PATTERN;
}
final PatternLayout patternLayout = new PatternLayout();
patternLayout.setContext(getContext());
patternLayout.setPattern(subject);
patternLayout.setPostCompileProcessor(null);
patternLayout.start();
return patternLayout;
}
/**
* Collates content of all log events being tracked in an internal buffer
* as an email message and sends an email using SES.
*
* @param buffer The internal buffer being used to track log events to
* append to the log destination.
* @param lastEvent The last log event added to the internal buffer.
*/
@Override
protected void sendBuffer(final CyclicBuffer<ILoggingEvent> buffer, final ILoggingEvent lastEvent)
{
try
{
// Prepare an email body using content the various log events
// waiting to be appended to the log destination.
final StringBuffer body = new StringBuffer();
// Add an email header, possibly containing an overview and/or
// introduction.
Optional.ofNullable(layout.getFileHeader())
.ifPresent(body::append);
// Add a presentation header, possibly containing a log summary.
Optional.ofNullable(layout.getPresentationHeader())
.ifPresent(body::append);
// Add log events.
fillBuffer(buffer, body);
// Add a presentation footer, possibly containing a log summary.
Optional.ofNullable(layout.getPresentationFooter())
.ifPresent(body::append);
// Add an email footer, possibly containing a conclusion.
Optional.ofNullable(layout.getFileFooter())
.ifPresent(body::append);
// Prepare a formatted subject.
String subject = subjectLayout.doLayout(lastEvent);
// The subject must not contain new-line characters, which cause
// an SMTP error (LOGBACK-865). Truncate the string at the first
// new-line character.
final int newLinePos = (subject != null) ? subject.indexOf('\n') : -1;
if (newLinePos > -1)
{
subject = subject.substring(0, newLinePos);
}
final SendEmailRequest request = new SendEmailRequest()
.withDestination(new Destination().withToAddresses(recipients))
.withMessage(new Message()
.withBody(new Body()
.withHtml(new Content()
.withCharset(getCharsetEncoding())
.withData(body.toString())))
.withSubject(new Content()
.withCharset(getCharsetEncoding())
.withData(subject)))
.withSource(getFrom());
sesClient.sendEmail(request);
}
catch (final Exception e)
{
addError("Error occurred while sending logs via email using Amazon SES.", e);
}
}
/**
* Add a log event to a cyclic buffer.
*
* @param buffer A {@link CyclicBuffer} maintained by this appender for
* tracking log events yet to be appended.
* @param event The log event to append.
*/
@Override
protected void subAppend(final CyclicBuffer<ILoggingEvent> buffer, final ILoggingEvent event)
{
if (includeCallerData)
{
event.getCallerData();
}
event.prepareForDeferredProcessing();
buffer.add(event);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment