/** * Copyright 2005-2016 Crown Equipment Corporation. All rights reserved. * See license distributed with this file. */ package com.gu.logback.appender.kinesis; import java.net.URI; import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingDeque; import java.util.concurrent.ThreadFactory; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.core.SdkClient; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; import software.amazon.awssdk.core.retry.RetryPolicy; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.sts.auth.StsAssumeRoleCredentialsProvider; import com.gu.logback.appender.kinesis.helpers.BlockFastProducerPolicy; import com.gu.logback.appender.kinesis.helpers.NamedThreadFactory; import com.gu.logback.appender.kinesis.helpers.Validator; import ch.qos.logback.core.AppenderBase; import ch.qos.logback.core.LayoutBase; import ch.qos.logback.core.spi.DeferredProcessingAware; /** * Base class for Kinesis and Kinesis Firehose appenders containing common * attributes, * * @since 1.4 */ public abstract class BaseKinesisAppender<Event extends DeferredProcessingAware, Client extends SdkClient> extends AppenderBase<Event> { private String encoding = AppenderConstants.DEFAULT_ENCODING; private int maxRetries = AppenderConstants.DEFAULT_MAX_RETRY_COUNT; private int bufferSize = AppenderConstants.DEFAULT_BUFFER_SIZE; private int threadCount = AppenderConstants.DEFAULT_THREAD_COUNT; private int shutdownTimeout = AppenderConstants.DEFAULT_SHUTDOWN_TIMEOUT_SEC; private String endpoint; private String region; private String streamName; private String roleToAssumeArn; private boolean initializationFailed = false; private BlockingQueue<Runnable> taskBuffer; private ThreadPoolExecutor threadPoolExecutor; private LayoutBase<Event> layout; private AwsCredentialsProvider credentials = DefaultCredentialsProvider.create(); private Client client; /** * Configures appender instance and makes it ready for use by the consumers. * It validates mandatory parameters and confirms if the configured stream is * ready for publishing data yet. * * Error details are made available through the fallback handler for this * appender * * @throws IllegalStateException if we encounter issues configuring this * appender instance */ @Override public void start() { if(layout == null) { initializationFailed = true; addError("Invalid configuration - No layout for appender: " + name); return; } if(streamName == null) { initializationFailed = true; addError("Invalid configuration - streamName cannot be null for appender: " + name); return; } ClientOverrideConfiguration clientConfiguration = ClientOverrideConfiguration.builder() .retryPolicy(RetryPolicy.defaultRetryPolicy().toBuilder().numRetries(maxRetries).build()) .putHeader("User-Agent", AppenderConstants.USER_AGENT_STRING) .build(); BlockingQueue<Runnable> taskBuffer = new LinkedBlockingDeque<Runnable>(bufferSize); threadPoolExecutor = new ThreadPoolExecutor(threadCount, threadCount, AppenderConstants.DEFAULT_THREAD_KEEP_ALIVE_SEC, TimeUnit.SECONDS, taskBuffer, setupThreadFactory(), new BlockFastProducerPolicy()); threadPoolExecutor.prestartAllCoreThreads(); Optional<URI> endpointOverride; if(!Validator.isBlank(endpoint)) { if(!Validator.isBlank(region)) { addError("Received configuration for both region as well as Amazon Kinesis endpoint. (" + endpoint + ") will be used as endpoint instead of default endpoint for region (" + region + ")"); } endpointOverride = Optional.of(URI.create(endpoint)); } else { endpointOverride = Optional.empty(); } this.client = createClient(credentials, clientConfiguration, threadPoolExecutor, findRegion(), endpointOverride); validateStreamName(client, streamName); super.start(); } /** * Closes this appender instance. Before exiting, the implementation tries to * flush out buffered log events within configured shutdownTimeout seconds. If * that doesn't finish within configured shutdownTimeout, it would drop all * the buffered log events. */ @Override public void stop() { threadPoolExecutor.shutdown(); BlockingQueue<Runnable> taskQueue = threadPoolExecutor.getQueue(); int bufferSizeBeforeShutdown = threadPoolExecutor.getQueue().size(); boolean gracefulShutdown = true; try { gracefulShutdown = threadPoolExecutor.awaitTermination(shutdownTimeout, TimeUnit.SECONDS); } catch(InterruptedException e) { // we are anyways cleaning up } finally { int bufferSizeAfterShutdown = taskQueue.size(); if(!gracefulShutdown || bufferSizeAfterShutdown > 0) { String errorMsg = "Kinesis Log4J Appender (" + name + ") waited for " + shutdownTimeout + " seconds before terminating but could send only " + (bufferSizeAfterShutdown - bufferSizeBeforeShutdown) + " logevents, it failed to send " + bufferSizeAfterShutdown + " pending log events from it's processing queue"; addError(errorMsg); } } client.close(); } /** * Validate that the stream name exists and is in a valid status. */ protected abstract void validateStreamName(Client client, String streamName); /** * This method is called whenever a logging happens via logger.log(..) API * calls. Implementation for this appender will take in log events instantly * as long as the buffer is not full (as per user configuration). This call * will block if internal buffer is full until internal threads create some * space by publishing some of the records. * * If there is any error in parsing logevents, those logevents would be * dropped. */ @Override protected void append(Event logEvent) { if(initializationFailed) { addError("Check the configuration and whether the configured stream " + streamName + " exists and is active. Failed to initialize kinesis logback appender: " + name); return; } try { String message = this.layout.doLayout(logEvent); putMessage(message); } catch(Exception e) { addError("Failed to schedule log entry for publishing into Kinesis stream: " + streamName, e); } } /** * Send message to client * * @param message formatted message to send * @throws Exception if unable to add message */ protected abstract void putMessage(String message) throws Exception; /** * Creates the thread factory to be used by the {@link #threadPoolExecutor}. */ private ThreadFactory setupThreadFactory() { return new NamedThreadFactory(getClass().getSimpleName() + "[" + streamName + "]-"); } /** * Determine region. If not specified tries to determine region from where the * application is running or fall back to the default. * * @return Region to configure the client */ private Region findRegion() { boolean regionProvided = !Validator.isBlank(this.region); if(!regionProvided) { // Determine region from where application is running, or fall back to default region return Region.of(AppenderConstants.DEFAULT_REGION); } return Region.of(this.region); } public LayoutBase<Event> getLayout() { return layout; } public void setLayout(LayoutBase<Event> layout) { this.layout = layout; } /** * Returns configured stream name * * @return configured stream name */ public String getStreamName() { return streamName; } /** * Sets streamName for the kinesis stream to which data is to be published. * * @param streamName name of the kinesis stream to which data is to be * published. */ public void setStreamName(String streamName) { Validator.validate(!Validator.isBlank(streamName), "streamName cannot be blank"); this.streamName = streamName.trim(); } /** * Configured encoding for the data to be published. If none specified, * default is UTF-8 * * @return encoding for the data to be published. If none specified, default * is UTF-8 */ public String getEncoding() { return this.encoding; } /** * Sets encoding for the data to be published. If none specified, default is * UTF-8 * * @param charset encoding for expected log messages */ public void setEncoding(String charset) { Validator.validate(!Validator.isBlank(charset), "encoding cannot be blank"); this.encoding = charset.trim(); } /** * Returns configured maximum number of retries between API failures while * communicating with Kinesis. This is used in AWS SDK's default retries for * HTTP exceptions, throttling errors etc. * * @return configured maximum number of retries between API failures while * communicating with Kinesis */ public int getMaxRetries() { return maxRetries; } /** * Configures maximum number of retries between API failures while * communicating with Kinesis. This is used in AWS SDK's default retries for * HTTP exceptions, throttling errors etc. * * @param maxRetries the number of retries between API failures */ public void setMaxRetries(int maxRetries) { Validator.validate(maxRetries > 0, "maxRetries must be > 0"); this.maxRetries = maxRetries; } /** * Returns configured buffer size for this appender. This implementation would * buffer these many log events in memory while parallel threads are trying to * publish them to Kinesis. * * @return configured buffer size for this appender. */ public int getBufferSize() { return bufferSize; } /** * Configures buffer size for this appender. This implementation would buffer * these many log events in memory while parallel threads are trying to * publish them to Kinesis. * * @param bufferSize buffer size for this appender * */ public void setBufferSize(int bufferSize) { Validator.validate(bufferSize > 0, "bufferSize must be >0"); this.bufferSize = bufferSize; } /** * Returns configured number of parallel thread count that would work on * publishing buffered events to Kinesis * * @return configured number of parallel thread count that would work on * publishing buffered events to Kinesis */ public int getThreadCount() { return threadCount; } /** * Configures number of parallel thread count that would work on publishing * buffered events to Kinesis * * @param parallelCount number of parallel thread count * */ public void setThreadCount(int parallelCount) { Validator.validate(parallelCount > 0, "threadCount must be >0"); this.threadCount = parallelCount; } /** * Returns configured timeout between shutdown and clean up. When this * appender is asked to close/stop, it would wait for at most these many * seconds and try to send all buffered records to Kinesis. However if it * fails to publish them before timeout, it would drop those records and exit * immediately after timeout. * * @return configured timeout for shutdown and clean up. */ public int getShutdownTimeout() { return shutdownTimeout; } /** * Configures timeout between shutdown and clean up. When this appender is * asked to close/stop, it would wait for at most these many seconds and try * to send all buffered records to Kinesis. However if it fails to publish * them before timeout, it would drop those records and exit immediately after * timeout. * * @param shutdownTimeout timeout between shutdown and clean up * */ public void setShutdownTimeout(int shutdownTimeout) { Validator.validate(shutdownTimeout > 0, "shutdownTimeout must be >0"); this.shutdownTimeout = shutdownTimeout; } /** * Returns count of tasks scheduled to send records to Kinesis. Since * currently each task maps to sending one record, it is equivalent to number * of records in the buffer scheduled to be sent to Kinesis. * * @return count of tasks scheduled to send records to Kinesis. */ public int getTaskBufferSize() { return taskBuffer.size(); } public String getRoleToAssumeArn() { return roleToAssumeArn; } public void setRoleToAssumeArn(String roleToAssumeArn) { this.roleToAssumeArn = roleToAssumeArn; if(!Validator.isBlank(roleToAssumeArn)) { String sessionId = "session" + Math.random(); StsAssumeRoleCredentialsProvider remoteAccountCredentials = StsAssumeRoleCredentialsProvider.builder().refreshRequest(builder -> builder.roleArn(roleToAssumeArn).roleSessionName(sessionId).build()).build(); credentials = remoteAccountCredentials; } } public AwsCredentialsProvider getCredentialsProvider() { return credentials; } public void setCredentialsProvider(AwsCredentialsProvider credentialsProvider) { this.credentials = credentialsProvider; } /** * Returns configured Kinesis endpoint. * * @return configured kinesis endpoint */ public String getEndpoint() { return endpoint; } /** * Set kinesis endpoint. If set, it overrides the default kinesis endpoint in * the configured region * * @param endpoint kinesis endpoint to which requests should be made. */ public void setEndpoint(String endpoint) { this.endpoint = endpoint; } /** * Returns configured region for Kinesis. * * @return configured region for Kinesis */ public String getRegion() { return region; } /** * Configures the region and default endpoint for all Kinesis calls. If not * overridden by {@link #setEndpoint(String)}, all Kinesis requests are made * to the default endpoint in this region. * * @param region the Kinesis region whose endpoint should be used for kinesis * requests */ public void setRegion(String region) { this.region = region; } protected void setInitializationFailed(boolean initializationFailed) { this.initializationFailed = initializationFailed; } protected abstract Client createClient(AwsCredentialsProvider credentials, ClientOverrideConfiguration configuration, ThreadPoolExecutor executor, Region region, Optional<URI> endpointOverride); protected Client getClient() { return client; } }