mirror of
https://github.com/digital-asset/daml.git
synced 2024-11-05 03:56:26 +03:00
Handle misc error codes todos. [DPP-606] (#13248)
changelog_begin changelog_end
This commit is contained in:
parent
a8d55727c5
commit
ee6a5f9e0e
@ -51,7 +51,7 @@ object Main {
|
||||
"resolution",
|
||||
)(i =>
|
||||
(
|
||||
i.className,
|
||||
i.errorCodeClassName,
|
||||
i.category,
|
||||
i.hierarchicalGrouping.groupings,
|
||||
i.conveyance,
|
||||
|
@ -8,7 +8,7 @@ import com.daml.error.{ErrorClass, Explanation, Resolution}
|
||||
|
||||
/** Contains error presentation data to be used for documentation rendering on the website.
|
||||
*
|
||||
* @param className The error class name (see [[com.daml.error.ErrorCode]]).
|
||||
* @param errorCodeClassName The error class name (see [[com.daml.error.ErrorCode]]).
|
||||
* @param category The error code category (see [[com.daml.error.ErrorCategory]]).
|
||||
* @param hierarchicalGrouping The hierarchical code grouping
|
||||
* (see [[com.daml.error.ErrorClass]] and [[com.daml.error.ErrorGroup]]).
|
||||
@ -18,7 +18,7 @@ import com.daml.error.{ErrorClass, Explanation, Resolution}
|
||||
* @param resolution The suggested error resolution.
|
||||
*/
|
||||
case class ErrorCodeDocItem(
|
||||
className: String, // TODO error codes: Rename to `errorCodeName` or `errorCodeClassName` to prevent confusion
|
||||
errorCodeClassName: String,
|
||||
category: String,
|
||||
hierarchicalGrouping: ErrorClass,
|
||||
conveyance: Option[String],
|
||||
|
@ -52,7 +52,7 @@ object ErrorCodeDocumentationGenerator {
|
||||
.map { errorCode =>
|
||||
val annotations = parseErrorCodeAnnotations(errorCode)
|
||||
ErrorCodeDocItem(
|
||||
className = errorCode.getClass.getName,
|
||||
errorCodeClassName = errorCode.getClass.getName,
|
||||
category = simpleClassName(errorCode.category),
|
||||
hierarchicalGrouping = errorCode.parent,
|
||||
conveyance = errorCode.errorConveyanceDocString,
|
||||
|
@ -25,7 +25,7 @@ class ErrorCodeDocumentationGeneratorSpec extends AnyFlatSpec with Matchers {
|
||||
|
||||
val expectedErrorDocItems = Seq(
|
||||
ErrorCodeDocItem(
|
||||
className = SeriousError.getClass.getTypeName,
|
||||
errorCodeClassName = SeriousError.getClass.getTypeName,
|
||||
category = "SystemInternalAssumptionViolated",
|
||||
hierarchicalGrouping = ErrorClass(Nil),
|
||||
conveyance = Some(
|
||||
@ -38,7 +38,7 @@ class ErrorCodeDocumentationGeneratorSpec extends AnyFlatSpec with Matchers {
|
||||
resolution = Some(Resolution("Turn it off and on again.")),
|
||||
),
|
||||
ErrorCodeDocItem(
|
||||
className = DeprecatedError.getClass.getTypeName: @nowarn("cat=deprecation"),
|
||||
errorCodeClassName = DeprecatedError.getClass.getTypeName: @nowarn("cat=deprecation"),
|
||||
category = "SystemInternalAssumptionViolated",
|
||||
hierarchicalGrouping = ErrorClass(Nil),
|
||||
conveyance = Some(
|
||||
@ -52,7 +52,7 @@ class ErrorCodeDocumentationGeneratorSpec extends AnyFlatSpec with Matchers {
|
||||
resolution = Some(Resolution("Turn it off and on again.")),
|
||||
),
|
||||
ErrorCodeDocItem(
|
||||
className = NotSoSeriousError.getClass.getTypeName,
|
||||
errorCodeClassName = NotSoSeriousError.getClass.getTypeName,
|
||||
category = "TransientServerFailure",
|
||||
hierarchicalGrouping = ErrorClass(
|
||||
List(
|
||||
|
@ -30,12 +30,13 @@ trait BaseError extends LocationMixin {
|
||||
* throwableO = Some(throwable)
|
||||
* )
|
||||
* }
|
||||
*
|
||||
* NOTE: This throwable's details are not included the exception communicated to the gRPC clients
|
||||
* so if you want them communicated, you need to explicitly add them to the e.g. context map or cause string.
|
||||
*/
|
||||
def throwableO: Option[Throwable] = None
|
||||
|
||||
/** The context (declared fields) of this error
|
||||
*
|
||||
* At the moment, we'll figure them out using reflection.
|
||||
*/
|
||||
def context: Map[String, String] = Map()
|
||||
|
||||
|
@ -16,6 +16,22 @@ trait ContextualizedErrorLogger {
|
||||
def error(message: String, throwable: Throwable): Unit
|
||||
}
|
||||
|
||||
object ContextualizedErrorLogger {
|
||||
|
||||
/** Formats the context as a string for logging */
|
||||
protected[error] def formatContextAsString(contextMap: Map[String, String]): String = {
|
||||
contextMap
|
||||
.filter(_._2.nonEmpty)
|
||||
.toSeq
|
||||
.sortBy(_._1)
|
||||
.map { case (k, v) =>
|
||||
s"$k=$v"
|
||||
}
|
||||
.mkString(", ")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object NoLogging extends ContextualizedErrorLogger {
|
||||
override def properties: Map[String, String] = Map.empty
|
||||
override def correlationId: Option[String] = None
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
package com.daml.error
|
||||
|
||||
import com.daml.error.ErrorCode.formatContextAsString
|
||||
import com.daml.logging.entries.{LoggingKey, LoggingValue}
|
||||
import com.daml.logging.{ContextualizedLogger, LoggingContext, LoggingValueStringSerializer}
|
||||
import org.slf4j.event.Level
|
||||
@ -74,7 +73,7 @@ class DamlContextualizedErrorLogger(
|
||||
val mergedContext = err.context ++ err.location.map(("location", _)).toList.toMap ++ extra
|
||||
|
||||
LoggingContext.withEnrichedLoggingContext(
|
||||
"err-context" -> ("{" + formatContextAsString(mergedContext) + "}")
|
||||
"err-context" -> ("{" + ContextualizedErrorLogger.formatContextAsString(mergedContext) + "}")
|
||||
) { implicit loggingContext =>
|
||||
val message = errorCode.toMsg(err.cause, correlationId)
|
||||
(logLevel, err.throwableO) match {
|
||||
|
@ -77,7 +77,7 @@ object ErrorCategory {
|
||||
extends ErrorCategoryImpl(
|
||||
grpcCode = Some(Code.UNAVAILABLE),
|
||||
logLevel = Level.INFO,
|
||||
retryable = Some(ErrorCategoryRetry("load balancer", 1.second)),
|
||||
retryable = Some(ErrorCategoryRetry(1.second)),
|
||||
securitySensitive = false,
|
||||
asInt = 1,
|
||||
rank = 3,
|
||||
@ -99,7 +99,7 @@ object ErrorCategory {
|
||||
extends ErrorCategoryImpl(
|
||||
grpcCode = Some(Code.ABORTED),
|
||||
logLevel = Level.INFO,
|
||||
retryable = Some(ErrorCategoryRetry("application", 1.second)),
|
||||
retryable = Some(ErrorCategoryRetry(1.second)),
|
||||
securitySensitive = false,
|
||||
asInt = 2,
|
||||
rank = 3,
|
||||
@ -125,7 +125,7 @@ object ErrorCategory {
|
||||
extends ErrorCategoryImpl(
|
||||
grpcCode = Some(Code.DEADLINE_EXCEEDED),
|
||||
logLevel = Level.INFO,
|
||||
retryable = Some(ErrorCategoryRetry("application", 1.second)),
|
||||
retryable = Some(ErrorCategoryRetry(1.second)),
|
||||
securitySensitive = false,
|
||||
asInt = 3,
|
||||
rank = 3,
|
||||
@ -355,12 +355,9 @@ object ErrorCategory {
|
||||
implicit val orderingErrorType: Ordering[ErrorCategory] = Ordering.by[ErrorCategory, Int](_.rank)
|
||||
}
|
||||
|
||||
// TODO error codes: `who` is not used?
|
||||
/** Default retryability information
|
||||
*
|
||||
* Every error category has a default retryability classification.
|
||||
* An error code may adjust the retry duration.
|
||||
*
|
||||
* The `who` string allows to suggest where the retry should be done ideally.
|
||||
*/
|
||||
case class ErrorCategoryRetry(who: String, duration: Duration)
|
||||
case class ErrorCategoryRetry(duration: Duration)
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
package com.daml.error
|
||||
|
||||
import com.daml.error.ErrorCode.{ValidMetadataKeyRegex, truncateResourceForTransport}
|
||||
import com.daml.error.ErrorCode.MaxCauseLogLength
|
||||
import com.daml.error.definitions.DamlError
|
||||
import com.daml.error.utils.ErrorDetails
|
||||
import com.google.rpc.Status
|
||||
@ -60,8 +60,14 @@ abstract class ErrorCode(val id: String, val category: ErrorCategory)(implicit
|
||||
|
||||
/** @return message including error category id, error code id, correlation id and cause
|
||||
*/
|
||||
def toMsg(cause: => String, correlationId: Option[String]): String =
|
||||
s"${codeStr(correlationId)}: ${ErrorCode.truncateCause(cause)}"
|
||||
def toMsg(cause: => String, correlationId: Option[String]): String = {
|
||||
val truncatedCause =
|
||||
if (cause.length > MaxCauseLogLength)
|
||||
cause.take(MaxCauseLogLength) + "..."
|
||||
else
|
||||
cause
|
||||
s"${codeStr(correlationId)}: $truncatedCause"
|
||||
}
|
||||
|
||||
def asGrpcStatus(err: BaseError)(implicit loggingContext: ContextualizedErrorLogger): Status = {
|
||||
val statusInfo = getStatusInfo(err)(loggingContext)
|
||||
@ -95,7 +101,8 @@ abstract class ErrorCode(val id: String, val category: ErrorCategory)(implicit
|
||||
val resourceInfos =
|
||||
if (code.category.securitySensitive) Seq()
|
||||
else
|
||||
truncateResourceForTransport(err.resources)
|
||||
ErrorCode
|
||||
.truncateResourcesForTransport(err.resources)
|
||||
.map { case (resourceType, resourceValue) =>
|
||||
ErrorDetails.ResourceInfoDetail(
|
||||
typ = resourceType.asString,
|
||||
@ -138,7 +145,7 @@ abstract class ErrorCode(val id: String, val category: ErrorCategory)(implicit
|
||||
/** True if this error may appear on the API */
|
||||
protected def exposedViaApi: Boolean = category.grpcCode.nonEmpty
|
||||
|
||||
private def getStatusInfo(
|
||||
private[error] def getStatusInfo(
|
||||
err: BaseError
|
||||
)(implicit loggingContext: ContextualizedErrorLogger): ErrorCode.StatusInfo = {
|
||||
val correlationId = loggingContext.correlationId
|
||||
@ -152,7 +159,7 @@ abstract class ErrorCode(val id: String, val category: ErrorCategory)(implicit
|
||||
loggingContext.warn(s"Passing non-grpc error via grpc $id ")
|
||||
Code.INTERNAL
|
||||
}
|
||||
val contextMap = getTruncatedContext(err) + ("category" -> category.asInt.toString)
|
||||
val contextMap = ErrorCode.truncateContext(err) + ("category" -> category.asInt.toString)
|
||||
|
||||
ErrorCode.StatusInfo(
|
||||
grpcStatusCode = grpcStatusCode,
|
||||
@ -162,31 +169,6 @@ abstract class ErrorCode(val id: String, val category: ErrorCategory)(implicit
|
||||
)
|
||||
}
|
||||
|
||||
private def getTruncatedContext(
|
||||
error: BaseError
|
||||
)(implicit loggingContext: ContextualizedErrorLogger): Map[String, String] = {
|
||||
val raw: Seq[(String, String)] =
|
||||
(error.context ++ loggingContext.properties).toSeq.filter(_._2.nonEmpty).sortBy(_._2.length)
|
||||
val maxPerEntry = ErrorCode.MaxContentBytes / Math.max(1, raw.size)
|
||||
// truncate smart, starting with the smallest value strings such that likely only truncate the largest args
|
||||
raw
|
||||
.foldLeft((Map.empty[String, String], 0)) { case ((map, free), (k, v)) =>
|
||||
val adjustedKey = ValidMetadataKeyRegex.replaceAllIn(k, "").take(63)
|
||||
val maxSize = free + maxPerEntry - adjustedKey.length
|
||||
val truncatedValue = if (maxSize >= v.length || v.isEmpty) v else v.take(maxSize) + "..."
|
||||
// note that we silently discard empty context values and we automatically make the
|
||||
// key "gRPC compliant"
|
||||
if (v.isEmpty || adjustedKey.isEmpty) {
|
||||
(map, free + maxPerEntry)
|
||||
} else {
|
||||
// keep track of "free space" such that we can keep larger values around
|
||||
val newFree = free + maxPerEntry - adjustedKey.length - truncatedValue.length
|
||||
(map + (adjustedKey -> truncatedValue), newFree)
|
||||
}
|
||||
}
|
||||
._1
|
||||
}
|
||||
|
||||
/** The error conveyance doc string provides a statement about the form this error will be returned to the user */
|
||||
private[error] def errorConveyanceDocString: Option[String] = {
|
||||
val loggedAs = s"This error is logged with log-level $logLevel on the server side."
|
||||
@ -223,41 +205,49 @@ object ErrorCode {
|
||||
correlationId: Option[String],
|
||||
)
|
||||
|
||||
private def truncateContext(
|
||||
error: BaseError
|
||||
)(implicit loggingContext: ContextualizedErrorLogger): Map[String, String] = {
|
||||
val raw: Seq[(String, String)] =
|
||||
(error.context ++ loggingContext.properties).toSeq.filter(_._2.nonEmpty).sortBy(_._2.length)
|
||||
val maxPerEntry = ErrorCode.MaxContentBytes / Math.max(1, raw.size)
|
||||
// truncate smart, starting with the smallest value strings such that likely only truncate the largest args
|
||||
raw
|
||||
.foldLeft((Map.empty[String, String], 0)) { case ((map, free), (k, v)) =>
|
||||
val adjustedKey = ValidMetadataKeyRegex.replaceAllIn(k, "").take(63)
|
||||
val maxSize = free + maxPerEntry - adjustedKey.length
|
||||
val truncatedValue = if (maxSize >= v.length || v.isEmpty) v else v.take(maxSize) + "..."
|
||||
// Note that we silently discard empty context values and we automatically make the
|
||||
// key "gRPC compliant"
|
||||
if (v.isEmpty || adjustedKey.isEmpty) {
|
||||
(map, free + maxPerEntry)
|
||||
} else {
|
||||
// Keep track of "free space" such that we can keep larger values around
|
||||
val newFree = free + maxPerEntry - adjustedKey.length - truncatedValue.length
|
||||
(map + (adjustedKey -> truncatedValue), newFree)
|
||||
}
|
||||
}
|
||||
._1
|
||||
}
|
||||
|
||||
/** Truncate resource information such that we don't exceed a max error size */
|
||||
def truncateResourceForTransport(
|
||||
res: Seq[(ErrorResource, String)]
|
||||
def truncateResourcesForTransport(
|
||||
resources: Seq[(ErrorResource, String)]
|
||||
): Seq[(ErrorResource, String)] = {
|
||||
res
|
||||
resources
|
||||
.foldLeft((Seq.empty[(ErrorResource, String)], 0)) { case ((acc, spent), elem) =>
|
||||
val tot = elem._1.asString.length + elem._2.length
|
||||
val newSpent = tot + spent
|
||||
if (newSpent < ErrorCode.MaxContentBytes) {
|
||||
(acc :+ elem, newSpent)
|
||||
} else {
|
||||
// TODO error codes: Here we silently drop resource info.
|
||||
// Signal that it was truncated by logs or truncate only expensive fields?
|
||||
// Note we silently drop resource info.
|
||||
(acc, spent)
|
||||
}
|
||||
}
|
||||
._1
|
||||
}
|
||||
|
||||
/** Formats the context as a string for e.g. transport or file logging */
|
||||
def formatContextAsString(contextMap: Map[String, String]): String = {
|
||||
contextMap
|
||||
.filter(_._2.nonEmpty)
|
||||
.toSeq
|
||||
.sortBy(_._1)
|
||||
.map { case (k, v) =>
|
||||
s"$k=$v"
|
||||
}
|
||||
.mkString(", ")
|
||||
}
|
||||
|
||||
private[error] def truncateCause(cause: String): String =
|
||||
if (cause.length > MaxCauseLogLength) {
|
||||
cause.take(MaxCauseLogLength) + "..."
|
||||
} else cause
|
||||
}
|
||||
|
||||
// Use these annotations to add more information to the documentation for an error on the website
|
||||
|
@ -4,23 +4,9 @@
|
||||
package com.daml.error
|
||||
|
||||
abstract class ErrorGroup()(implicit parent: ErrorClass) {
|
||||
private val simpleClassName: String = getClass.getSimpleName.replace("$", "")
|
||||
val fullClassName: String = getClass.getName
|
||||
// Hit https://github.com/scala/bug/issues/5425?orig=1 here: we cannot use .getSimpleName in deeply nested objects
|
||||
// TODO error codes: Switch to using .getSimpleName when switching to JDK 9+
|
||||
implicit val errorClass: ErrorClass = resolveErrorClass()
|
||||
|
||||
private def resolveErrorClass(): ErrorClass = {
|
||||
val name = fullClassName
|
||||
.replace("$", ".")
|
||||
.split("\\.")
|
||||
.view
|
||||
.reverse
|
||||
.find(segment => segment.trim.nonEmpty)
|
||||
.getOrElse(
|
||||
throw new IllegalStateException(
|
||||
s"Could not parse full class name: '${fullClassName}' for the error class name"
|
||||
)
|
||||
)
|
||||
parent.extend(Grouping(docName = name, fullClassName = fullClassName))
|
||||
}
|
||||
implicit val errorClass: ErrorClass =
|
||||
parent.extend(Grouping(docName = simpleClassName, fullClassName = fullClassName))
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ object AuthorizationChecks extends LedgerApiErrors.AuthorizationChecks {
|
||||
loggingContext: ContextualizedErrorLogger
|
||||
) extends DamlErrorWithDefiniteAnswer("Stale stream authorization. Retry quickly.") {
|
||||
override def retryable: Option[ErrorCategoryRetry] = Some(
|
||||
ErrorCategoryRetry(who = "application", duration = 0.seconds)
|
||||
ErrorCategoryRetry(duration = 0.seconds)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
package com.daml.error.samples
|
||||
import scala.concurrent.duration._
|
||||
|
||||
import com.daml.error.definitions.{DamlError}
|
||||
import com.daml.error.definitions.DamlError
|
||||
|
||||
object DummmyServer {
|
||||
|
||||
@ -36,7 +36,7 @@ object DummmyServer {
|
||||
)
|
||||
|
||||
override def retryable: Option[ErrorCategoryRetry] = Some(
|
||||
ErrorCategoryRetry("me", 123.second + 456.milliseconds)
|
||||
ErrorCategoryRetry(123.second + 456.milliseconds)
|
||||
)
|
||||
|
||||
override def context: Map[String, String] = Map("foo" -> "bar")
|
||||
|
@ -94,11 +94,14 @@ trait ErrorsAssertions extends Matchers with OptionValues with AppendedClues {
|
||||
)
|
||||
}
|
||||
|
||||
/** @param verifyEmptyStackTrace - should be enabled for the server-side testing and disabled for the client side testing
|
||||
*/
|
||||
def assertError(
|
||||
actual: StatusRuntimeException,
|
||||
expectedStatusCode: Code,
|
||||
expectedMessage: String,
|
||||
expectedDetails: Seq[ErrorDetails.ErrorDetail],
|
||||
verifyEmptyStackTrace: Boolean = true,
|
||||
): Unit = {
|
||||
val actualStatus = StatusProto.fromThrowable(actual)
|
||||
val actualDetails = actualStatus.getDetailsList.asScala.toSeq
|
||||
@ -110,6 +113,14 @@ trait ErrorsAssertions extends Matchers with OptionValues with AppendedClues {
|
||||
ErrorDetails.from(actualDetails) should contain theSameElementsAs expectedDetails
|
||||
}
|
||||
}
|
||||
if (verifyEmptyStackTrace) {
|
||||
cp {
|
||||
Statement.discard {
|
||||
actual.getStackTrace.length shouldBe 0 withClue ("it should contain no stacktrace")
|
||||
}
|
||||
}
|
||||
}
|
||||
cp { Statement.discard { actual.getCause shouldBe null } }
|
||||
cp.reportAll()
|
||||
}
|
||||
|
||||
|
@ -6,14 +6,13 @@ package com.daml.error
|
||||
import org.scalatest.flatspec.AnyFlatSpec
|
||||
import org.scalatest.matchers.should.Matchers
|
||||
|
||||
class DamlContextualizedErrorLoggerSpec extends AnyFlatSpec with Matchers {
|
||||
|
||||
behavior of classOf[DamlContextualizedErrorLogger].getName
|
||||
class ContextualizedErrorLoggerSpec extends AnyFlatSpec with Matchers {
|
||||
|
||||
it should "sort entries by keys and skip empty values" in {
|
||||
val contextMap = Map("c" -> "C", "a" -> "A", "b" -> "B", "empty value" -> "")
|
||||
|
||||
val actual = ErrorCode.formatContextAsString(contextMap)
|
||||
val actual =
|
||||
ContextualizedErrorLogger.formatContextAsString(contextMap)
|
||||
|
||||
actual shouldBe "a=A, b=B, c=C"
|
||||
}
|
@ -7,6 +7,7 @@ import ch.qos.logback.classic.Level
|
||||
import com.daml.error.utils.ErrorDetails
|
||||
import com.daml.error.utils.testpackage.SeriousError
|
||||
import com.daml.logging.LoggingContext
|
||||
import com.daml.platform.testing.LogCollector.ThrowableEntry
|
||||
import com.daml.platform.testing.{LogCollector, LogCollectorAssertions}
|
||||
import com.google.rpc.Status
|
||||
import io.grpc.Status.Code
|
||||
@ -114,7 +115,7 @@ class ErrorCodeSpec
|
||||
override val cause: String = "cause123"
|
||||
|
||||
override def retryable: Option[ErrorCategoryRetry] = Some(
|
||||
ErrorCategoryRetry(who = "unused", duration = 123.seconds + 456.milliseconds)
|
||||
ErrorCategoryRetry(duration = 123.seconds + 456.milliseconds)
|
||||
)
|
||||
|
||||
override def resources: Seq[(ErrorResource, String)] =
|
||||
@ -132,6 +133,9 @@ class ErrorCodeSpec
|
||||
)
|
||||
|
||||
override def definiteAnswerO: Option[Boolean] = Some(false)
|
||||
|
||||
override def throwableO: Option[Throwable] =
|
||||
Some(new RuntimeException("runtimeException123"))
|
||||
}
|
||||
|
||||
val errorLoggerBig = DamlContextualizedErrorLogger.forClass(
|
||||
@ -175,15 +179,7 @@ class ErrorCodeSpec
|
||||
.addAllDetails(details.map(_.toRpcAny).asJava)
|
||||
.build()
|
||||
val testedError = TestedError()
|
||||
testedError.logWithContext(Map.empty)(errorLoggerBig)
|
||||
|
||||
assertSingleLogEntry(
|
||||
actual = LogCollector.readAsEntries[this.type, this.type],
|
||||
expectedLogLevel = Level.INFO,
|
||||
expectedMsg = "FOO_ERROR_CODE(8,123corre): cause123",
|
||||
expectedMarkerAsString =
|
||||
"""{loggingEntryKey: "loggingEntryValue", err-context: "{contextKey1=contextValue1, kkk????=keyWithInvalidCharacters, location=ErrorCodeSpec.scala:<line-number>}"}""",
|
||||
)
|
||||
assertStatus(
|
||||
actual = testedErrorCode.asGrpcStatus(testedError)(errorLoggerBig),
|
||||
expected = expectedStatus,
|
||||
@ -227,6 +223,12 @@ class ErrorCodeSpec
|
||||
expectedMsg = "FOO_ERROR_CODE_SECURITY_SENSITIVE(4,123corre): cause123",
|
||||
expectedMarkerAsString =
|
||||
"""{loggingEntryKey: "loggingEntryValue", err-context: "{contextKey1=contextValue1, kkk????=keyWithInvalidCharacters, location=ErrorCodeSpec.scala:<line-number>}"}""",
|
||||
expectedThrowableEntry = Some(
|
||||
ThrowableEntry(
|
||||
className = "java.lang.RuntimeException",
|
||||
message = "runtimeException123",
|
||||
)
|
||||
),
|
||||
)
|
||||
assertStatus(
|
||||
actual = testedErrorCode.asGrpcStatus(testedError)(errorLoggerBig),
|
||||
@ -258,8 +260,13 @@ class ErrorCodeSpec
|
||||
class FooErrorBig(override val code: ErrorCode) extends BaseError {
|
||||
override val cause: String = "cause123"
|
||||
|
||||
override def context: Map[String, String] =
|
||||
super.context ++ Map(
|
||||
("y" * ErrorCode.MaxContentBytes) -> ("y" * ErrorCode.MaxContentBytes)
|
||||
)
|
||||
|
||||
override def retryable: Option[ErrorCategoryRetry] = Some(
|
||||
ErrorCategoryRetry(who = "unused", duration = 123.seconds + 456.milliseconds)
|
||||
ErrorCategoryRetry(duration = 123.seconds + 456.milliseconds)
|
||||
)
|
||||
|
||||
override def resources: Seq[(ErrorResource, String)] =
|
||||
@ -297,8 +304,9 @@ class ErrorCodeSpec
|
||||
"category" -> testedErrorCode.category.asInt.toString,
|
||||
"definite_answer" -> "false",
|
||||
"loggingEntryKey" -> "'loggingEntryValue'",
|
||||
"loggingEntryValueTooBig" -> ("'" + "x" * 1854 + "..."),
|
||||
"loggingEntryValueTooBig" -> ("'" + "x" * 473 + "..."),
|
||||
("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") -> "'loggingEntryKeyTooBig'",
|
||||
("yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy") -> ("y" * 1317 + "..."),
|
||||
),
|
||||
),
|
||||
requestInfo,
|
||||
|
@ -277,10 +277,6 @@ final class Authorizer(
|
||||
authenticatedClaimsFromContext()
|
||||
.fold(
|
||||
ex => {
|
||||
// TODO error codes: Remove once fully relying on self-service error codes with logging on creation
|
||||
logger.debug(
|
||||
s"No authenticated claims found in the request context. Returning UNAUTHENTICATED"
|
||||
)
|
||||
observer.onError(ex)
|
||||
},
|
||||
claims =>
|
||||
|
@ -15,6 +15,7 @@ import com.daml.error.{
|
||||
ContextualizedErrorLogger,
|
||||
DamlContextualizedErrorLogger,
|
||||
ErrorAssertionsWithLogCollectorAssertions,
|
||||
ErrorCode,
|
||||
}
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.platform.testing.LogCollector.ExpectedLogEntry
|
||||
@ -716,11 +717,6 @@ class ErrorFactoriesSpec
|
||||
)
|
||||
}
|
||||
|
||||
"should create an ApiException without the stack trace" in {
|
||||
val status = Status.newBuilder().setCode(Code.INTERNAL.value()).build()
|
||||
val exception = ErrorFactories.grpcError(status)
|
||||
exception.getStackTrace shouldBe Array.empty
|
||||
}
|
||||
}
|
||||
|
||||
private def expectedMarkerRegex(extraInner: String): Some[String] = {
|
||||
@ -737,13 +733,15 @@ class ErrorFactoriesSpec
|
||||
message: String,
|
||||
details: Seq[ErrorDetails.ErrorDetail],
|
||||
logEntry: ExpectedLogEntry,
|
||||
): Unit =
|
||||
assertError(io.grpc.protobuf.StatusProto.toStatusRuntimeException(status))(
|
||||
): Unit = {
|
||||
val e = io.grpc.protobuf.StatusProto.toStatusRuntimeException(status)
|
||||
assertError(new ErrorCode.ApiException(e.getStatus, e.getTrailers))(
|
||||
code,
|
||||
message,
|
||||
details,
|
||||
logEntry,
|
||||
)
|
||||
}
|
||||
|
||||
private def assertError(
|
||||
error: DamlError
|
||||
|
@ -15,7 +15,6 @@ import com.daml.ledger.participant.state.{v2 => state}
|
||||
import com.daml.lf.data.Ref
|
||||
import com.daml.logging.{ContextualizedLogger, LoggingContext}
|
||||
import com.daml.platform.apiserver.services.admin.SynchronousResponse.{Accepted, Rejected}
|
||||
import com.daml.platform.server.api.validation.ErrorFactories
|
||||
import com.daml.telemetry.TelemetryContext
|
||||
import io.grpc.StatusRuntimeException
|
||||
|
||||
@ -72,14 +71,11 @@ class SynchronousResponse[Input, Entry, AcceptedEntry](
|
||||
Some(submissionId),
|
||||
)
|
||||
Future.failed(
|
||||
// TODO error codes: simplify
|
||||
ErrorFactories.grpcError(
|
||||
LedgerApiErrors.ServiceNotRunning
|
||||
.Reject("Party submission")(
|
||||
errorLogger
|
||||
)
|
||||
.asGrpcStatus
|
||||
)
|
||||
LedgerApiErrors.ServiceNotRunning
|
||||
.Reject("Party submission")(
|
||||
errorLogger
|
||||
)
|
||||
.asGrpcError
|
||||
)
|
||||
}
|
||||
.flatten
|
||||
|
@ -119,18 +119,15 @@ private[apiserver] final class ApiTransactionService private (
|
||||
override def getTransactionByEventId(
|
||||
request: GetTransactionByEventIdRequest
|
||||
): Future[GetTransactionResponse] = {
|
||||
// There is no problem in leaking the loggingContext in here, but the construction looks suspicious
|
||||
// TODO error codes: Replace with non-closure-based enriched loggingContext builder here and in other constructions as well
|
||||
implicit val errorLogger: ContextualizedErrorLogger = withEnrichedLoggingContext(
|
||||
implicit val enrichedLoggingContext: LoggingContext = LoggingContext.enriched(
|
||||
logging.ledgerId(request.ledgerId),
|
||||
logging.eventId(request.eventId),
|
||||
logging.parties(request.requestingParties),
|
||||
) { implicit loggingContext =>
|
||||
logger.info("Received request for transaction by event ID.")
|
||||
new DamlContextualizedErrorLogger(logger, loggingContext, None)
|
||||
}
|
||||
logger.trace(s"Transaction by event ID request: $request")
|
||||
|
||||
)(loggingContext)
|
||||
logger.info("Received request for transaction by event ID.")(enrichedLoggingContext)
|
||||
implicit val errorLogger: ContextualizedErrorLogger =
|
||||
new DamlContextualizedErrorLogger(logger, enrichedLoggingContext, None)
|
||||
logger.trace(s"Transaction by event ID request: $request")(loggingContext)
|
||||
LfEventId
|
||||
.fromString(request.eventId.unwrap)
|
||||
.map { case LfEventId(transactionId, _) =>
|
||||
@ -141,7 +138,7 @@ private[apiserver] final class ApiTransactionService private (
|
||||
invalidArgument(s"invalid eventId: ${request.eventId}")
|
||||
}
|
||||
}
|
||||
.andThen(logger.logErrorsOnCall[GetTransactionResponse])
|
||||
.andThen(logger.logErrorsOnCall[GetTransactionResponse](loggingContext))
|
||||
}
|
||||
|
||||
override def getTransactionById(
|
||||
|
@ -20,9 +20,6 @@ import scala.annotation.tailrec
|
||||
*
|
||||
* * Problems that cannot be recovered from are non-transient. For example, an illegal argument exception inside a
|
||||
* database transaction or a unique constraint violation.
|
||||
*
|
||||
* TODO error codes: Move and handle the error specialization per storage backend
|
||||
* in [[com.daml.platform.store.backend.StorageBackend]].
|
||||
*/
|
||||
object DatabaseSelfServiceError {
|
||||
@tailrec
|
||||
|
@ -204,6 +204,7 @@ final class ErrorInterceptorSpec
|
||||
expectedMessage =
|
||||
"An error occurred. Please contact the operator and inquire about the request <no-correlation-id>",
|
||||
expectedDetails = Seq(),
|
||||
verifyEmptyStackTrace = false,
|
||||
)
|
||||
Assertions.succeed
|
||||
}
|
||||
@ -218,6 +219,7 @@ final class ErrorInterceptorSpec
|
||||
expectedMessage = s"FOO_MISSING_ERROR_CODE(11,0): Foo is missing: $expectedMsg",
|
||||
expectedDetails =
|
||||
Seq(ErrorDetails.ErrorInfoDetail("FOO_MISSING_ERROR_CODE", Map("category" -> "11"))),
|
||||
verifyEmptyStackTrace = false,
|
||||
)
|
||||
Assertions.succeed
|
||||
}
|
||||
|
@ -120,6 +120,7 @@ final class OngoingStreamAuthIT
|
||||
),
|
||||
ErrorDetails.RetryInfoDetail(0.seconds),
|
||||
),
|
||||
verifyEmptyStackTrace = false,
|
||||
)
|
||||
case _ => fail("Unexpected error", t)
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ package com.daml.platform.testing
|
||||
import ch.qos.logback.classic.Level
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent
|
||||
import ch.qos.logback.core.AppenderBase
|
||||
import com.daml.platform.testing.LogCollector.Entry
|
||||
import com.daml.platform.testing.LogCollector.{Entry, ThrowableEntry}
|
||||
import com.daml.scalautil.Statement
|
||||
import org.scalatest.Checkpoints.Checkpoint
|
||||
import org.scalatest.{AppendedClues, OptionValues}
|
||||
@ -20,7 +20,13 @@ import scala.reflect.ClassTag
|
||||
|
||||
object LogCollector {
|
||||
|
||||
case class Entry(level: Level, msg: String, marker: Option[Marker])
|
||||
case class ThrowableEntry(className: String, message: String)
|
||||
case class Entry(
|
||||
level: Level,
|
||||
msg: String,
|
||||
marker: Option[Marker],
|
||||
throwableEntry: Option[ThrowableEntry] = None,
|
||||
)
|
||||
case class ExpectedLogEntry(level: Level, msg: String, markerRegex: Option[String])
|
||||
|
||||
private val log = TrieMap.empty[String, TrieMap[String, mutable.Builder[Entry, Vector[Entry]]]]
|
||||
@ -68,7 +74,15 @@ final class LogCollector extends AppenderBase[ILoggingEvent] {
|
||||
val log = LogCollector.log
|
||||
.getOrElseUpdate(test, TrieMap.empty)
|
||||
.getOrElseUpdate(e.getLoggerName, Vector.newBuilder)
|
||||
val _ = log.synchronized { log += Entry(e.getLevel, e.getMessage, Option(e.getMarker)) }
|
||||
val _ = log.synchronized {
|
||||
log += Entry(
|
||||
e.getLevel,
|
||||
e.getMessage,
|
||||
Option(e.getMarker),
|
||||
throwableEntry =
|
||||
Option(e.getThrowableProxy).map(t => ThrowableEntry(t.getClassName, t.getMessage)),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -82,6 +96,7 @@ trait LogCollectorAssertions extends OptionValues with AppendedClues { self: Mat
|
||||
expectedLogLevel: Level,
|
||||
expectedMsg: String,
|
||||
expectedMarkerAsString: String,
|
||||
expectedThrowableEntry: Option[ThrowableEntry],
|
||||
): Unit = {
|
||||
actual should have size 1 withClue ("expected exactly one log entry")
|
||||
val actualEntry = actual.head
|
||||
@ -91,6 +106,7 @@ trait LogCollectorAssertions extends OptionValues with AppendedClues { self: Mat
|
||||
cp { Statement.discard { actualEntry.level shouldBe expectedLogLevel } }
|
||||
cp { Statement.discard { actualEntry.msg shouldBe expectedMsg } }
|
||||
cp { Statement.discard { actualMarker shouldBe expectedMarkerAsString } }
|
||||
cp { Statement.discard { actualEntry.throwableEntry shouldBe expectedThrowableEntry } }
|
||||
cp.reportAll()
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,12 @@ object LoggingContext {
|
||||
def apply(entry: LoggingEntry, entries: LoggingEntry*): LoggingContext =
|
||||
new LoggingContext(LoggingEntries(entry +: entries: _*))
|
||||
|
||||
def enriched(entry: LoggingEntry, entries: LoggingEntry*)(implicit
|
||||
loggingContext: LoggingContext
|
||||
): LoggingContext = {
|
||||
loggingContext ++ LoggingEntries(entry +: entries: _*)
|
||||
}
|
||||
|
||||
private[logging] def newLoggingContext[A](entries: LoggingEntries)(f: LoggingContext => A): A =
|
||||
f(new LoggingContext(entries))
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user