All Downloads are FREE. Search and download functionalities are using the official Maven repository.

busymachines.pureharm.aws.logger.internals.AWSRemoteLoggerImpl.scala Maven / Gradle / Ivy

The newest version!
/** Copyright (c) 2017-2019 BusyMachines
  *
  * See company homepage at: https://www.busymachines.com/
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *     http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
package busymachines.pureharm.aws.logger.internals

import busymachines.pureharm.aws.logger._
import busymachines.pureharm.effects._
import busymachines.pureharm.effects.implicits._
import io.chrisdavenport.log4cats.SelfAwareStructuredLogger

/** Logger that only logs to remote (except failures while trying
  * to connect to AWS, which it logs locally).
  *
  * Composed with a fully local logger to implement an [[busymachines.pureharm.aws.logger.AWSLogger]]
  *
  * @param localLogger
  *   used to log any failures of logging to the cloud
  *
  * -----
  * @author Lorand Szakacs, https://github.com/lorandszakacs
  * @since 09 Apr 2019
  */
final private[logger] class AWSRemoteLoggerImpl[F[_]] private[logger] (
  private val config:      CloudWatchLoggerConfig,
  private val localLogger: SelfAwareStructuredLogger[F],
  private val remote:      AWSRemoteLoggerImpl.AWSHelper[F],
)(
  implicit private val F:  Concurrent[F],
  implicit private val cs: BlockingShifter[F],
) extends SelfAwareStructuredLogger[F] {
  import AWSRemoteLoggerImpl.Level

  override def trace(ctx: Map[String, String])(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Trace, ctx, msg)

  override def trace(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Trace, t, ctx, msg)

  override def debug(ctx: Map[String, String])(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Debug, ctx, msg)

  override def debug(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Debug, t, ctx, msg)

  override def info(ctx: Map[String, String])(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Info, ctx, msg)

  override def info(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Info, t, ctx, msg)

  override def warn(ctx: Map[String, String])(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Warn, ctx, msg)

  override def warn(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Warn, t, ctx, msg)

  override def error(ctx: Map[String, String])(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Error, ctx, msg)

  override def error(ctx: Map[String, String], t: Throwable)(msg: => String): F[Unit] =
    remote.logs2Cloud(Level.Error, t, ctx, msg)

  override def error(t: Throwable)(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Error, t, message)

  override def warn(t: Throwable)(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Warn, t, message)

  override def info(t: Throwable)(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Info, t, message)

  override def debug(t: Throwable)(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Debug, t, message)

  override def trace(t: Throwable)(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Trace, t, message)

  override def error(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Error, message)

  override def warn(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Warn, message)

  override def info(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Info, message)

  override def debug(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Debug, message)

  override def trace(message: => String): F[Unit] =
    remote.logs2Cloud(Level.Trace, message)

  override def isTraceEnabled: F[Boolean] = localLogger.isTraceEnabled

  override def isDebugEnabled: F[Boolean] = localLogger.isDebugEnabled

  override def isInfoEnabled: F[Boolean] = localLogger.isInfoEnabled

  override def isWarnEnabled: F[Boolean] = localLogger.isWarnEnabled

  override def isErrorEnabled: F[Boolean] = localLogger.isErrorEnabled
}

private[logger] object AWSRemoteLoggerImpl {

  import com.amazonaws.services.logs.AWSLogsAsync

  def apply[F[_]: Concurrent: Timer: BlockingShifter](
    config:      CloudWatchLoggerConfig,
    localLogger: SelfAwareStructuredLogger[F],
    awsLogs:     AWSLogsAsync,
  ): AWSRemoteLoggerImpl[F] =
    new AWSRemoteLoggerImpl(
      config      = config,
      localLogger = localLogger,
      remote      = new AWSHelper[F](config, localLogger, awsLogs),
    )

  sealed trait Level extends Product with Serializable

  object Level {
    case object Trace extends Level
    case object Debug extends Level
    case object Info  extends Level
    case object Warn  extends Level
    case object Error extends Level
  }

  import com.amazonaws.services.logs.model._
  import scala.jdk.CollectionConverters._

  final private[logger] class AWSHelper[F[_]: Timer](
    private val config:     CloudWatchLoggerConfig,
    private val logger:     SelfAwareStructuredLogger[F],
    private val awsLogs:    AWSLogsAsync,
  )(implicit private val F: Concurrent[F], private val shifter: BlockingShifter[F]) {

    private[logger] def logs2Cloud(l: Level, msg: String): F[Unit] =
      logWithLevel(l, msg)

    private[logger] def logs2Cloud(l: Level, t: Throwable, msg: String): F[Unit] =
      logWithLevel(l, s"$msg >>> reason: ${t.getLocalizedMessage} <<<")

    private[logger] def logs2Cloud(l: Level, ctx: Map[String, String], msg: String): F[Unit] =
      logWithLevel(l, s"$msg ** ${mkString(ctx)} **")

    private[logger] def logs2Cloud(l: Level, t: Throwable, ctx: Map[String, String], msg: String): F[Unit] =
      logWithLevel(l, s"$msg >>> reason: ${t.getLocalizedMessage} <<< ${mkString(ctx)}")

    private def mkString(ctx: Map[String, String]): String = ctx.mkString("{", ",", "}")

    //TODO: maybe this should put messages in a queue and only call logs2Cloud on a larger List
    //TODO: queue emptied at fixed size, or at fixed intervals
    //TODO: implement this in second iteration
    private def logWithLevel(l: Level, message: String): F[Unit] =
      for {
        //TODO: create time module
        now <- F.delay(java.time.ZonedDateTime.now().toInstant.toEpochMilli)
        log = new InputLogEvent().withTimestamp(now).withMessage(s"[${l.productPrefix}] $message")
        _ <- logToClouds(List(log))
      } yield ()

    /** FIXME: Super rough implementation. REVISE
      * Requires actual study of AWS logging to see if there's
      * anything to be improved here.
      */
    private def logToClouds(logs: List[InputLogEvent]): F[Unit] = {
      val logF: F[Unit] =
        for {
          logDesc  <- describeLogStreams
          tokenOpt <- getUploadSequenceToken(logDesc)
          plrq: PutLogEventsRequest = tokenOpt match {
            case Some(tk) =>
              new PutLogEventsRequest(config.groupName, config.streamName, logs.asJava)
                .withSequenceToken(tk)
            case None     =>
              new PutLogEventsRequest(config.groupName, config.streamName, logs.asJava)
                .withSequenceToken(null) //java :'(!
          }
          _ <- putLogsOnCloud(plrq).void
        } yield ()

      val nonFailingF = logF.timeout(config.timeout).recoverWith { case NonFatal(e) =>
        logger.trace(e)("Failed to log to AWS Cloud Watch!")
      }

      //This is where the magic happens!
      //N.B. we start a fiber off of logF to ensure
      //forkAndForget semantics, and the shifter ensures
      //that the fiber shifts the work on the blocking IO pool
      shifter.blockOn(nonFailingF.start).void
    }

    private def describeLogStreams: F[DescribeLogStreamsResult] = {
      val req = new DescribeLogStreamsRequest()
        .withLogGroupName(config.groupName)
        .withLogStreamNamePrefix(config.streamName)
      F.delay(awsLogs.describeLogStreams(req))
    }

    private def getUploadSequenceToken(lsr: DescribeLogStreamsResult): F[Option[String]] = F.delay {
      lsr.getLogStreams.asScala.find(_.getLogStreamName == config.streamName).map(_.getUploadSequenceToken)
    }

    //TODO: maybe put the next sequence token in an MVar and take it from there...
    //TODO: but first you'd have to see if that's how the API is intended to be used...
    private def putLogsOnCloud(plrq: PutLogEventsRequest): F[PutLogEventsResult] =
      F.delay(awsLogs.putLogEvents(plrq))
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy