Skip to content

Commit

Permalink
EG-433 - Error Reporting Framework (Merge)
Browse files Browse the repository at this point in the history
Error Reporting Framework - Phase 1
  • Loading branch information
Katelyn Baker authored Apr 30, 2020
2 parents 83dd9a9 + 7db25b8 commit 99fd089
Show file tree
Hide file tree
Showing 72 changed files with 1,353 additions and 14 deletions.
2 changes: 2 additions & 0 deletions common/logging/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ dependencies {
compile "com.jcabi:jcabi-manifests:$jcabi_manifests_version"

testCompile project(":test-utils")
testCompile "com.nhaarman:mockito-kotlin:$mockito_kotlin_version"
testCompile "org.mockito:mockito-core:$mockito_version"
}


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.corda.common.logging.errorReporting

import net.corda.common.logging.CordaVersion
import java.util.*

/**
* Provides information specific to Corda to the error reporting library.
*
* The primary use of this is to provide the URL to the docs site where the error information is hosted.
*/
class CordaErrorContextProvider : ErrorContextProvider {

companion object {
private const val BASE_URL = "https://docs.corda.net/docs"
private const val OS_PAGES = "corda-os"
private const val ENTERPRISE_PAGES = "corda-enterprise"
private const val ERROR_CODE_PAGE = "error-codes.html"
}

override fun getURL(locale: Locale): String {
val versionNumber = CordaVersion.releaseVersion

// This slightly strange block here allows the code to be merged across to Enterprise with no changes.
val productVersion = if (CordaVersion.platformEditionCode == "OS") {
OS_PAGES
} else {
ENTERPRISE_PAGES
}
return "$BASE_URL/$productVersion/$versionNumber/$ERROR_CODE_PAGE"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.corda.common.logging.errorReporting

/**
* A type representing an error condition.
*
* Error codes should be used in situations where an error is expected and information can be provided back to the user about what they've
* done wrong. Each error code should have a resource bundle defined for it, which contains set of properties that define the error string
* in different languages. See the resource bundles in common/logging/src/main/resources/errorReporting for more details.
*/
interface ErrorCode<CODES> where CODES: ErrorCodes, CODES: Enum<CODES> {

/**
* The error code.
*
* Error codes are used to indicate what sort of error occurred. A unique code should be returned for each possible
* error condition that could be reported within the defined namespace. The code should very briefly describe what has gone wrong, e.g.
* "failed-to-store" or "connection-unavailable".
*/
val code: CODES

/**
* Parameters to pass to the string template when reporting this error. The corresponding template that defines the error string in the
* resource bundle must be expecting this list of parameters. Parameters should be in the order required by the message template - for
* example, if the message template is "This error has argument {0} and argument {1}", the first element of this list will be placed
* into {0}.
*/
val parameters: List<Any>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.corda.common.logging.errorReporting

/**
* A collection of error codes.
*
* Types implementing this are required to be enum classes to be used in an error.
*/
interface ErrorCodes {
/**
* The namespace of this collection of errors.
*
* These are used to partition errors into categories, e.g. "database" or "cordapp". Namespaces should be unique, which can be enforced
* by using enum elements.
*/
val namespace: String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package net.corda.common.logging.errorReporting

import java.util.*

/**
* Provide context around reported errors by supplying product specific information.
*/
interface ErrorContextProvider {

/**
* Get the URL to the docs site where the error codes are hosted.
*
* Note that the correct docs site link is likely to depend on the following:
* - The locale of the error message
* - The product the error was reported from
* - The version of the product the error was reported from
*
* The returned URL must be the link the to the error code table in the documentation.
*
* @param locale The locale of the link
* @return The URL of the docs site, to be printed in the logs
*/
fun getURL(locale: Locale) : String
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.corda.common.logging.errorReporting

import org.slf4j.Logger

/**
* Reports error conditions to the logs, using localised error messages.
*/
internal interface ErrorReporter {
/**
* Report a particular error condition
*
* @param error The error to report
* @param logger The logger to use when reporting this error
*/
fun report(error: ErrorCode<*>, logger: Logger)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package net.corda.common.logging.errorReporting

import org.slf4j.Logger
import java.text.MessageFormat
import java.util.*

internal const val ERROR_INFO_RESOURCE = "ErrorInfo"
internal const val ERROR_CODE_MESSAGE = "errorCodeMessage"
internal const val ERROR_CODE_URL = "errorCodeUrl"

internal class ErrorReporterImpl(private val resourceLocation: String,
private val locale: Locale,
private val errorContextProvider: ErrorContextProvider) : ErrorReporter {

private fun fetchAndFormat(resource: String, property: String, params: Array<out Any>) : String {
val bundle = ResourceBundle.getBundle(resource, locale)
val template = bundle.getString(property)
val formatter = MessageFormat(template, locale)
return formatter.format(params)
}

// Returns the string appended to all reported errors, indicating the error code and the URL to go to.
// e.g. [Code: my-error-code, For further information, please go to https://docs.corda.net/corda-os/4.5/error-codes.html]
private fun getErrorInfo(error: ErrorCode<*>) : String {
val resource = "$resourceLocation/$ERROR_INFO_RESOURCE"
val codeMessage = fetchAndFormat(resource, ERROR_CODE_MESSAGE, arrayOf(error.formatCode()))
val urlMessage = fetchAndFormat(resource, ERROR_CODE_URL, arrayOf(errorContextProvider.getURL(locale)))
return "[$codeMessage, $urlMessage]"
}

override fun report(error: ErrorCode<*>, logger: Logger) {
val errorResource = ErrorResource.fromErrorCode(error, resourceLocation, locale)
val message = "${errorResource.getErrorMessage(error.parameters.toTypedArray())} ${getErrorInfo(error)}"
logger.error(message)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package net.corda.common.logging.errorReporting

import java.util.*

/**
* Entry point into the Error Reporting framework.
*
* This creates the error reporter used to report errors. The `initialiseReporting` method should be called to build a reporter before any
* errors are reported.
*/
class ErrorReporting private constructor(private val localeString: String,
private val resourceLocation: String,
private val contextProvider: ErrorContextProvider?) {

constructor() : this(DEFAULT_LOCALE, DEFAULT_LOCATION, null)

private companion object {
private const val DEFAULT_LOCALE = "en-US"
private const val DEFAULT_LOCATION = "."

private var errorReporter: ErrorReporter? = null
}

/**
* Set the locale to use when reporting errors
*
* @param locale The locale tag to use when reporting errors, e.g. en-US
*/
@Suppress("UNUSED_PARAMETER")
fun withLocale(locale: String) : ErrorReporting {
// Currently, if anything other than the default is used this is entirely untested. As a result, an exception is thrown here to
// indicate that this functionality is not ready at this point in time.
throw LocaleSettingUnsupportedException()
}

/**
* Set the location of the resource bundles containing the error codes.
*
* @param location The location within the JAR of the resource bundle
*/
fun usingResourcesAt(location: String) : ErrorReporting {
return ErrorReporting(localeString, location, contextProvider)
}

/**
* Set the context provider to supply project-specific information about the errors.
*
* @param contextProvider The context provider to use with error reporting
*/
fun withContextProvider(contextProvider: ErrorContextProvider) : ErrorReporting {
return ErrorReporting(localeString, resourceLocation, contextProvider)
}

/**
* Set up the reporting of errors.
*/
fun initialiseReporting() {
if (contextProvider == null) {
throw NoContextProviderSuppliedException()
}
if (errorReporter != null) {
throw DoubleInitializationException()
}
errorReporter = ErrorReporterImpl(resourceLocation, Locale.forLanguageTag(localeString), contextProvider)
}

internal fun getReporter() : ErrorReporter {
return errorReporter ?: throw ReportingUninitializedException()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package net.corda.common.logging.errorReporting

abstract class ErrorReportingException(message: String, cause: Throwable? = null) : Exception(message, cause)

/**
* Occurs when reporting is requested before the error reporting code has been initialized
*/
class ReportingUninitializedException : ErrorReportingException("Error reporting is uninitialized")

/**
* Occurs when no error context provider is supplied while initializing error reporting
*/
class NoContextProviderSuppliedException
: ErrorReportingException("No error context provider was supplied when initializing error reporting")

/**
* Occurs if the error reporting framework has been initialized twice
*/
class DoubleInitializationException : ErrorReportingException("Error reporting has previously been initialized")

/**
* Occurs if a locale is set while initializing the error reporting framework.
*
* This is done as locale support has not yet been properly designed, and so using anything other than the default is untested.
*/
class LocaleSettingUnsupportedException :
ErrorReportingException("Setting a locale other than the default is not supported in the first release")
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package net.corda.common.logging.errorReporting

import org.slf4j.Logger

/**
* Report errors that have occurred.
*
* Doing this allows the error reporting framework to find the corresponding resources for the error and pick the correct locale.
*
* @param error The error that has occurred.
*/
fun Logger.report(error: ErrorCode<*>) = ErrorReporting().getReporter().report(error, this)

internal fun ErrorCode<*>.formatCode() : String {
val namespaceString = this.code.namespace.toLowerCase().replace("_", "-")
val codeString = this.code.toString().toLowerCase().replace("_", "-")
return "$namespaceString-$codeString"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package net.corda.common.logging.errorReporting

import net.corda.common.logging.errorReporting.ResourceBundleProperties.ACTIONS_TO_FIX
import net.corda.common.logging.errorReporting.ResourceBundleProperties.ALIASES
import net.corda.common.logging.errorReporting.ResourceBundleProperties.MESSAGE_TEMPLATE
import net.corda.common.logging.errorReporting.ResourceBundleProperties.SHORT_DESCRIPTION
import java.text.MessageFormat
import java.util.*

/**
* A representation of a single error resource file.
*
* This handles selecting the right properties from the resource bundle and formatting the error message.
*/
class ErrorResource private constructor(private val bundle: ResourceBundle,
private val locale: Locale) {

companion object {
/**
* Construct an error resource from a provided code.
*
* @param errorCode The code to get the resource bundle for
* @param resourceLocation The location in the JAR of the error code resource bundles
* @param locale The locale to use for this resource
*/
fun fromErrorCode(errorCode: ErrorCode<*>, resourceLocation: String, locale: Locale) : ErrorResource {
val resource = "$resourceLocation/${errorCode.formatCode()}"
val bundle = ResourceBundle.getBundle(resource, locale)
return ErrorResource(bundle, locale)
}

/**
* Construct an error resource using resources loaded in a given classloader
*
* @param resource The resource bundle to load
* @param classLoader The classloader used to load the resource bundles
* @param locale The locale to use for this resource
*/
fun fromLoader(resource: String, classLoader: ClassLoader, locale: Locale) : ErrorResource {
val bundle = ResourceBundle.getBundle(resource, locale, classLoader)
return ErrorResource(bundle, locale)
}
}

private fun getProperty(propertyName: String) : String = bundle.getString(propertyName)

private fun formatTemplate(template: String, args: Array<Any>) : String {
val formatter = MessageFormat(template, locale)
return formatter.format(args)
}

fun getErrorMessage(args: Array<Any>): String {
val template = getProperty(MESSAGE_TEMPLATE)
return formatTemplate(template, args)
}

val shortDescription: String = getProperty(SHORT_DESCRIPTION)
val actionsToFix: String = getProperty(ACTIONS_TO_FIX)
val aliases: String = getProperty(ALIASES)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package net.corda.common.logging.errorReporting

/**
* Namespaces for errors within the node.
*/
enum class NodeNamespaces {
DATABASE,
CORDAPP
}

/**
* Errors related to database connectivity
*/
enum class NodeDatabaseErrors : ErrorCodes {
COULD_NOT_CONNECT,
MISSING_DRIVER,
FAILED_STARTUP,
PASSWORD_REQUIRED_FOR_H2;

override val namespace = NodeNamespaces.DATABASE.toString()
}

/**
* Errors related to loading of Cordapps
*/
enum class CordappErrors : ErrorCodes {
DUPLICATE_CORDAPPS_INSTALLED,
MULTIPLE_CORDAPPS_FOR_FLOW,
MISSING_VERSION_ATTRIBUTE,
INVALID_VERSION_IDENTIFIER;

override val namespace = NodeNamespaces.CORDAPP.toString()
}
Loading

0 comments on commit 99fd089

Please sign in to comment.