http4s-armeria

Maven Central Build Status

Http4s server and client on Armeria

Highlights

  • You can run Http4s services on top of Armeria's asynchronous and reactive server.
  • You can maximize your service and client with Armeria's awesome features such as:
    • gRPC server and client
    • Circuit Breaker
    • Automatic retry
    • Dynamic service discovery
    • Distributed tracing
    • and so on

Installation

Add the following dependencies to build.sbt

// For server
libraryDependencies += "org.http4s" %% "http4s-armeria-server" % "<lastest-version>"
// For client
libraryDependencies += "org.http4s" %% "http4s-armeria-client" % "<lastest-version>"

Quick start

http4s integration

Run your http4s service with Armeria server

You can bind your http4s service using ArmeriaServerBuilder[F].withHttpRoutes().

import cats.effect._
import com.linecorp.armeria.common.metric.{MeterIdPrefixFunction, PrometheusMeterRegistries}
import com.linecorp.armeria.server.metric.{MetricCollectingService, PrometheusExpositionService}
import org.http4s.armeria.server.{ArmeriaServer, ArmeriaServerBuilder}

object ArmeriaExample extends IOApp {
  override def run(args: List[String]): IO[ExitCode] =
    ArmeriaExampleApp.resource[IO].use(_ => IO.never).as(ExitCode.Success)
}

object ArmeriaExampleApp {
  def builder[F[_]: ConcurrentEffect: ContextShift: Timer]: ArmeriaServerBuilder[F] = {
    val registry = PrometheusMeterRegistries.newRegistry()
    val prometheusRegistry = registry.getPrometheusRegistry
    ArmeriaServerBuilder[F]
      .bindHttp(8080)
      // Sets your own meter registry
      .withMeterRegistry(registry)
      // Binds HttpRoutes to Armeria server
      .withHttpRoutes("/http4s", ExampleService[F].routes())
      // Adds PrometheusExpositionService provided by Armeria for exposing Prometheus metrics
      .withHttpService("/metrics", PrometheusExpositionService.of(prometheusRegistry))
      // Decorates your services with MetricCollectingService for collecting metrics
      .withDecorator(
        MetricCollectingService.newDecorator(MeterIdPrefixFunction.ofDefault("server")))
  }

  def resource[F[_]: ConcurrentEffect: ContextShift: Timer]: Resource[F, ArmeriaServer[F]] =
    builder[F].resource
}

Call your service with http4s-armeria client

You can create http4s client using ArmeriaClientBuilder.

import com.linecorp.armeria.client.circuitbreaker._
import com.linecopr.armeria.client.logging._
import com.linecopr.armeria.client.retry._
import org.http4s.armeria.client.ArmeriaClientBuilder 

val client: Client[IO] = 
  ArmeriaClientBuilder
    .unsafe[IO](s"http://127.0.0.1:${server.activeLocalPort()}")
    // Automically retry on unprocessed requests
    .withDecorator(RetryingClient.newDecorator(RetryRule.onUnprocessed()))
    // Open circuit on 5xx server error status
    .withDecorator(CircuitBreakerClient.newDecorator(CircuitBreaker.ofDefaultName(),
                                                     CircuitBreakerRule.onServerErrorStatus()))
    // Log requests and responses
    .withDecorator(LoggingClient.newDecorator())
    .withResponseTimeout(10.seconds)
    .build()
    
val response = client.expect[String]("Armeria").unsafeRunSync()

fs2-grpc integration

Run your fs2-grpc service with Armeria gRPC server

Add the following dependencies to build.sbt.

libraryDependencies += Seq(
  "com.linecorp.armeria" % "armeria-grpc" % "1.5.0",
  "com.linecorp.armeria" %% "armeria-scalapb" % "1.5.0")

Add your fs2-grpc service to GrpcService.

import com.linecorp.armeria.server.grpc.GrpcService
import com.linecorp.armeria.common.scalapb.ScalaPbJsonMarshaller

// Build gRPC service
val grpcService = GrpcService
  .builder()
  .addService(HelloServiceFs2Grpc.bindService(new HelloServiceImpl))
  // Register `ScalaPbJsonMarshaller` to support gRPC JSON format
  .jsonMarshallerFactory(_ => ScalaPbJsonMarshaller())
  .enableUnframedRequests(true)
  .build()

You can run http4s service and gRPC service together with sharing a single HTTP port.

ArmeriaServerBuilder[IO]
  .bindHttp(httpPort)
  .withHttpServiceUnder("/grpc", grpcService)
  .withHttpRoutes("/rest", ExampleService[IO].routes())
  .resource

Call your gRPC service using fs2-grpc with Armeria gRPC client

import com.linecorp.armeria.client.Clients
import com.linecorp.armeria.client.grpc.{GrpcClientOptions, GrpcClientStubFactory}

val client: HelloServiceFs2Grpc[IO, Metadata] =
 Clients
   .builder(s"gproto+http://127.0.0.1:$httpPort/grpc/")
   // Add a circuit breaker for your gRPC client
   .decorator(CircuitBreakerClient.newDecorator(CircuitBreaker.ofDefaultName(),
                                                CircuitBreakerRule.onServerErrorStatus()))
   .option(GrpcClientOptions.GRPC_CLIENT_STUB_FACTORY.newValue(new GrpcClientStubFactory {
     // Specify `ServiceDescriptor` of your generated gRPC stub
     override def findServiceDescriptor(clientType: Class[_]): ServiceDescriptor =
       HelloServiceGrpc.SERVICE

     // Returns a newly created gRPC client stub from the given `Channel`
     override def newClientStub(clientType: Class[_], channel: Channel): AnyRef =
       HelloServiceFs2Grpc.stub[IO](channel)

   }))
   .build(classOf[HelloServiceFs2Grpc[IO, Metadata]])

Visit examples to find a fully working example.