Je dois enregistrer les demandes des clients akka http ainsi que leurs réponses. Bien qu'il semble y avoir un indice d'API pour consigner ces demandes, il n'y a pas de documentation claire sur la façon dont cela devrait être fait. Mon approche a été de créer une demande enregistrée qui enveloppe de manière transparente Http().singleRequest(req)
comme suit:
def loggedRequest(req: HttpRequest)
(implicit system: ActorSystem, ctx: ExecutionContext, m: Materializer): Future[HttpResponse] = {
Http().singleRequest(req).map { resp ⇒
Unmarshal(resp.entity).to[String].foreach{s ⇒
system.log.info(req.toString)
system.log.info(resp.toString + "\n" + s)
}
resp
}
}
Malheureusement, je dois saisir l’avenir soit par l’intermédiaire du marécage, soit en demandant simplement resp.entity.dataBytes
afin de récupérer le corps de la réponse. J'obtiens la journalisation mais la promesse est accomplie et je ne peux plus démasquer l'entité aux données réelles. Une solution de travail consignerait la demande et la réponse et passerait ce cas de test sans IllegalStateException
avec "Promise déjà terminée":
describe("Logged rest requests") {
it("deliver typed responses") {
val foo = Rest.loggedRequest(Get(s"http://127.0.0.1:9000/some/path"))
val resp = foo.futureValue(patience)
resp.status shouldBe StatusCodes.OK
val res = Unmarshal(resp.entity).to[MyClass].futureValue
}
}
Les idées sont les bienvenues.
L'une des solutions que j'ai trouvées consiste à utiliser:
import akka.http.scaladsl.server.directives.DebuggingDirectives
val clientRouteLogged = DebuggingDirectives.logRequestResult("Client ReST", Logging.InfoLevel)(clientRoute)
Http().bindAndHandle(clientRouteLogged, interface, port)
Ce qui peut facilement enregistrer la demande et aboutir au format brut (octets). Le problème est que ces journaux sont complètement illisibles. Et c'est ici que c'est devenu compliqué.
Voici mon exemple qui encode l'entité de la demande/réponse et l'écrit dans l'enregistreur.
Vous pouvez transmettre une fonction à:
DebuggingDirectives.logRequestResult
def logRequestResult(magnet: LoggingMagnet[HttpRequest ⇒ RouteResult ⇒ Unit])
C'est une fonction écrite en utilisant modèle d'aimant :
LoggingMagnet[HttpRequest ⇒ RouteResult ⇒ Unit]
Où:
LoggingMagnet[T](f: LoggingAdapter ⇒ T)
Grâce à cela, nous avons accès à toutes les pièces dont nous avons besoin pour enregistrer la demande et le résultat. Nous avons LoggingAdapter, HttpRequest et RouteResult
Dans mon cas, j'ai créé une fonction interne. Je ne veux plus transmettre tous les paramètres.
def logRequestResult(level: LogLevel, route: Route)
(implicit m: Materializer, ex: ExecutionContext) = {
def myLoggingFunction(logger: LoggingAdapter)(req: HttpRequest)(res: Any): Unit = {
val entry = res match {
case Complete(resp) =>
entityAsString(resp.entity).map(data ⇒ LogEntry(s"${req.method} ${req.uri}: ${resp.status} \n entity: $data", level))
case other =>
Future.successful(LogEntry(s"$other", level))
}
entry.map(_.logTo(logger))
}
DebuggingDirectives.logRequestResult(LoggingMagnet(log => myLoggingFunction(log)))(route)
}
La partie la plus importante est la dernière ligne où j'ai mis myLoggingFunction dans logRequestResult.
La fonction appelée myLoggingFunction, simple correspondait au résultat du calcul du serveur et créait un LogEntry basé sur celui-ci.
La dernière chose est une méthode qui permet de décoder l'entité résultante d'un flux.
def entityAsString(entity: HttpEntity)
(implicit m: Materializer, ex: ExecutionContext): Future[String] = {
entity.dataBytes
.map(_.decodeString(entity.contentType().charset().value))
.runWith(Sink.head)
}
La méthode peut être facilement ajoutée à n'importe quel itinéraire akka-http.
val myLoggedRoute = logRequestResult(Logging.InfoLevel, clinetRoute)
Http().bindAndHandle(myLoggedRoute, interface, port)
Pour une autre solution, ce code enregistre l'IP de la demande et associe un nombre aléatoire à chaque demande et réponse afin qu'ils puissent être associés dans les journaux. Il enregistre également le temps de réponse.
Étant donné que la demande peut prendre un certain temps à traiter et peut échouer, je voulais voir la demande immédiatement et voir la réponse si et quand elle revient.
RequestFields
n'est que les données dont je me soucie de la demande. Il y a beaucoup de bruit par défaut.
val logRequestResponse: Directive0 =
extractRequestContext flatMap { ctx =>
extractClientIP flatMap { ip =>
val id = scala.math.abs(Rand.nextLong).toString
onSuccess(RequestFields.fromIdIpAndRequest(id, ip, ctx.request)) flatMap { req =>
logger.info("request", req.asJson)
val i = Instant.now()
mapRouteResultWith { result =>
Result.fromIdStartTimeAndRouteResult(id, i, result) map { res =>
logger.info("response", res.asJson)
result
}
}
}
}
}
Ma solution complète, inspirée de @seanmcl
trait TraceDirectives extends LazyLogging {
private val counter: AtomicLong = new AtomicLong(0)
private def log: Directive0 = count flatMap { requestId =>
mapInnerRoute(addLoggingToRoute(requestId, _))
}
private def count: Directive1[Long] = Directive { innerRouteSupplier =>
ctx =>
innerRouteSupplier(Tuple1(counter.incrementAndGet()))(ctx)
}
private def addLoggingToRoute(requestId: Long, innerRoute: Route): Route = {
ctx => {
val requestStopwatch = Stopwatch.createStarted()
extractClientIP { ip =>
logger.info("Http request, id: {}, uri: {}, forwarded ip: {}", requestId, ctx.request.uri, ip)
mapResponse(httpResponse => {
logger.info("Http response, id: {}, code: {}, time: {}", requestId, httpResponse.status.intValue(), requestStopwatch.toString)
httpResponse
})(innerRoute)
}(ctx)
}
}
}
object TraceDirectives extends TraceDirectives