5

In our code we create many "finagle pipelines" like so:

val f1 = new Filter[A,B,C,D](...)
val f2 = new SimpleFilter[C,D](...)
val f3 = new Filter[C,D,E,F](...)
val s = new Service[E,F](...)

val pipeline: Service[A,B] = f1 andThen f2 andThen f3 andThen s

I would now like the ability to "insert" loggers anywhere in such a chain. The logger would only log the fact that a request came in and a response was received. Something like this:

class LoggerFilter[Req, Resp](customLog: String) extends SimpleFilter[Req, Resp] with LazyLogging{
  override def apply(request: Req, service: Service[Req, Resp]): Future[Resp] = {
    logger.info(s"$customLog => Request: ${request.getClass.getName} -> ${service.toString}")
    service(request).map{resp =>
      logger.info(s"$customLog => Response: ${resp.getClass.getName} -> ${request.getClass.getName}")
      resp
    }
  }
}

With this approach we have to keep declaring multiple loggers so that the types can align correctly and then we insert at the "right location".

val logger1 = new LoggerFilter[A,B]("A->B Logger")
val logger2 = new LoggerFilter[C,D]("C->D Logger")
val logger3 = new LoggerFilter[E,F]("E->F Logger")

val pipeline = logger1 andThen f1 andThen f2 andThen logger2 andThen f3 andThen logger3 andThen s

Is there a way this can be avoided? Is it possible to just have a single logger that can infer the Req/Resp types automagically and be "insertable anywhere" in the chain?

E.g.:

val logger = getTypeAgnosticLogger // What's the implementation?

val pipeline = logger andThen f1 andThen f2 andThen logger andThen f3 andThen logger andThen s

// Is this possible - params for logger to print?
val pipeline = logger("f1") andThen f1 andThen f2 andThen logger("f3") andThen f3 andThen logger("s") andThen s
Mario Galic
  • 47,285
  • 6
  • 56
  • 98
PhD
  • 11,202
  • 14
  • 64
  • 112
  • Can you try to define the helper method like `def logger[A,B](param: String) = new LoggerFilter[A,B](param)` instead of `val`? – Krzysztof Atłasik Jun 10 '19 at 20:09
  • Not sure I understand how it'd help - won't we still have to spell out the types/params in each of the method calls? – PhD Jun 10 '19 at 20:19
  • Probably it should be inferred by the compiler, but I'm not sure, it was just a blind shot. At least it should be inferred if the logger is between chain elements with defined types if it's at the beginning or the end of pipeline, you'd have to specify types explicitly. – Krzysztof Atłasik Jun 10 '19 at 20:23
  • What's the type of `pipeline`? – Krzysztof Atłasik Jun 10 '19 at 20:28
  • @KrzysztofAtłasik - updated. It'll be `Service[A,B]` – PhD Jun 10 '19 at 20:34

1 Answers1

5

I couldn't work out a way of defining an automagic logger. My first idea was relying on compiler type inference as per @Krzysztof's suggestion, but that ended up with a type error due to a logger with parameters [Nothing, Nothing] so it seems compiler inference triggered too early there. Given that, I'm not sure it's possible in the way you describe without explicit types on each logger.

However, what you can do to get something very similar is to extend the Filter and Service classes with a withLogging method that attaches the Logger prior to running. At that point, you've got enough type information to construct the logger explicitly, and it also lets you pass in your param:

implicit class FilterLogging[ReqIn, RepOut, ReqOut, RepIn](filter: Filter[ReqIn, RepOut, ReqOut, RepIn]) {

  def withLogging(param: String) : Filter[ReqIn, RepOut, ReqOut, RepIn] = new LoggerFilter[ReqIn, RepOut](param).andThen(filter)
}

implicit class ServiceLogging[Req, Rep](service: Service[Req, Rep]) {

  def withLogging(param: String) : Service[Req, Rep] = new LoggerFilter[Req, Rep](param).andThen(service)
}

val pipeline = f1.withLogging("f1") andThen f2 andThen f3.withLogging("f3") andThen service.withLogging("s")
PhD
  • 11,202
  • 14
  • 64
  • 112
Astrid
  • 1,808
  • 12
  • 24
  • Interesting. But why an implicit class though? – PhD Jun 18 '19 at 18:02
  • 1
    The implicit class is used to extent `Filter` with `withLogging` - this is the standard scala way of monkey-patching new methods onto existing classes. This way, it doesn't rely on you using specific custom `Filter` or `Service` classes, any that extend the base Finagle type will work with logging. – Astrid Jun 18 '19 at 18:29
  • That makes sense. I like this solution in lieu of the "impossible" one that I advocated for. It's not intrusive and can be "dropped in" on a pipeline/filter/service as needed and is explicit that it's being logged. – PhD Jun 18 '19 at 21:58