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

com.snowplowanalytics.snowplow.runtime.HealthProbe.scala Maven / Gradle / Ivy

/*
 * Copyright (c) 2023-present Snowplow Analytics Ltd. All rights reserved.
 *
 * This program is licensed to you under the Snowplow Community License Version 1.0,
 * and you may not use this file except in compliance with the Snowplow Community License Version 1.0.
 * You may obtain a copy of the Snowplow Community License Version 1.0 at https://docs.snowplow.io/community-license-1.0
 */
package com.snowplowanalytics.snowplow.runtime

import cats.Show
import cats.implicits._
import cats.data.Kleisli
import cats.effect.{Async, Resource, Sync}
import com.comcast.ip4s.{Ipv4Address, Port}
import io.circe.Decoder
import org.http4s.ember.server.EmberServerBuilder
import org.http4s.{HttpApp, Response, Status}
import org.typelevel.log4cats.Logger
import org.typelevel.log4cats.slf4j.Slf4jLogger
import fs2.io.net.Network

object HealthProbe {

  private implicit def logger[F[_]: Sync]: Logger[F] = Slf4jLogger.getLogger[F]

  def resource[F[_]: Async, RuntimeService: Show](
    port: Port,
    appHealth: AppHealth[F, ?, RuntimeService]
  ): Resource[F, Unit] = {
    implicit val network: Network[F] = Network.forAsync[F]
    EmberServerBuilder
      .default[F]
      .withHost(Ipv4Address.fromBytes(0, 0, 0, 0))
      .withPort(port)
      .withMaxConnections(1)
      .withHttpApp(httpApp(appHealth))
      .build
      .evalTap { _ =>
        Logger[F].info(s"Health service listening on port $port")
      }
      .void
  }

  object decoders {
    implicit def portDecoder: Decoder[Port] = Decoder.decodeInt.emap { port =>
      Port.fromInt(port).toRight("Invalid port")
    }
  }

  private[runtime] def httpApp[F[_]: Sync, RuntimeService: Show](
    appHealth: AppHealth[F, ?, RuntimeService]
  ): HttpApp[F] =
    Kleisli { _ =>
      val problemsF = for {
        runtimeUnhealthies <- appHealth.unhealthyRuntimeServiceMessages
        setupHealth <- appHealth.setupHealth.get
      } yield {
        val allUnhealthy = runtimeUnhealthies ++ (setupHealth match {
          case AppHealth.SetupStatus.Unhealthy(_) => Some("External setup configuration")
          case _                                  => None
        })

        val allAwaiting = setupHealth match {
          case AppHealth.SetupStatus.AwaitingHealth => Some("External setup configuration")
          case _                                    => None
        }

        val unhealthyMsg = if (allUnhealthy.nonEmpty) {
          val joined = allUnhealthy.mkString("Services are unhealthy [", ", ", "]")
          Some(joined)
        } else None

        val awaitingMsg = if (allAwaiting.nonEmpty) {
          val joined = allAwaiting.mkString("Services are awaiting a healthy status [", ", ", "]")
          Some(joined)
        } else None

        if (unhealthyMsg.isEmpty && awaitingMsg.isEmpty)
          None
        else
          Some((unhealthyMsg ++ awaitingMsg).mkString(" AND "))
      }

      problemsF.flatMap {
        case Some(errorMsg) =>
          Logger[F].warn(s"Health probe returning 503: $errorMsg").as {
            Response(status = Status.ServiceUnavailable)
          }
        case None =>
          Logger[F].debug("Health probe returning 200").as {
            Response(status = Status.Ok)
          }
      }
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy