Created
July 1, 2021 11:51
-
-
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).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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