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

com.malliina.logstreams.client.FS2Appender.scala Maven / Gradle / Ivy

The newest version!
package com.malliina.logstreams.client

import cats.effect.IO
import cats.effect.kernel.{Async, Resource, Sync}
import cats.effect.std.Dispatcher
import cats.effect.unsafe.{IORuntime, IORuntimeConfig, Scheduler}
import cats.syntax.all.{catsSyntaxFlatMapOps, toFunctorOps}
import com.malliina.http.io.{HttpClientF2, HttpClientIO, WebSocketF}
import com.malliina.logback.fs2.{FS2AppenderComps, LoggingComps}
import com.malliina.logstreams.client.FS2Appender.ResourceParts

import java.util.concurrent.{ExecutorService, Executors}
import scala.concurrent.ExecutionContext
import scala.concurrent.duration.DurationInt

object FS2Appender:
  val executor: ExecutorService = Executors.newCachedThreadPool()
  val ec: ExecutionContext = ExecutionContext.fromExecutor(executor)

  case class SocketComps[F[_]](comps: LoggingComps[F], http: HttpClientF2[F])

  case class ResourceParts[F[_]](
    comps: LoggingComps[F],
    http: HttpClientF2[F],
    finalizer: F[Unit]
  )

  private def customRuntime: IORuntime =
    val (scheduler, finalizer) = IORuntime.createDefaultScheduler()
    IORuntime(ec, ec, scheduler, finalizer, IORuntimeConfig())

  private def dispatched(d: Dispatcher[IO], dispatcherFinalizer: IO[Unit]): ResourceParts[IO] =
    val resource = for
      comps <- Resource.eval(FS2AppenderComps.io(d))
      http <- HttpClientIO.resource[IO]
    yield SocketComps(comps, http)
    val (comps, finalizer) = d.unsafeRunSync(resource.allocated[SocketComps[IO]])
    ResourceParts(comps.comps, comps.http, finalizer >> dispatcherFinalizer)

  def unsafe: ResourceParts[IO] =
    val rt = customRuntime
    val (d, finalizer) = Dispatcher.parallel[IO].allocated.unsafeRunSync()(rt)
    dispatched(d, finalizer >> IO(rt.shutdown()))

  def default[F[_]: Async](
    d: Dispatcher[F],
    http: HttpClientF2[F],
    extraHeaders: Map[String, String] = Map.empty
  ): F[FS2AppenderF[F]] =
    FS2AppenderComps
      .io(d)
      .map: parts =>
        FS2AppenderF(ResourceParts(parts, http, Async[F].unit), extraHeaders)

class FS2Appender(
  res: ResourceParts[IO]
) extends FS2AppenderF[IO](res, Map.empty):
  def this() = this(FS2Appender.unsafe)
  override def stop(): Unit =
    super.stop()
    FS2Appender.executor.shutdown()

class FS2AppenderF[F[_]: Async](
  res: ResourceParts[F],
  extraHeaders: Map[String, String]
) extends SocketAppender[F, WebSocketF[F]](res.comps):
  val F = Sync[F]
  private var socketClosable: F[Unit] = F.unit
  override def start(): Unit =
    if getEnabled then
      val result = for
        url <- toMissing(endpoint, "endpoint")
        user <- toMissing(username, "username")
        pass <- toMissing(password, "password")
      yield
        val headers: List[KeyValue] = List(HttpUtil.basicAuth(user, pass))
        addInfo(s"Connecting to logstreams URL '$url' for Logback...")
        val socketIo: Resource[F, WebSocketF[F]] =
          res.http.socket(url, headers.map(kv => kv.key -> kv.value).toMap ++ extraHeaders)
        val (socket, closer) = d.unsafeRunSync(socketIo.allocated[WebSocketF[F]])
        client = Option(socket)
        socketClosable = closer
        d.unsafeRunAndForget(socket.events.compile.drain)
        val task: F[Unit] = logEvents
          .groupWithin(100, 200.millis)
          .evalMap(es => socket.send(LogEvents(es.toList)))
          .onComplete:
            fs2.Stream
              .eval(F.delay(addInfo(s"Appender [$name] completed.")))
              .flatMap(_ => fs2.Stream.empty)
          .compile
          .drain
        d.unsafeRunAndForget(task)
        super.start()
      result.left.toOption foreach addError
    else addInfo("Logstreams client is disabled.")

  override def stop(): Unit =
    d.unsafeRunSync(client.map(_.close).getOrElse(F.unit) >> res.finalizer >> socketClosable)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy