Logging and Error Handling for the MealOrderService
When building "real" code it is important to consider how users, or other developers on your team, will be able to support it, including troubleshoot problems that arise. We have already used the Manage Processes screen to see what is happening in business processes. When writing custom code, as we have in the Meal Program Library, you have all the standard ways of tracing and debugging the code using standard tools.
Debugging
If your instance allows remote connections, you can use your IDE to connect to your instance, place breakpoints, single-step through code, as described in the documentation. Note: most cloud-based development sandboxes currently do not allow these connections.
Error Handling
In many cases, when errors occur it is best handled by catching an exception in the form of the RxException class. If iit is indicating an error on the server, by default it will be propagated back to the client, and will be logged in the relevant service log, such as process.log that is accessible in the same fashion as your own custom logs.
You may want to do some special handling of errors. You can actually implement a custom class that extends RxException. In the sample code, you can see this in the class MealOrderException. In this class, we do several things:
- We extend RxException, the exception thrown by the framework itself. This causes it to be properly communicated back to the client.
- Define our own constants for the different situations. We have elected to use a Java enum here called MealOrderMessage, but we could have used static final int constants as well.
- Because RxException uses integer error codes, we need to provide a constructor for MealOrderMessage that converts from int, and an IntValue() method to convert the other way. Note: this code uses underscore characters in the numeric literals for readability, as in 600_100 for 600100 - this may look strange, but the underscores are ignored by the compiler and this is perfectly valid Java code.
- We need to overload at least one of the constructors for RxException. For a simple message with appended text, that wraps the original Exception, we use that particular constructor.
- We pass the bundle-id to the framework so it can handle messages from different bundles properly.
import com.bmc.arsys.rx.services.RxException;
public class MealOrderException extends RxException {
private static final long serialVersionUID = -1801158764215645524L;
public enum MealOrderMessage {
MEAL_ORDER_NOT_FOUND(600_100),
MEAL_ORDER_INVALID(600_101),
MEAL_ORDER_CANT_MODIFY(600_102),
MEAL_ORDER_CANT_READ_ASSOCIATION(600_103),
MEAL_ORDER_CANT_CREATE(600_104),
MEAL_ORDER_NO_SUCH_DISH(600_105),
MEAL_ORDER_NO_SUCH_EMPLOYEE(600_106),
MEAL_ORDER_BAD_DATE_FORMAT(600_107),
MEAL_ORDER_CANT_FORMAT(600_108),
MEAL_ORDER_CANT_LOAD_ORDER_INFO(600_109);
private final int intValue;
MealOrderMessage(int intValue) {
this.intValue= intValue;
}
public int intValue() {
return intValue;
}
}
public MealOrderException(MealOrderMessage errorMessage, String appendedText, RxException e) {
super(errorMessage.intValue(), MealOrderConstants.MEAL_PROGRAM_BUNDLE_ID, appendedText, e);
}
public MealOrderException(MealOrderMessage errorMessage, Exception e) {
super(errorMessage.intValue(), MealOrderConstants.MEAL_PROGRAM_BUNDLE_ID, e.getLocalizedMessage(), null);
}
public MealOrderException(MealOrderMessage errorMessage, String appendedText) {
super(errorMessage.intValue(), MealOrderConstants.MEAL_PROGRAM_BUNDLE_ID, appendedText, null);
}
}
You will note that there is no text provided for the error codes. That is because text must be provided in the localizable properties file for this bundle, found in src/main/resources/localized-strings.properties.
600101=The Order is Invalid
600102=The Order cold not be modified
600103=Could not read data associated to Order
600104=Could not create order
600105=No such dish
600106=No such employee
600107=Not a valid date format
600108=Cannot format response
600109=Cannot load order information
In order to test this, as a developer, it is not enough to simply but the strings in this file. You will also need to run the "localization" maven profile at least once. This causes these strings to be loaded into the string catalog of BMC Helix Platform.
Here's an example where we wrap an RxException into a MealOrderException with our own error message. This code also logs a different message directly into the configured log for this bundle.
try {
mealOrderRecordInstance = recordService.getRecordInstance(
MealOrderConstants.MEAL_ORDER_RECORD_DEF_NAME, getId());
}
catch (RxException e) {
ServiceLocator.getLogger().error("No such Order " + getId());
throw new MealOrderException(
MealOrderException.MealOrderMessage.MEAL_ORDER_NOT_FOUND,
getId(),
e );
}
This can be tested by invoking Get Order Info using an invalid Order ID.
Logging
As you may have noticed in the previous snippet, it is also possible to log various events at various levels. The Logging Service provides a uniform way to format filterable information into a configured server-side log file that can be managed and retrieved to trace through code execution. Your code can emit logging statements using the Logging Service. For example, note the beginning of the MealOrderInfoDataPageQuery's execute() method:
To configure where this output goes, and the level of logging, find the com.example.meal-program-lib bundle configuration in the Administration tab of BMC Helix Innovation Studio.
Here's an example of testing and examining the log (and also shows the use of exception wrapping). Consider the following logging statements found in the cancelMealOrder() implementation.
* Set an Order into the status Cancelled.
* @param mealOrderId
* @return
*/
@Override
@Action(scope = Scope.PUBLIC)
public boolean cancelMealOrder(
@ActionParameter(name = "mealOrderId") @NotBlank String mealOrderId) {
ServiceLocator.getLogger().trace("cancelMealOrder(" + mealOrderId + ")");
RecordService recordService = ServiceLocator.getRecordService();
RecordInstance mealOrderRecordInstance;
try {
mealOrderRecordInstance = recordService.getRecordInstance(
MealOrderConstants.MEAL_ORDER_RECORD_DEF_NAME, mealOrderId);
}
catch (RxException e) {
ServiceLocator.getLogger().error("Cannot cancel - no such Order " + mealOrderId);
throw new MealOrderException(MealOrderException.MealOrderMessage.MEAL_ORDER_NOT_FOUND, mealOrderId, e);
}
if (ServiceLocator.getLogger().isInfoEnabled()) {
// Don't pay performance penalty for this operation unless this level is enabled.
ServiceLocator.getLogger().info("previous status was: "
+ mealOrderRecordInstance.getFieldValue(MealOrderConstants.MEAL_ORDER_STATUS_FIELD_ID));
}
ServiceLocator.getLogger().trace("cancelMealOrder updating status for "
+ mealOrderId + " to Cancelled ("
+ MealOrderConstants.MEAL_ORDER_STATUS_CANCELLED
+ ")");
// Update the meal order itself to be Cancelled.
try {
mealOrderRecordInstance.setFieldValue(MealOrderConstants.MEAL_ORDER_STATUS_FIELD_ID,
MealOrderConstants.MEAL_ORDER_STATUS_CANCELLED);
recordService.updateRecordInstance(mealOrderRecordInstance);
}
catch (RxException e) {
ServiceLocator.getLogger().error("Cannot set Order " + mealOrderId + " to Cancelled" + mealOrderId);
throw new MealOrderException(MealOrderException.MealOrderMessage.MEAL_ORDER_CANT_MODIFY, mealOrderId, e);
}
ServiceLocator.getLogger().trace("END - cancelMealOrder(" + mealOrderId + ")");
return true;
}
Let's run this method to see the log output.
- Create an Order - it will now be in Submitted state - and capture the ID using the Edit Data screen.
- Using Postman to send the command:
- Be sure to include the default-bundle-scope header. Otherwise, logging will only occur in the context of a calling Application.
- Pass the ID of the Order as the required parameter of the command.
- Be sure to include the default-bundle-scope header. Otherwise, logging will only occur in the context of a calling Application.
- Use the Mid-Tier to retrieve the mealprogramlibrary.log file that was configured earlier.
You can see the trace statements appearing in the log.
mealprogramlibrary.log - SnippetINFO START - cancelMealOrder(AGGA4D22JRZ70AOWLJ0BOVPAFKA09O)
INFO previous status was: 0
INFO cancelMealOrder updating status for AGGA4D22JRZ70AOWLJ0BOVPAFKA09O to Cancelled
INFO END - cancelMealOrder(AGGA4D22JRZ70AOWLJ0BOVPAFKA09O)- Try a bad ID to test error handling:
- Change the body of the POST.
- Note the response shows the custom Exception text.
The log shows both trace and error statements.
mealprogramlibrary.log - SnippetINFO CancelMealOrderCommand called for meal order bad-id
TRACE cancelMealOrder(bad-id)
ERROR Cannot cancel - no such Order bad-id
- Change the body of the POST.
Challenge
- Add your own logging, re-deploy the Library, and test it using Postman
- Add your own error handling:
- Add a new constant to MealOrderMessage.
- Be sure to add English text for it in localized-strings.properties, and localize it using the -Plocalization command.
- Throw it under some circumstance in your code.
- Test it with Postman to be sure it returns the right error.
What Have you Learned
- To create maintainable code, make sure you are logging important events or adding appropriate tracing.
- The Logging Service can be used to send logs to a retrievable file, or to view messages in the Browser (for synchronous processes)
- You can extend RxException in order to wrap it with more meaningful error messages.
For more information, see Handling-exceptions.