package xyz.driver.core package reporting 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 explicitly 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 { import 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 */ final def traceRoot[A](name: String, tags: Map[String, String] = Map.empty)(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 */ final def traceRootAsync[A](name: String, tags: Map[String, String] = Map.empty)( 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. */ final def trace[A]( name: String, tags: Map[String, String] = Map.empty, 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 */ final def traceAsync[A]( name: String, tags: Map[String, String] = Map.empty, 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 message. */ def log(severity: Severity, message: String, reason: Option[Throwable])(implicit ctx: SpanContext): Unit /** Log a debug message. */ final def debug(message: String)(implicit ctx: SpanContext): Unit = log(Severity.Debug, message, None) final def debug(message: String, reason: Throwable)(implicit ctx: SpanContext): Unit = log(Severity.Debug, message, Some(reason)) /** Log an informational message. */ final def info(message: String)(implicit ctx: SpanContext): Unit = log(Severity.Informational, message, None) final def info(message: String, reason: Throwable)(implicit ctx: SpanContext): Unit = log(Severity.Informational, message, Some(reason)) /** Log a warning message. */ final def warn(message: String)(implicit ctx: SpanContext): Unit = log(Severity.Warning, message, None) final def warn(message: String, reason: Throwable)(implicit ctx: SpanContext): Unit = log(Severity.Warning, message, Some(reason)) /** Log an error message. */ final def error(message: String)(implicit ctx: SpanContext): Unit = log(Severity.Error, message, None) final def error(message: String, reason: Throwable)(implicit ctx: SpanContext): Unit = log(Severity.Error, message, Some(reason)) } object Reporter { /** 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 } sealed trait Severity object Severity { case object Debug extends Severity case object Informational extends Severity case object Warning extends Severity case object Error extends Severity } }