Filtering Thrift Requests

c.t.finatra.thrift.filters.AccessLoggingFilter

  • provides for psuedo “Common Log Format” style logging for Thrift requests and responses.

c.t.finatra.thrift.filters.ExceptionMappingFilter

c.t.finatra.thrift.filters.LoggingMDCFilter

c.t.finatra.thrift.filters.StatsFilter

  • Tracks per-method stats scoped under per_method_stats/<method> including success/failure (with exceptions) counters, and a latency (in millis) histogram.

c.t.finatra.http.filters.TraceIdMDCFilter

Global Filters

The ThriftRouter allows you to build a global Filter chain which will trigger on the request path when executing an RPC call to methods implemented by the added Thrift Controller.

Filters must be a subclass of the c.t.finagle.Filter.TypeAgnostic which is a

c.t.finagle.Filter[T, Rep, T, Rep]

that is polymorphic in T.

If you want to apply a Filter or Filters to all methods of your Thrift Controller, call the ThriftRouter#filter method, to register a Filter.TypeAgnostic:

import DoEverythingModule
import com.twitter.finatra.thrift.ThriftServer
import com.twitter.finatra.thrift.routing.ThriftRouter
import com.twitter.finatra.thrift.filters._

object ExampleServerMain extends ExampleServer

class ExampleServer extends ThriftServer {

  override val modules = Seq(
    DoEverythingModule)

  override def configureThrift(router: ThriftRouter): Unit = {
    router
      .filter[LoggingMDCFilter]
      .filter[TraceIdMDCFilter]
      .filter[ThriftMDCFilter]
      .filter[AccessLoggingFilter]
      .filter[StatsFilter]
      .add[ExampleThriftController]
  }
}

Note, like HTTP, Filters are applied in the order they are defined on all methods. Filters can be added to the ThriftRouter by type (as in the example above) or by instance.

For more information see the Finagle User’s Guide section on Filters.

Per-method Filtering

TypeAgnostic Filters

You can filter by a TypeAgnostic Filter per-method implemented in a Controller, by calling the handle(ThriftMethod)#filtered Function e.g.:

import com.twitter.finagle.{Filter, Service, SimpleFilter}
import com.twitter.util.Future

val countEchoFilter = new Filter.TypeAgnostic {
  private[this] val echos = stats.counter("echo_calls")
  def toFilter[Req, Rep]: Filter[Req, Rep, Req, Rep] = new SimpleFilter[Req, Rep]{
    def apply(request: Req, service: Service[Req, Rep]): Future[Rep] = {
      echos.incr()
      service(request)
    }
  }
}

...

import com.foo.bar.thriftscala.EchoService.Echo
import com.twitter.finatra.thrift.Controller
import com.twitter.util.Future
import scala.util.control.NoStackTrace

class ExampleController extends Controller {

  handle(Echo).filtered(countEchoFilter) { args: Echo.Args =>
    if (args.msg == "clientError") {
      Future.exception(new Exception("client error") with NoStackTrace)
    } else {
      Future.value(args.msg)
    }
  }
}

Note that you can chain handle(ThriftMethod)#filtered calls arbitrarily deep.

Typed Filters

If you’d like to specify a typed Filter, use the handle(ThriftMethod)#withService Function and apply your typed Filter[-ReqIn, +RepOut, +ReqOut, -RepIn] to your Service[-ReqOut, +RepIn] implementation.

import com.foo.bar.thriftscala.EchoService.Echo
import com.twitter.finagle.{Filter, Service, SimpleFilter}
import com.twitter.inject.Logging
import com.twitter.util.Future

val echoLoggingFilter = new Filter[Echo.Args, String, Echo.Args, String] with Logging {
  def apply(request: Echo.Args, service: Service[Echo.Args, String]): Future[String] = {
    info(s"Received request message: ${request.msg}")
    service(request)
  }
}

...

import com.foo.bar.thriftscala.EchoService.Echo
import com.twitter.finatra.thrift.Controller
import com.twitter.util.Future
import scala.util.control.NoStackTrace

class ExampleController extends Controller {

  val svc: Service[Echo.Args, String] = Service.mk { args: Echo.Args =>
    if (args.msg == "clientError") {
      Future.exception(new Exception("client error") with NoStackTrace)
    } else {
      Future.value(args.msg)
    }
  }

  handle(Echo).withService(new echoLoggingFilter.andThen(svc))
}

For more information on the handle(ThriftMethod) DSL of the Controller, see the documentation on Thrift Controllers.

Request Scope

Guice supports custom scopes in addition to the defined @Singleton, @SessionScoped, and @RequestScoped scopes. @RequestScoped is often used to allow injection of instances which can change depending on the incoming request (e.g. the currently authenticated User).

Finatra provides a custom implementation of the default Guice @RequestScoped functionality which works across Finagle non-blocking threads. The default Guice @RequestScoped implementation uses ThreadLocals which will not work within the context of a Twitter c.t.util.Future.

Note

Fields added to the Custom Request Scope will remain present in threads launched from a FuturePool.

Adding Classes into the Custom Request Scope

First add a dependency on com.twitter:inject-request-scope (finatra/inject/inject-request-scope).

Then define a module which mixes in the c.t.inject.requestscope.RequestScopeBinding trait. This trait defines #bindRequestScope[T] which will bind the given type to an “unseeded” Provider[T] of the type in the custom “FinagleRequestScope”. E.g.,

import com.twitter.inject.TwitterModule
import com.twitter.inject.requestscope.RequestScopeBinding

object UserModule extends TwitterModule with RequestScopeBinding {

  override def configure(): Unit = {
    bindRequestScope[User]
  }
}

Important

Remember to include this Module in your server’s list of Modules.

You must then “seed” this Provider[T] by obtaining an instance of the FinagleRequestScope and calling #seed[T](instance). For request scoping, you would generally do this in a TypeAgnostic Filter executed on the request path.

For example, to define a TypeAgnostic Filter which seeds a User into the “FinagleRequestScope”:

import com.twitter.finagle.{Filter, Service}
import com.twitter.inject.requestscope.FinagleRequestScope
import com.twitter.util.Future
import javax.inject.{Inject, Singleton}

@Singleton
class UserFilter @Inject()(
  finagleRequestScope: FinagleRequestScope
) extends Filter.TypeAgnostic {

  def toFilter[Req, Rep]: Filter[Req, Rep, Req, Rep] =
    new Filter[Req, Rep, Req, Rep] {
      def apply[Req, Rep](request: Req, service: Service[Req, Rep]): Future[Rep] = {
        val userId = parseUserId(request) // User-defined method to parse a "user id" from the request
        val user = User(userId)
        finagleRequestScope.seed[User](user)
        service(request)
      }
    }
}

Next, add the FinagleRequestScopeFilter.TypeAgnostic to your server _above_ the defined Filter which seeds the provided instance.

E.g., for the UserFilter defined above (shown with commonly recommended Filters in the recommended order):

import com.google.inject.Module
import com.twitter.finatra.thrift.exceptions.FinatraThriftExceptionMapper
import com.twitter.finatra.thrift.ThriftServer
import com.twitter.finatra.thrift.routing.ThriftRouter
import com.twitter.finatra.thrift.filters._
import com.twitter.finatra.thrift.modules.ClientIdAcceptlistModule

class Server extends ThriftServer {
  override def modules: Seq[Module] = Seq(ClientIdAcceptlistModule)

  override def configureThrift(router: ThriftRouter): Unit = {
    router
      .filter[LoggingMDCFilter]
      .filter[TraceIdMDCFilter]
      .filter[ThriftMDCFilter]
      .filter[AccessLoggingFilter]
      .filter[StatsFilter]
      .filter[ExceptionMappingFilter]
      .filter[ClientIdAcceptlistFilter]
      .filter[FinagleRequestScopeFilter.TypeAgnostic]
      .filter[UserFilter]
      .exceptionMapper[FinatraThriftExceptionMapper]
      .add[MyController]
    }
}

Lastly, wherever you need to access the Request scoped User inject a User or a Provider[User] type.

import com.twitter.finagle.Service
import com.twitter.finatra.thrift.Controller
import javax.inject.{Inject, Provider, Singleton}

@Singleton
class MyController @Inject()(
  dao: GroupsDAO,
  user: Provider[User])
  extends Controller with MyService.BaseServiceIface {

  val getUser: Service[GetUser.Args, GetUser.SuccessType] = handle(GetUser) { args: GetUser.Args =>
    "The incoming user has id " + user.get.id
  }
}

Note

The Provider[User] type must be used when injecting into a Singleton class.