package net.shrine.problem import java.net.InetAddress import java.util.Date import java.util.concurrent.Executors import net.shrine.log.Loggable import net.shrine.serialization.{XmlMarshaller, XmlUnmarshaller} import net.shrine.slick.NeedsWarmUp import scala.concurrent.ExecutionContext.Implicits.global import scala.concurrent.{ExecutionContext, Future, Promise} import scala.util.Try import scala.xml.{Elem, Node, NodeSeq} /** * Describes what information we have about a problem at the site in code where we discover it. * * @author david * @since 8/6/15 */ trait Problem { def summary:String def problemName = getClass.getName def throwable:Option[Throwable] = None def stamp:Stamp def description:String def exceptionXml(exception:Option[Throwable]): Option[Elem] = { exception.map{x => {x.getClass.getName} {x.getMessage} {x.getStackTrace.map(line => {line})}{exceptionXml(Option(x.getCause)).getOrElse("")} }} def throwableDetail: Option[Elem] = exceptionXml(throwable) def detailsXml: NodeSeq = NodeSeq.fromSeq(
{throwableDetail.getOrElse("")}
) def toDigest:ProblemDigest = ProblemDigest(problemName,stamp.pretty,summary,description,detailsXml,stamp.time) /** * Temporary replacement for onCreate, which will be released come Scala 2.13 * TODO: remove when Scala 2.13 releases */ def hackToHandleAfterInitialization(handler:ProblemHandler):Future[Unit] = { import scala.concurrent.blocking Future { var continue = true while (continue) { try { blocking(synchronized(handler.handleProblem(this))) continue = false } catch { case un:UninitializedFieldError => Thread.sleep(5) continue = true } } Unit } } } case class ProblemDigest(codec: String, stampText: String, summary: String, description: String, detailsXml: NodeSeq, epoch: Long) extends XmlMarshaller { override def toXml: Node = { {codec} {stampText} {summary} {description} {epoch} {detailsXml} } /** * Ignores detailXml. equals with scala.xml is impossible. See http://www.scala-lang.org/api/2.10.3/index.html#scala.xml.Equality$ */ override def equals(other: Any): Boolean = other match { case that: ProblemDigest => (that canEqual this) && codec == that.codec && stampText == that.stampText && summary == that.summary && description == that.description && epoch == that.epoch case _ => false } /** * Ignores detailXml */ override def hashCode: Int = { val prime = 67 codec.hashCode + prime * (stampText.hashCode + prime *(summary.hashCode + prime * (description.hashCode + prime * epoch.hashCode()))) } } object ProblemDigest extends XmlUnmarshaller[ProblemDigest] with Loggable { override def fromXml(xml: NodeSeq): ProblemDigest = { val problemNode = xml \ "problem" require(problemNode.nonEmpty,s"No problem tag in $xml") def extractText(tagName:String) = (problemNode \ tagName).text val codec = extractText("codec") val stampText = extractText("stamp") val summary = extractText("summary") val description = extractText("description") val detailsXml: NodeSeq = problemNode \ "details" val epoch = try { extractText("epoch").toLong } catch { case nx:NumberFormatException => error(s"While parsing xml representing a ProblemDigest, the epoch could not be parsed into a long", nx) 0 } ProblemDigest(codec,stampText,summary,description,detailsXml,epoch) } } case class Stamp(host:InetAddress,time:Long,source:ProblemSources.ProblemSource) { def pretty = s"${new Date(time)} on ${host.getHostName} ${source.pretty}" } object Stamp { //TODO: val dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS")? //TODO: Currently the stamp text is locale specific, which can change depending on the jre/computer running it... def apply(source:ProblemSources.ProblemSource, timer: => Long): Stamp = Stamp(InetAddress.getLocalHost, timer, source) } abstract class AbstractProblem(source:ProblemSources.ProblemSource) extends Problem { def timer = System.currentTimeMillis override val stamp = Stamp(source, timer) private val config = ProblemConfigSource.config.getConfig("shrine.problem") hackToHandleAfterInitialization(ProblemConfigSource.getObject("problemHandler", config)) } trait ProblemHandler extends NeedsWarmUp { def handleProblem(problem:Problem) } /** * An example problem handler */ object LoggingProblemHandler extends ProblemHandler with Loggable { override def handleProblem(problem: Problem): Unit = { problem.throwable.fold(error(problem.toString))(throwable => error(problem.toString,throwable) ) } override def warmUp(): Unit = Unit } object DatabaseProblemHandler extends ProblemHandler with Loggable { override def handleProblem(problem: Problem): Unit = { Problems.DatabaseConnector.insertProblem(problem.toDigest) } override def warmUp(): Unit = Problems.warmUp } /** * Mainly for testing, when you don't want problems to print a bunch * to stdout */ object NoOpProblemHandler extends ProblemHandler { override def handleProblem(problem: Problem): Unit = Unit override def warmUp(): Unit = Unit } object ProblemSources{ sealed trait ProblemSource { def pretty = getClass.getSimpleName.dropRight(1) } case object Adapter extends ProblemSource case object Commons extends ProblemSource case object Dsa extends ProblemSource case object Hub extends ProblemSource case object Qep extends ProblemSource case object Unknown extends ProblemSource def problemSources = Set(Adapter,Commons,Dsa,Hub,Qep,Unknown) } case class ProblemNotYetEncoded(internalSummary:String,t:Option[Throwable] = None) extends AbstractProblem(ProblemSources.Unknown){ override val summary = "An unanticipated problem encountered." override val throwable = { val rx = t.fold(new IllegalStateException(s"$summary"))( new IllegalStateException(s"$summary",_) ) rx.fillInStackTrace() Some(rx) } val reportedAtStackTrace = new IllegalStateException("Capture reporting stack trace.") override val description = "This problem is not yet classified in Shrine source code. Please report the details to the Shrine dev team." override val detailsXml: NodeSeq = NodeSeq.fromSeq(
{internalSummary} {throwableDetail.getOrElse("")}
) } object ProblemNotYetEncoded { def apply(summary:String,x:Throwable):ProblemNotYetEncoded = ProblemNotYetEncoded(summary,Some(x)) }