/**
* Copyright (C) 2010-2013 Andrei Pozolotin <Andrei.Pozolotin@gmail.com>
*
* All rights reserved. Licensed under the OSI BSD License.
*
* http://www.opensource.org/licenses/bsd-license.php
*/
package com.carrotgarden.log4j.aws.sns;
import java.io.File;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.log4j.AppenderSkeleton;
import org.apache.log4j.Layout;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.helpers.LogLog;
import org.apache.log4j.helpers.OptionConverter;
import org.apache.log4j.spi.LoggingEvent;
import org.codehaus.jackson.annotate.JsonProperty;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.SerializationConfig.Feature;
import com.amazonaws.auth.AWSCredentials;
import com.amazonaws.auth.PropertiesCredentials;
import com.amazonaws.services.sns.AmazonSNSAsync;
import com.amazonaws.services.sns.AmazonSNSAsyncClient;
import com.amazonaws.services.sns.model.ListTopicsResult;
import com.amazonaws.services.sns.model.PublishRequest;
import com.amazonaws.services.sns.model.Topic;
/**
* AWS SNS appender
*
* original idea from
*
* https://github.com/apetresc/amazon-sns-log4j-appender
*
* https://github.com/insula/log4j-sns
*
*/
public class Appender extends AppenderSkeleton {
public static final int DEFAULT_POOL_MIN = 0;
public static final int DEFAULT_POOL_MAX = 10;
//
/** log4j config option; amazon credentials file; must exist */
@JsonProperty
protected String credentials;
/** log4j config option; SNS topic name; must exist */
@JsonProperty
protected String topicName;
/**
* log4j config option; SNS topic subject; use for instance identity;
* optional
*/
@JsonProperty
protected String topicSubject;
/** log4j config option; minimum thread pool size; optional */
@JsonProperty
protected int poolMin = DEFAULT_POOL_MIN;
/** log4j config option; maximum thread pool size; optional */
@JsonProperty
protected int poolMax = DEFAULT_POOL_MAX;
/** log4j config option; layout class name; optional */
@JsonProperty
public Layout getLaoyut() {
return super.getLayout();
}
/** render for {@link #toString()} */
@JsonProperty
public String getLayoutClassName() {
final Layout layout = getLayout();
return layout == null ? null : layout.getClass().getName();
}
/** log4j config option; layout class name; optional */
public void setLayoutClassName(final String layoutClassName) {
final Layout defaultLayout = new PatternLayout();
layout = (Layout) OptionConverter.instantiateByClassName( //
layoutClassName, //
Layout.class, //
defaultLayout //
);
}
/** render for {@link #toString()} */
@JsonProperty
public String getEvaluatorClassName() {
final Evaluator evaluator = getEvaluator();
return evaluator == null ? null : evaluator.getClass().getName();
}
/** log4j config option; evaluator class name; optional */
public void setEvaluatorClassName(final String evaluatorClassName) {
final Evaluator defaultEvaluator = new EvaluatorThrottler();
evaluator = (Evaluator) OptionConverter.instantiateByClassName( //
evaluatorClassName, //
Evaluator.class, //
defaultEvaluator //
);
}
/** evaluator configured for this appender */
@JsonProperty
protected String evaluatorProperties;
//
/** evaluator configured for this appender */
@JsonProperty
protected Evaluator evaluator;
/**
* topic ARN resolved from existing amazon topic name
*
* http://aws.amazon.com/sns/faqs/#10
*/
@JsonProperty
protected String topicARN;
/** AWS SNS client thread pool */
protected ExecutorService service;
/** AWS SNS client dedicated to the appender */
protected AmazonSNSAsync amazonClient;
/** appender activation status */
@JsonProperty
protected volatile boolean isActive;
//
public boolean isActive() {
return isActive;
}
public boolean isTriggering(final LoggingEvent event) {
return isActive && evaluator.isTriggeringEvent(event);
}
public boolean hasCredentials() {
return credentials != null;
}
public boolean hasTopicName() {
return topicName != null;
}
public boolean hasTopicSubject() {
return topicSubject != null;
}
public boolean hasEvaluator() {
return evaluator != null;
}
public boolean hasLayout() {
return layout != null;
}
public boolean hasTopicARN() {
return topicARN != null;
}
public boolean hasAmazonClient() {
return amazonClient != null;
}
/** provide amazon login credentials from file */
protected boolean ensureCredentials() {
if (hasCredentials()) {
final File file = new File(getCredentials());
if (file.exists() && file.isFile() && file.canRead()) {
return true;
}
}
LogLog.error("sns: ivalid option", new IllegalArgumentException(
"Credentials"));
return false;
}
/** amazon topic name is required option */
protected boolean ensureTopicName() {
if (hasTopicName()) {
return true;
} else {
LogLog.error("sns: ivalid option", new IllegalArgumentException(
"TopicName"));
return false;
}
}
/** instantiate amazon client */
protected boolean ensureAmazonClient() {
try {
final File file = new File(getCredentials());
final AWSCredentials creds = new PropertiesCredentials(file);
amazonClient = new AmazonSNSAsyncClient(creds, service);
return true;
} catch (final Exception e) {
LogLog.error("sns: amazon client init failure", e);
return false;
}
}
/** resolve topic ARN from topic name */
protected boolean ensureTopicARN() {
try {
final ListTopicsResult result = amazonClient.listTopics();
final List<Topic> topicList = result.getTopics();
for (final Topic entry : topicList) {
final String arn = entry.getTopicArn();
final String name = Util.topicNameFromARN(arn);
if (getTopicName().equals(name)) {
topicARN = arn;
return true;
}
}
LogLog.error("sns: unknown topic name",
new IllegalArgumentException(getTopicName()));
return false;
} catch (final Exception e) {
LogLog.error("sns: amazon topic lookup failure", e);
return false;
}
}
/** provide default throttling evaluator */
protected boolean ensureEvaluator() {
try {
if (!hasEvaluator()) {
setEvaluator(new EvaluatorThrottler());
}
getEvaluator().setProperties(getEvaluatorProperties());
return true;
} catch (final Exception e) {
LogLog.error("sns: evaluator init falure", e);
return false;
}
}
/** provide default JSON event layout renderer */
protected boolean ensureLayout() {
try {
if (!hasLayout()) {
setLayout(new LayoutJSON());
}
return true;
} catch (final Exception e) {
LogLog.error("sns: layout init failure", e);
return false;
}
}
/** provide AWS SNS thread pool */
protected boolean ensureService() {
try {
service = new ThreadPoolExecutor(//
poolMin, //
poolMax, //
60L, //
TimeUnit.SECONDS, //
new SynchronousQueue<Runnable>(), //
new ThreadFactoryAWS() //
);
return true;
} catch (final Exception e) {
LogLog.warn("sns: failed to init service; using default", e);
service = Executors.newCachedThreadPool();
return true;
}
}
@Override
public synchronized void activateOptions() {
isActive = true //
&& ensureLayout() //
&& ensureEvaluator() //
&& ensureService() //
&& ensureCredentials() //
&& ensureAmazonClient() //
&& ensureTopicName() //
&& ensureTopicARN() //
;
LogLog.warn("sns: appender activate : " + getClass().getName() + "\n"
+ this);
if (!isActive()) {
LogLog.error("sns: appender is disabled due to invalid configration : "
+ getClass().getName());
}
}
/** */
@Override
public synchronized void close() {
isActive = false;
LogLog.warn("sns: appender deactivate : " + getClass().getName());
if (hasAmazonClient()) {
service.shutdown();
service = null;
amazonClient.shutdown();
amazonClient = null;
}
}
/** will used json layout by default */
@Override
public boolean requiresLayout() {
return true;
}
@Override
public void append(final LoggingEvent event) {
// LogLog.warn("event=" + event.getMessage());
if (!isTriggering(event)) {
return;
}
// LogLog.warn("event=" + event.getLoggerName());
String message;
if (hasLayout()) {
message = getLayout().format(event);
} else {
message = event.getRenderedMessage();
}
message = Util.forceByteLimit(message, Util.MESSAGE_LIMIT);
String subject;
if (hasTopicSubject()) {
subject = getTopicSubject();
subject = Util.forceByteLimit(subject, Util.SUBJECT_LIMIT);
} else {
subject = null;
}
publish(message, subject);
}
protected void publish(final String message, final String subject) {
try {
final PublishRequest request = new PublishRequest(//
topicARN, message, subject);
amazonClient.publishAsync(request);
} catch (final Exception e) {
LogLog.error("sns: publish failure", e);
}
}
public String getCredentials() {
return credentials;
}
public void setCredentials(final String credentials) {
this.credentials = credentials;
}
public String getTopicName() {
return topicName;
}
public void setTopicName(final String topicName) {
this.topicName = topicName;
}
public String getTopicSubject() {
return topicSubject;
}
public void setTopicSubject(final String topicSubject) {
this.topicSubject = topicSubject;
}
public Evaluator getEvaluator() {
return evaluator;
}
public void setEvaluator(final Evaluator evaluator) {
this.evaluator = evaluator;
}
public int getPoolMin() {
return poolMin;
}
public void setPoolMin(final int poolMin) {
this.poolMin = poolMin;
}
public void setPoolMin(final String poolMinText) {
this.poolMin = Util.getIntValue(poolMinText, DEFAULT_POOL_MIN);
}
public int getPoolMax() {
return poolMax;
}
public void setPoolMax(final int poolMax) {
this.poolMax = poolMax;
}
public void setPoolMax(final String poolMaxText) {
this.poolMax = Util.getIntValue(poolMaxText, DEFAULT_POOL_MAX);
}
public String getEvaluatorProperties() {
return evaluatorProperties;
}
public void setEvaluatorProperties(final String evaluatorProperties) {
this.evaluatorProperties = evaluatorProperties;
}
/** render as JSON; use only @JsonProperty annotated fields */
@Override
public String toString() {
try {
final ObjectMapper mapper = new ObjectMapper();
mapper.configure(Feature.INDENT_OUTPUT, true);
mapper.configure(Feature.USE_ANNOTATIONS, true);
mapper.configure(Feature.AUTO_DETECT_FIELDS, false);
mapper.configure(Feature.AUTO_DETECT_GETTERS, false);
mapper.configure(Feature.AUTO_DETECT_IS_GETTERS, false);
mapper.configure(Feature.FAIL_ON_EMPTY_BEANS, false);
return mapper.writeValueAsString(this);
} catch (final Exception e) {
LogLog.error("sns: ", e);
return "{}";
}
}
}