Handle misc error codes todos. [DPP-606] (#13248)

changelog_begin
changelog_end
This commit is contained in:
pbatko-da 2022-03-14 17:16:31 +01:00 committed by GitHub
parent a8d55727c5
commit ee6a5f9e0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 157 additions and 141 deletions

View File

@ -51,7 +51,7 @@ object Main {
"resolution",
)(i =>
(
i.className,
i.errorCodeClassName,
i.category,
i.hierarchicalGrouping.groupings,
i.conveyance,

View File

@ -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],

View File

@ -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,

View File

@ -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(

View File

@ -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()

View File

@ -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

View File

@ -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 {

View File

@ -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)

View File

@ -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

View File

@ -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))
}

View File

@ -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)
)
}

View File

@ -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")

View File

@ -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()
}

View File

@ -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"
}

View File

@ -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,

View File

@ -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 =>

View File

@ -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

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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
}

View File

@ -120,6 +120,7 @@ final class OngoingStreamAuthIT
),
ErrorDetails.RetryInfoDetail(0.seconds),
),
verifyEmptyStackTrace = false,
)
case _ => fail("Unexpected error", t)
}

View File

@ -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()
}

View File

@ -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))