Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#476 Allow configuring maximum length of error messages in email notifications #477

Merged
merged 3 commits into from
Aug 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pramen/core/src/main/resources/reference.conf
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ pramen {
# one transformer are different from partitions produced by other transformers.
# You can set it to false to have a more strict setup and disallow this behavior.
enable.multiple.jobs.per.output.table = true

# Limits on certain elements of notiications (email etc)
notifications {
# The maximum length in characters of the Reason field in the completed tasks table.
reason.max.length = 1024
# The maximum length of errors and exceptions in the notification body. The default value is selected
# so that Pramen can handle at least 100 exceptions in a single email notification.
exception.max.length = 65536
}
}

pramen.py {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,20 @@

package za.co.absa.pramen.core.notify.message

import com.typesafe.config.Config
import za.co.absa.pramen.api.notification.NotificationEntry.Paragraph
import za.co.absa.pramen.api.notification.{Style, TableHeader, TextElement}
import za.co.absa.pramen.core.exceptions.OsSignalException
import za.co.absa.pramen.core.utils.ResourceUtils
import za.co.absa.pramen.core.notify.pipeline.PipelineNotificationBuilderHtml.NOTIFICATION_EXCEPTION_MAX_LENGTH_KEY
import za.co.absa.pramen.core.utils.{ConfigUtils, ResourceUtils}
import za.co.absa.pramen.core.utils.StringUtils.{renderThreadDumps, renderThrowable}

import scala.collection.mutable.ListBuffer
import scala.compat.Platform.EOL

class MessageBuilderHtml extends MessageBuilder {
class MessageBuilderHtml(conf: Config) extends MessageBuilder {
private val style = ResourceUtils.getResourceString("/email_template/style.css")
private val maxExceptionLength = ConfigUtils.getOptionInt(conf, NOTIFICATION_EXCEPTION_MAX_LENGTH_KEY)
private val body = new ListBuffer[String]

override def withParagraph(text: Seq[TextElement]): MessageBuilderHtml = {
Expand Down Expand Up @@ -73,7 +76,7 @@ class MessageBuilderHtml extends MessageBuilder {
renderThreadDumps(signalException.threadStackTraces)
case other =>
withParagraph(Seq(TextElement(description, Style.Exception)))
renderThrowable(other)
renderThrowable(other, maximumLength = maxExceptionLength)
}

withUnformattedText(rendered)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ object PipelineNotificationBuilderHtml {
val MIN_RPS_JOB_DURATION_SECONDS = 60
val MIN_RPS_RECORDS = 1000
val MIN_MEGABYTES = 10
val NOTIFICATION_REASON_MAX_LENGTH_KEY = "pramen.notifications.reason.max.length"
val NOTIFICATION_EXCEPTION_MAX_LENGTH_KEY = "pramen.notifications.exception.max.length"
}

class PipelineNotificationBuilderHtml(implicit conf: Config) extends PipelineNotificationBuilder {
import PipelineNotificationBuilderHtml._

private val log = LoggerFactory.getLogger(this.getClass)

private val timestampFmt: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm Z")
Expand All @@ -52,6 +56,9 @@ class PipelineNotificationBuilderHtml(implicit conf: Config) extends PipelineNot
private var minRps = 0
private var goodRps = Int.MaxValue

private val maxReasonLength = ConfigUtils.getOptionInt(conf, NOTIFICATION_REASON_MAX_LENGTH_KEY)
private val maxExceptionLength = ConfigUtils.getOptionInt(conf, NOTIFICATION_EXCEPTION_MAX_LENGTH_KEY)

var appException: Option[Throwable] = None
var warningFlag: Boolean = false
var appName: String = "Unspecified Job"
Expand Down Expand Up @@ -131,7 +138,7 @@ class PipelineNotificationBuilderHtml(implicit conf: Config) extends PipelineNot
}

def renderBody(): String = {
val builder = new MessageBuilderHtml
val builder = new MessageBuilderHtml(conf)

renderHeader(builder)

Expand Down Expand Up @@ -290,9 +297,16 @@ class PipelineNotificationBuilderHtml(implicit conf: Config) extends PipelineNot
.withText(info.infoDate.toString, Style.Error)
)

val errorMessage = ex.getMessage

val errorMessageTruncated = maxReasonLength match {
case Some(maxLength) if errorMessage.length > maxLength => errorMessage.substring(0, maxLength) + "..."
case _ => errorMessage
}

paragraphBuilder
.withText(" has failed with an exception: ", Style.Exception)
.withText(ex.getMessage, Style.Error)
.withText(errorMessageTruncated, Style.Error)

builder.withParagraph(paragraphBuilder)
renderException(builder, ex)
Expand All @@ -311,7 +325,7 @@ class PipelineNotificationBuilderHtml(implicit conf: Config) extends PipelineNot
val stderrMsg = if (stderr.isEmpty) "" else s"""Last <b>stderr</b> lines:\n${stderr.mkString("", EOL, EOL)}"""
s"$msg\n$stdoutMsg\n$stderrMsg"
case ex: Throwable =>
renderThrowable(ex)
renderThrowable(ex, maximumLength = maxExceptionLength)
}

builder.withUnformattedText(text)
Expand Down Expand Up @@ -599,7 +613,7 @@ class PipelineNotificationBuilderHtml(implicit conf: Config) extends PipelineNot
}

private[core] def getFailureReason(task: TaskResult): String = {
task.runStatus.getReason match {
val reason = task.runStatus.getReason match {
case Some(reason) => reason
case None =>
if (task.dependencyWarnings.isEmpty) {
Expand All @@ -609,6 +623,11 @@ class PipelineNotificationBuilderHtml(implicit conf: Config) extends PipelineNot
s"Optional dependencies failed for: $tables"
}
}

maxReasonLength match {
case Some(maxLength) if reason.length > maxLength => reason.substring(0, maxLength) + "..."
case _ => reason
}
}

private[core] def getFinishTime(task: TaskResult): String = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,14 +169,19 @@ object StringUtils {
}

/** Renders an exception as a string */
def renderThrowable(ex: Throwable, level: Int = 1): String = {
def renderThrowable(ex: Throwable, level: Int = 1, maximumLength: Option[Int] = None): String = {
val prefix = " " * (level * 2)
val base = s"""${ex.toString}\n${ex.getStackTrace.map(s => s"$prefix$s").mkString("", EOL, EOL)}"""
val cause = Option(ex.getCause) match {
case Some(c) if level < 6 => s"\n${prefix}Caused by " + renderThrowable(c, level + 1)
case _ => ""
}
base + cause
val fullText = base + cause

maximumLength match {
case Some(len) if fullText.length > len => fullText.substring(0, len) + "..."
case _ => fullText
}
}

def renderThreadDumps(threadStackTraces: Seq[ThreadStackTrace]): String = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package za.co.absa.pramen.core.tests.notify.message

import com.typesafe.config.ConfigFactory
import org.scalatest.wordspec.AnyWordSpec
import za.co.absa.pramen.api.notification.NotificationEntry.Paragraph
import za.co.absa.pramen.api.notification.{Style, TableHeader, TextElement}
Expand All @@ -28,7 +29,7 @@ class MessageBuilderHtmlSuite extends AnyWordSpec with TextComparisonFixture {
"render an email with the requested elements" in {
val expected = ResourceUtils.getResourceString("/test/notify/expectedMessage.dat")

val builder = new MessageBuilderHtml
val builder = new MessageBuilderHtml(ConfigFactory.empty())
val tb = new TableBuilderHtml

val actual = builder
Expand Down
Loading
Loading