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

com.comcast.money.akka.http.MoneyTrace.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2012 Comcast Cable Communications Management, LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.comcast.money.akka.http

import akka.http.scaladsl.model.HttpEntity.ChunkStreamPart
import akka.http.scaladsl.model.{ ContentTypes, HttpEntity, HttpRequest, HttpResponse }
import akka.http.scaladsl.server.Directives.{ complete, extractRequest }
import akka.http.scaladsl.server.Route
import akka.stream.scaladsl.{ Sink, Source }
import com.comcast.money.akka.{ MoneyExtension, SpanContextWithStack, TraceContext }
import com.comcast.money.api.{ Note, SpanId }
import com.comcast.money.core.{ Money, Tracer }

import scala.concurrent.{ ExecutionContext, Future }
import scala.util.{ Failure, Success }

/**
 * Traces a call to a instrumented Akka Http Routing DSL
 * The [[akka.http.scaladsl.server.Directive]] will execute application code and traces the time taken to fulfill a request.
 *
 * The directive will need to be the last directive used.
 *
 * The [[TraceContext]] created can be used to start and stop spans in other sections of the codebase
 */

object MoneyTrace {

  private val formatter = Money.Environment.formatter

  /**
   * Returns a Route to be used to complete a Akka Http Routing DSL
   *
   * Traces application code for a synchronous Request and Response
   *
   * NB: Only logic called by the function f will be traced within the requests Span
   *
   * Example:
   *
   * {{{
   *   get {
   *       pathSingleSlash {
   *         MoneyTrace {
   *           (tracedRequest: TracedRequest) => TracedResponse(HttpResponse(entity = "response"), tracedRequest.spanContext)
   *         }
   *       }
   *     }
   * }}}
   *
   * @param f              function from [[TracedRequest]] to [[TracedResponse]] should trigger all application code
   * @param moneyExtension [[akka.actor.ActorSystem]] extension to interact with [[com.comcast.money.core.Money]]
   * @param requestSKC     [[HttpRequestSpanKeyCreator]] to create a key for the Span generated in the Directive
   * @return [[Route]]
   */

  def apply(f: TracedRequest => TracedResponse)(implicit moneyExtension: MoneyExtension, requestSKC: HttpRequestSpanKeyCreator): Route =
    createDirective {
      (request, traceContext) =>

        val tracedResponse = f(TracedRequest(request, traceContext))

        traceContext.tracer.stopSpan(tracedResponse.isSuccess)
        complete(tracedResponse.response)
    }

  /**
   * returns a Route to complete a AkkaHttp routing DSL
   *
   * Traces asynchronous [[Future]] based application code
   *
   * NB: Only logic called by the function f will be traced within the requests Span
   *
   * {{{
   *   get {
   *       pathSingleSlash {
   *         MoneyTrace {
   *           (tracedRequest: TracedRequest) => Future(TracedResponse(HttpResponse(entity = "response"), tracedRequest.spanContext))
   *         }
   *       }
   *     }
   * }}}
   *
   * @param f                asynchronous application logic from TracedRequest to Future[TracedResponse]
   * @param moneyExtension [[akka.actor.ActorSystem]] extension to interact with [[com.comcast.money.core.Money]]
   * @param requestSKC     [[HttpRequestSpanKeyCreator]] to create a key for the Span generated in the Directive
   * @param executionContext the [[ExecutionContext]] for the [[Future]]
   * @return [[Route]]
   */

  def apply(f: TracedRequest => Future[TracedResponse])(implicit moneyExtension: MoneyExtension, requestSKC: HttpRequestSpanKeyCreator, executionContext: ExecutionContext): Route =
    createDirective {
      (request, traceContext) =>
        val eventualTracedResponse = f(TracedRequest(request, traceContext))

        eventualTracedResponse onComplete {
          case Success(tracedResponse) =>
            traceContext.tracer.stopSpan(tracedResponse.isSuccess)
          case Failure(e) =>
            traceContext.tracer.record(Note.of(e.getMessage, e.getStackTrace.toString))
            traceContext.tracer.stopSpan(result = false)
        }

        complete(eventualTracedResponse.map(_.response))
    }

  /**
   * Returns a Route to be used to complete a Akka Http Routing DSL
   *
   * Traces the completion of Stream being used to create a chunked Response to a client
   *
   * NB: Only logic called by the function f will be traced within the requests Span
   *
   * Example:
   *
   * {{{
   *   get {
   *       path("chunked") {
   *           MoneyTrace fromChunkedSource {
   *             (_: TracedRequest) => Source(List("a","b","c")).map(ChunkStreamPart(_))
   *           }
   *         }
   *     }
   * }}}
   *
   * @param f              application code that creates the [[Source]] to be used to complete the request
   * @param moneyExtension [[akka.actor.ActorSystem]] extension to interact with [[com.comcast.money.core.Money]]
   * @param requestSKC     [[HttpRequestSpanKeyCreator]] to create a key for the Span generated in the Directive
   * @return [[Route]]
   */

  def fromChunkedSource(f: TracedRequest => Source[ChunkStreamPart, _])(implicit moneyExtension: MoneyExtension, requestSKC: HttpRequestSpanKeyCreator): Route =
    createDirective {
      (request, traceContext) =>
        val source = f(TracedRequest(request, traceContext))

        val tracedSource = source alsoTo {
          Sink onComplete {
            case Success(_) => traceContext.tracer.stopSpan()
            case Failure(e) =>
              traceContext.tracer.record(Note.of(e.getMessage, e.getStackTrace.toString))
              traceContext.tracer.stopSpan(result = false)
          }
        }

        complete(HttpResponse(entity = HttpEntity.Chunked(ContentTypes.`text/plain(UTF-8)`, tracedSource)))
    }

  /**
   * INTERNAL API
   */

  /**
   * Returns [[Route]] for AkkaHttp Routing DSL
   *
   * Setup for tracing a [[HttpRequest]]
   *
   * Constructs a fresh [[TraceContext]] and builds a [[Tracer]] with the [[TraceContext]]
   *
   * The request is then checked for a existing [[SpanId]] if it exists a [[com.comcast.money.api.Span]]
   * is created from it and added to the [[TraceContext]] otherwise a fresh Span is started
   * and added to the [[TraceContext]]
   *
   * @param toRoute        the function that completes the [[HttpRequest]]
   * @param moneyExtension [[akka.actor.ActorSystem]] extension to interact with [[com.comcast.money.core.Money]]
   * @param requestSKC     [[HttpRequestSpanKeyCreator]] to create a key for the Span generated in the Directive
   * @return Route completing Routing DSL
   */

  private def createDirective(toRoute: (HttpRequest, TraceContext) => Route)(implicit moneyExtension: MoneyExtension, requestSKC: HttpRequestSpanKeyCreator): Route =
    extractRequest {
      request =>
        val traceContext: TraceContext = TraceContext(new SpanContextWithStack)

        maybeExtractHeaderSpanId(request) match {
          case Some(spanId) => traceContext.spanContext.push(traceContext.tracer.spanFactory.newSpan(spanId, requestSKC.httpRequestToKey(request)))
          case None => traceContext.tracer.startSpan(requestSKC.httpRequestToKey(request))
        }

        toRoute(request, traceContext)
    }

  /**
   * Returns a [[Option]] of [[SpanId]]
   *
   * returns [[Some]] when there is a [[SpanId]] present in the [[HttpRequest]] headers
   * returns [[None]] when there is not a [[SpanId]] present in the [[HttpRequest]] headers
   *
   * @param request [[HttpRequest]] that may contain a Money header
   * @return Option[SpanId]
   */

  private def maybeExtractHeaderSpanId(request: HttpRequest): Option[SpanId] = {
    val headerMap = request.headers.map {
      header => header.name -> header.value
    }
      .toMap

    formatter.fromHttpHeaders(headerMap.keys, key => headerMap.getOrElse(key, null))
  }

}

case class TracedRequest(request: HttpRequest, traceContext: TraceContext)

case class TracedResponse(response: HttpResponse, isSuccess: Boolean = true)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy