From 5ec270aa98b806f32338fa25357abdf45dd0625b Mon Sep 17 00:00:00 2001 From: Jakob Odersky Date: Wed, 22 Aug 2018 12:51:36 -0700 Subject: Trait-based initialization and other utilities Adds the concept of a 'platform', a centralized place in which environment-specific information will be managed, and provides common initialization logic for most "standard" apps. As part of the common initialization, other parts of core have also been reworked: - HTTP-related unmarshallers and path matchers have been factored out from core.json to a new core.rest.directives package (core.json extends those unmarshallers and matchers for backwards compatibility) - CORS handling has also been moved to a dedicated utility trait - Some custom headers have been moved from raw headers to typed ones in core.rest.headers - The concept of a "reporter" has been introduced. A reporter is a context-aware combination of tracing and logging. It is intended to issue diagnostic messages that can be traced across service boundaries. Closes #192 Closes #195 --- .../scala/xyz/driver/core/reporting/Reporter.scala | 164 +++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 src/main/scala/xyz/driver/core/reporting/Reporter.scala (limited to 'src/main/scala/xyz/driver/core/reporting/Reporter.scala') diff --git a/src/main/scala/xyz/driver/core/reporting/Reporter.scala b/src/main/scala/xyz/driver/core/reporting/Reporter.scala new file mode 100644 index 0000000..2425044 --- /dev/null +++ b/src/main/scala/xyz/driver/core/reporting/Reporter.scala @@ -0,0 +1,164 @@ +package xyz.driver.core.reporting + +import com.typesafe.scalalogging.Logger +import org.slf4j.helpers.NOPLogger +import xyz.driver.core.reporting.Reporter.CausalRelation + +import scala.concurrent.Future + +/** Context-aware diagnostic utility for distributed systems, combining logging and tracing. + * + * Diagnostic messages (i.e. logs) are a vital tool for monitoring applications. Tying such messages to an + * execution context, such as a stack trace, simplifies debugging greatly by giving insight to the chains of events + * that led to a particular message. In synchronous systems, execution contexts can easily be determined by an + * external observer, and, as such, do not need to be propagated explicitly to sub-components (e.g. a stack trace on + * the JVM shows all relevant information). In asynchronous systems and especially distributed systems however, + * execution contexts are not easily determined by an external observer and hence need to be explcictly passed across + * service boundaries. + * + * This reporter provides tracing and logging utilities that explicitly require references to execution contexts + * (called [[SpanContext]]s here) intended to be passed across service boundaries. It embraces Scala's + * implicit-parameter-as-a-context paradigm. + * + * Tracing is intended to be compatible with the + * [[https://github.com/opentracing/specification/blob/master/specification.md OpenTrace specification]], and hence its + * guidelines on naming and tagging apply to methods provided by this Reporter as well. + * + * Usage example: + * {{{ + * val reporter: Reporter = ??? + * object Repo { + * def getUserQuery(userId: String)(implicit ctx: SpanContext) = reporter.trace("query"){ implicit ctx => + * reporter.debug("Running query") + * // run query + * } + * } + * object Service { + * def getUser(userId: String)(implicit ctx: SpanContext) = reporter.trace("get_user"){ implicit ctx => + * reporter.debug("Getting user") + * Repo.getUserQuery(userId) + * } + * } + * reporter.traceRoot("static_get", Map("user" -> "john")) { implicit ctx => + * Service.getUser("john") + * } + * }}} + * + * Note that computing traces may be a more expensive operation than traditional logging frameworks provide (in terms + * of memory and processing). It should be used in interesting and actionable code paths. + * + * @define rootWarning Note: the idea of the reporting framework is to pass along references to traces as + * implicit parameters. This method should only be used for top-level traces when no parent + * traces are available. + */ +trait Reporter { + + def traceWithOptionalParent[A]( + name: String, + tags: Map[String, String], + parent: Option[(SpanContext, CausalRelation)])(op: SpanContext => A): A + def traceWithOptionalParentAsync[A]( + name: String, + tags: Map[String, String], + parent: Option[(SpanContext, CausalRelation)])(op: SpanContext => Future[A]): Future[A] + + /** Trace the execution of an operation, if no parent trace is available. + * + * $rootWarning + */ + def traceRoot[A](name: String, tags: Map[String, String])(op: SpanContext => A): A = + traceWithOptionalParent( + name, + tags, + None + )(op) + + /** Trace the execution of an asynchronous operation, if no parent trace is available. + * + * $rootWarning + * + * @see traceRoot + */ + def traceRootAsync[A](name: String, tags: Map[String, String])(op: SpanContext => Future[A]): Future[A] = + traceWithOptionalParentAsync( + name, + tags, + None + )(op) + + /** Trace the execution of an operation, in relation to a parent context. + * + * @param name The name of the operation. Note that this name should not be too specific. According to the + * OpenTrace RFC: "An operation name, a human-readable string which concisely represents the work done + * by the Span (for example, an RPC method name, a function name, or the name of a subtask or stage + * within a larger computation). The operation name should be the most general string that identifies a + * (statistically) interesting class of Span instances. That is, `"get_user"` is better than + * `"get_user/314159"`". + * @param tags Attributes associated with the traced event. Following the above example, if `"get_user"` is an + * operation name, a good tag would be `("account_id" -> 314159)`. + * @param relation Relation of the operation to its parent context. + * @param op The operation to be traced. The trace will complete once the operation returns. + * @param ctx Context of the parent trace. + * @tparam A Return type of the operation. + * @return The value of the child operation. + */ + def trace[A](name: String, tags: Map[String, String], relation: CausalRelation = CausalRelation.Child)( + op: /* implicit (gotta wait for Scala 3) */ SpanContext => A)(implicit ctx: SpanContext): A = + traceWithOptionalParent( + name, + tags, + Some(ctx -> relation) + )(op) + + /** Trace the operation of an asynchronous operation. + * + * Contrary to the synchronous version of this method, a trace is completed once the child operation completes + * (rather than returns). + * + * @see trace + */ + def traceAsync[A](name: String, tags: Map[String, String], relation: CausalRelation = CausalRelation.Child)( + op: /* implicit (gotta wait for Scala 3) */ SpanContext => Future[A])(implicit ctx: SpanContext): Future[A] = + traceWithOptionalParentAsync( + name, + tags, + Some(ctx -> relation) + )(op) + + /** Log a debug message. */ + def debug(message: String)(implicit ctx: SpanContext): Unit + + /** Log an informational message. */ + def info(message: String)(implicit ctx: SpanContext): Unit + + /** Log a warning message. */ + def warn(message: String)(implicit ctx: SpanContext): Unit + + /** Log a error message. */ + def error(message: String)(implicit ctx: SpanContext): Unit + + /** Log a error message with an associated throwable that caused the error condition. */ + def error(message: String, reason: Throwable)(implicit ctx: SpanContext): Unit + +} + +object Reporter { + + val NoReporter: Reporter = new NoTraceReporter(Logger.apply(NOPLogger.NOP_LOGGER)) + + /** A relation in cause. + * + * Corresponds to + * [[https://github.com/opentracing/specification/blob/master/specification.md#references-between-spans OpenTrace references between spans]] + */ + sealed trait CausalRelation + object CausalRelation { + + /** One event is the child of another. The parent completes once the child is complete. */ + case object Child extends CausalRelation + + /** One event follows from another, not necessarily being the parent. */ + case object Follows extends CausalRelation + } + +} -- cgit v1.2.3