org.gfccollective.aws.cloudwatch.CloudWatchLogsClient.scala Maven / Gradle / Ivy
The newest version!
package org.gfccollective.aws.cloudwatch
import software.amazon.awssdk.services.cloudwatchlogs.CloudWatchLogsAsyncClient
import software.amazon.awssdk.services.cloudwatchlogs.model._
import scala.concurrent.ExecutionContext
import scala.util.Success
import org.gfccollective.concurrent.JavaConverters._
import org.gfccollective.concurrent.SameThreadExecutionContext
import org.gfccollective.logging.OpenLoggable
import scala.compat.java8.FutureConverters._
import scala.collection.JavaConverters._
import scala.concurrent.Future
import scala.util.control.NonFatal
/** A type class of things that can be converted to CloudWatch log events. */
trait ToCloudWatchLogsData[A] {
/** A function to convert type A to CW log events. */
def toLogEvents(a: A): Seq[InputLogEvent]
}
/** Implement this to store the nextSequenceToken in a manner that works for your app.
* How you do this will be determined by whether or not you have race conditions, concurrency issues, multiple nodes,
* etc. Using the default implementation below will likely not work well if you have any of the above.
*
* The nextSequenceToken is unique for each group/stream combination in CloudWatch Logs.
* */
trait NextSequenceTokenPersistor {
def getNextSequenceToken(groupName: String, streamName: String): String
def setNextSequenceToken(groupName: String, streamName: String, token: String): Unit
}
/**
* Push selected logs to AWS CloudWatch Logs.
* Some are meant to be 'system health' logs, like our Future-based RPC call times.
* Some are meant to be 'business' logs, like number of logins.
*
* Trait exists mainly to make it possible to inject this as a dependency.
*/
trait CloudWatchLogsClient {
/** Log group in CloudWatch Logs will be a concatenation of all of these, except for the last one,
* which will be the stream.
* Every time you enter a namespace it'll get appended to the current one, e.g. "a/b/c".
*/
def enterNamespace( n: String
): CloudWatchLogsClient
/** Run a closure with a namespaced CW logs client, just a convenience function.
*
* @param n namespace to enter
* @param run closure to run
* @tparam R closure result type
* @return closure result
*/
def withNamespace[R]( n: String
)( run: (CloudWatchLogsClient) => R
): R = {
run(this.enterNamespace(n))
}
/** Requests log data to be sent asynchronously.
* It will block only to schedule async tasks, shouldn't be noticeable under low contention.
* HTTP service calls are asynchronous.
*
* http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
*
* @param logName the name of the AWS CW Logs stream the log events will be written to
* @param a object to send logs data for
* @param tcwmdEv evidence that ToCloudWatchLogsData implementation exists for type A
* @tparam A type of the object we convert to logs data
*/
def putLogData[A]( logName: String,
a: A
)( implicit tcwmdEv: ToCloudWatchLogsData[A],
nstp: NextSequenceTokenPersistor
): Unit
/** Brackets a block of code and puts a given log if an exception occurs.
*
* @param logName the name of the AWS CW Logs stream the log events will be written to
* @param t2a construct log events from throwable (e.g. you may want to report different exceptions differently)
* @param run block of code to run
* @param tcwmdEv evidence that ToCloudWatchLogsData implementation exists for type A
* @tparam A type of log
* @tparam R type of result
* @return result of the execution of the given closure
*/
def putExceptionLogData[A,R]( logName: String,
t2a: (Throwable) => A
)( run: => R
)( implicit tcwmdEv: ToCloudWatchLogsData[A],
nstp: NextSequenceTokenPersistor
): R = {
try {
run
} catch {
case NonFatal(e) =>
this.putLogData(logName, t2a(e)) // report exception and re-throw it
throw e
}
}
/** Brackets a block of asynchronous code (Future result) and puts a given log if an exception occurs
* either synchronously or wrapped in the result of the Future.
*
* @param logName the name of the AWS CW Logs stream the log events will be written to
* @param t2a construct log from throwable (e.g. you may want to report different exceptions differently)
* @param run block of code to run
* @param tcwmdEv evidence that ToCloudWatchLogsData implementation exists for type A
* @tparam A type of log
* @tparam R type of result
* @return result of the execution of the given closure
*/
def putAsynchronousExceptionLogData[A,R]( logName: String,
t2a: (Throwable) => A
)( run: => Future[R]
)( implicit tcwmdEv: ToCloudWatchLogsData[A],
nstp: NextSequenceTokenPersistor
): Future[R] = {
val safeFuture: Future[R] = this.putExceptionLogData(logName, t2a)(run) // if run() explodes before returning a future we capture it here
implicit val ec = SameThreadExecutionContext // putLogData should be lightweight
safeFuture.onComplete {
case Success(NonFatal(e)) =>
this.putLogData(logName, t2a(e))
case _ =>
()
}
safeFuture
}
}
object CloudWatchLogsClient {
/** Creates default implementation of the CloudWatchLogsClient.
*
* http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
*
* @param logNamespace CloudWatch log namespace
* @return CloudWatchLogsClient instance
*/
def apply( logNamespace: String
, awsLogs: CloudWatchLogsAsyncClient = CloudWatchLogsAsyncClient.create()
): CloudWatchLogsClient = CloudWatchLogsClientImpl(logNamespace, awsLogs)
/**
* A convenience definition of NextSequenceTokenPersistor that can be used in some cases. Be warned that
* this may not work for every use case, and you should consider implementing this for yourself.
*/
implicit object DefaultNextSequenceTokenPersistor extends NextSequenceTokenPersistor {
var nextSequenceTokens: Map[(String, String), String] = Map.empty
override
def getNextSequenceToken(groupName: String, streamName: String): String = nextSequenceTokens.get((groupName, streamName)).orNull
override
def setNextSequenceToken(groupName: String, streamName: String, token: String): Unit = {
nextSequenceTokens = nextSequenceTokens ++ Map((groupName, streamName) -> token)
}
}
}
/** Default implementation of CloudWatchLogsClient, trait is for dependency injection. */
private[cloudwatch]
object CloudWatchLogsClientImpl {
private
val Logger = new OpenLoggable {}
private
val awsClient = CloudWatchLogsAsyncClient.create
private
val executor = {
import java.util.concurrent._
new ThreadPoolExecutor(
1 // core pool size
, 2 * Runtime.getRuntime.availableProcessors // max pool size
, 60L, TimeUnit.SECONDS, // keep alive
new LinkedBlockingQueue[Runnable]()
)
}.asScala
}
private[cloudwatch]
case class CloudWatchLogsClientImpl (
namespace: String
, awsClient: CloudWatchLogsAsyncClient
) extends CloudWatchLogsClient {
import CloudWatchLogsClientImpl._
private implicit val ec = ExecutionContext.fromExecutorService(executor)
override
def enterNamespace( n: String
): CloudWatchLogsClient = {
this.copy(namespace = s"${this.namespace}/${n}") // Default CloudWatch Logs behavior makes this look like a path
}
override
def putLogData[A]( logName: String,
a: A
)( implicit tcwmdEv: ToCloudWatchLogsData[A],
nstp: NextSequenceTokenPersistor
): Unit = {
try {
putLogEvents(PutLogEventsRequest.builder
.logGroupName(s"/$namespace")
.logStreamName(logName)
.logEvents(tcwmdEv.toLogEvents(a).asJavaCollection).build)
return // Here to force the compiler to recognize the return value as Unit instead of PutLogEventResult
} catch {
case NonFatal(e) =>
Logger.error(e.getMessage, e)
}
}
/**
* Attempts to call the putLogEvents (http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html)
* endpoint with the given request, automatically filling in the nextSequenceToken stored in the given persistor.
*
* This is called recursively because any/all of the given exceptions could be thrown. If all are thrown (once per
* attempt), this method should be called until either a success or a non-caught exception is thrown.
*
* Watching for ResourceNotFoundException for both the group and stream is more efficient than using the
* DescribeLogGroups (http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DescribeLogGroups.html)
* or DescribeLogStreams (http://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_DescribeLogStreams.html)
* endpoints because the exception will be thrown once, only at the time of creation - thus calling the API three times
* once, but calling it only once per request thereafter. The alternative would be to call the "describe" endpoints on
* each putLogEvents request - thus calling the API twice, for every put request.
*
* Watching for InvalidSequenceTokenException allows this call to recover nicely if the nextSequenceToken is wrong or
* missing. This can be used to essentially ignore concurrency issues across multiple nodes (i.e. using this 'catch'
* in conjunction with the naive DefaultNextSequenceTokenPersistor above), but is not recommended because it will
* likely result in many double-calls to the API.
*
* @param request The log events to put to the API
* @param nstp Used to get the nextSequenceToken for the request; also used to store the nextSequenceToken from
* the response.
* @return The result of the put request, used mainly for getting the nextSequenceToken
*/
private
def putLogEvents(request: PutLogEventsRequest)(implicit nstp: NextSequenceTokenPersistor, ec: ExecutionContext): Future[PutLogEventsResponse] = {
val response: Future[PutLogEventsResponse] = (try {
awsClient.putLogEvents(request.toBuilder.sequenceToken(nstp.getNextSequenceToken(request.logGroupName, request.logStreamName)).build).toScala
} catch {
case e: ResourceNotFoundException if e.getMessage.toLowerCase.contains("log group") =>
awsClient.createLogGroup(
CreateLogGroupRequest.builder()
.logGroupName(request.logGroupName)
.build
)
putLogEvents(request)
case e: ResourceNotFoundException if e.getMessage.toLowerCase.contains("log stream") =>
awsClient.createLogStream(
CreateLogStreamRequest.builder
.logGroupName(request.logGroupName)
.logStreamName(request.logStreamName)
.build
).toScala
putLogEvents(request)
case e: InvalidSequenceTokenException => awsClient.putLogEvents(request.toBuilder.sequenceToken(e.expectedSequenceToken).build).toScala
}).andThen {
case Success(awsResponse) => nstp.setNextSequenceToken(request.logGroupName, request.logStreamName, awsResponse.nextSequenceToken)
}
response
}
}