Skip to content

Commit

Permalink
fix: provide generic exception handling functionality
Browse files Browse the repository at this point in the history
Introduce a log helper with static functions to get exception message /
exception stack whatever is thrown or provided.

Closes: hyperledger-cacti#1702
Signed-off-by: Michael Courtin <michael.courtin@accenture.com>
  • Loading branch information
m-courtin committed Feb 3, 2022
1 parent e900a23 commit d7469f9
Show file tree
Hide file tree
Showing 7 changed files with 785 additions and 11 deletions.
17 changes: 9 additions & 8 deletions packages/cactus-cmd-api-server/src/main/typescript/api-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
Bools,
Logger,
LoggerProvider,
ExceptionHelper,
Servers,
} from "@hyperledger/cactus-common";

Expand Down Expand Up @@ -241,16 +242,16 @@ export class ApiServer {

return { addressInfoCockpit, addressInfoApi, addressInfoGrpc };
} catch (ex) {
const errorMessage = `Failed to start ApiServer: ${ex.stack}`;
this.log.error(errorMessage);
const context = "Failed to start ApiServer";
this.log.exception(ex, context);
this.log.error(`Attempting shutdown...`);
try {
await this.shutdown();
this.log.info(`Server shut down after crash OK`);
} catch (ex) {
this.log.error(ApiServer.E_POST_CRASH_SHUTDOWN, ex);
this.log.exception(ex, ApiServer.E_POST_CRASH_SHUTDOWN);
}
throw new Error(errorMessage);
throw ExceptionHelper.getNewRuntimeErrorFromException(ex, context);
}
}

Expand Down Expand Up @@ -296,11 +297,11 @@ export class ApiServer {
await this.getPluginImportsCount(),
);
return this.pluginRegistry;
} catch (e) {
} catch (ex) {
this.pluginRegistry = new PluginRegistry({ plugins: [] });
const errorMessage = `Failed init PluginRegistry: ${e.stack}`;
this.log.error(errorMessage);
throw new Error(errorMessage);
const context = "Failed to init PluginRegistry";
this.log.exception(ex, context);
throw ExceptionHelper.getNewRuntimeErrorFromException(ex, context);
}
}

Expand Down
6 changes: 6 additions & 0 deletions packages/cactus-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
"name": "Peter Somogyvari",
"email": "peter.somogyvari@accenture.com",
"url": "https://accenture.com"
},
{
"name": "Michael Courtin",
"email": "michael.courtin@accenture.com",
"url": "https://accenture.com"
}
],
"license": "Apache-2.0",
Expand All @@ -60,6 +65,7 @@
},
"homepage": "https://github.com/hyperledger/cactus#readme",
"dependencies": {
"fast-safe-stringify": "2.1.1",
"json-stable-stringify": "1.0.1",
"key-encoder": "2.0.3",
"loglevel": "1.7.1",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
import { RuntimeError } from "run-time-error";
import stringify from "fast-safe-stringify";
import { Logger } from "../logging/logger";

export class ExceptionHelper {
/**
* Fetches the exception-content and submits it to the logger
*
* @param logger logger created thru the LoggerProvider.getOrCreate()
* @param exception as an arbitrary exception of whatever type and format it might be
* @param optionalAdditionalContext as optional on top info / context on the exception scenario
*/
public static logException(
logger: Logger,
exception: unknown,
optionalAdditionalContext = "",
): void {
const exceptionContent = this.getExceptionContent(
exception,
optionalAdditionalContext,
);

// log to destination
logger.error(...exceptionContent);
}

/**
* Creates a new RuntimeError based on the incoming exception and context to preserve the complete hierarchy of exception information
*
* @param exception as an arbitrary exception of whatever type and format it might be
* @param context providing context about the exception scenario
* @returns a new RuntimeError created from exception to preserve the complete hierarchy of exception information
*/
public static getNewRuntimeErrorFromException(
exception: unknown,
context: string,
): RuntimeError {
const exceptionMessageInfo = this.getExceptionMessageInfo(exception);
const exceptionStackInfo = this.getExceptionStackInfo(exception);
let content = "";
let runtimeError = new RuntimeError(context, exceptionMessageInfo.message);

if (exception instanceof RuntimeError || exception instanceof Error) {
// scenario 1: exception is already an instance of Error / RuntimeError -> can be used directly
runtimeError = new RuntimeError(context, exception);
} else {
// scenario 2: exception is of custom type
// -> need to fetch content
if (exceptionMessageInfo.containsCompleteStringifiedException) {
content = exceptionMessageInfo.message;
} else {
content =
exceptionMessageInfo.message + " - " + exceptionStackInfo.stack;
}

runtimeError = new RuntimeError(context, content);
}

return runtimeError;
}

/**
*
* @param exception as an arbitrary exception of whatever type and format it might be
* @param optionalAdditionalContext as optional on top info / context on the exception scenario
* @returns an array of string information about the exception like:
* - message,
* - stack
* - other properties (depending on the type of exception)
*/
public static getExceptionContent(
exception: unknown,
optionalAdditionalContext = "",
): string[] {
const exceptionStackInfo = this.getExceptionStackInfo(exception);
const exceptionMessageInfo = this.getExceptionMessageInfo(exception);
const validOptionalAdditionalContext =
optionalAdditionalContext &&
optionalAdditionalContext !== "" &&
optionalAdditionalContext !== " ";
let content: string[] = [];
let done = false;

// scenario 1: messageInfo.message is containing a fully stringified exception -> no need to additionally fetch for the exception stack
// and some valid additional context is provided
if (
exceptionMessageInfo.containsCompleteStringifiedException &&
validOptionalAdditionalContext
) {
content = [optionalAdditionalContext, exceptionMessageInfo.message];
done = true;
}

// scenario 2: messageInfo.message is containing a fully stringified exception -> no need to additionally fetch for the exception stack
// and no valid additional context is provided
if (
!done &&
exceptionMessageInfo.containsCompleteStringifiedException &&
!validOptionalAdditionalContext
) {
content = [exceptionMessageInfo.message];
}

// scenario 3: messageInfo.message is not containing a fully stringified exception -> need to fetch also exception stack
// and some valid additional context is provided
if (
!done &&
!exceptionMessageInfo.containsCompleteStringifiedException &&
validOptionalAdditionalContext
) {
content = [
optionalAdditionalContext,
exceptionMessageInfo.message,
exceptionStackInfo.stack,
];
}

// scenario 4: messageInfo.message is not containing a fully stringified exception -> need to fetch also exception stack
// and no valid additional context is provided
if (
!done &&
!exceptionMessageInfo.containsCompleteStringifiedException &&
!validOptionalAdditionalContext
) {
content = [exceptionMessageInfo.message, exceptionStackInfo.stack];
}

return content;
}

/**
* USE THIS FUNCTION ONLY IN CASE OF SPECIAL EXCEPTION HANDLING
* For a general exception handling / logging use the function logException() or consoleException() above
*
* @param exception as an arbitrary exception of whatever type and format it might be
* @returns the message information of the exception as a string
*/
public static getExceptionMessage(exception: unknown): string {
const exceptionMessageInfo = this.getExceptionMessageInfo(exception);

return exceptionMessageInfo.message;
}

/**
* USE THIS FUNCTION ONLY IN CASE OF SPECIAL EXCEPTION HANDLING
* For a general exception handling / logging use the function logException() or consoleException() above
*
* @param exception as an arbitrary exception of whatever type and format it might be
* @returns the stack information of the exception as a string
*/
public static getExceptionStack(exception: unknown): string {
const exceptionStackInfo = this.getExceptionStackInfo(exception);

return exceptionStackInfo.stack;
}

/**
* private helper method to obtain the message information of the exception as a message-string and
* an additional indicator if the message contains a fully stringified exception
*
* @param exception as an arbitrary exception of whatever type it might be
* @returns the message information of the exception as a message-string and an indicator if the message contains fully stringified exception
*/
private static getExceptionMessageInfo(
exception: unknown,
): { message: string; containsCompleteStringifiedException: boolean } {
// handle unknown exception input
const defaultMessage = "NO_MESSAGE_INCLUDED_IN_EXCEPTION";
const invalidException = "INVALID_EXCEPTION";
const invalidMessage = "INVALID_EXCEPTION_MESSAGE";
const customExceptionPrefix = "A CUSTOM EXCEPTION WAS THROWN: ";
let message = defaultMessage;
let exceptionHandled = false;
let containsCompleteStringifiedException = false;

// 1st need to check that exception is not null or undefined before trying to access the wanted message information
if (exception) {
const isOfTypeString = typeof exception === "string";
const isOfTypeObject = typeof exception === "object";
const hasOwnPropertyMessage = Object.hasOwnProperty.call(
exception,
"message",
);
const messageIsOfTypeString =
typeof (exception as Record<string, unknown>).message === "string";

// scenario 1: exception is of type object and providing a string message property
if (isOfTypeObject && hasOwnPropertyMessage && messageIsOfTypeString) {
message = (exception as { message: string }).message;
exceptionHandled = true;
}

// scenario 2: exception is of type object and providing a non-string message property
if (
!exceptionHandled &&
isOfTypeObject &&
hasOwnPropertyMessage &&
!messageIsOfTypeString
) {
// need to stringify message information first
message = this.safeJsonStringify(
(exception as { message: unknown }).message,
invalidMessage,
);
exceptionHandled = true;
}

// scenario 3: handling of string type exceptions
if (!exceptionHandled && isOfTypeString) {
message = customExceptionPrefix + exception;
exceptionHandled = true;
}

// scenario 4: handling of custom exceptions
if (!exceptionHandled && !isOfTypeString) {
// custom exception is of a different type -> need to stringify it
message =
customExceptionPrefix +
this.safeJsonStringify(exception, invalidException);
exceptionHandled = true;
containsCompleteStringifiedException = true;
}
}
return {
message,
containsCompleteStringifiedException,
};
}

/**
* private helper method to obtain the stack information of the exception as a stack-string and
* an additional indicator if the stack contains a fully stringified exception
*
* @param exception as an arbitrary exception of whatever type it might be
* @returns the stack information of the exception as a stack-string and an indicator if the stack contains fully stringified exception
*/
private static getExceptionStackInfo(
exception: unknown,
): { stack: string; containsCompleteStringifiedException: boolean } {
// handle unknown exception input
const fallbackStack = "NO_STACK_INFORMATION_INCLUDED_IN_EXCEPTION";
const invalidStack = "INVALID_STACK_INFORMATION";
const invalidException = "INVALID_EXCEPTION";
const customExceptionPrefix = "A CUSTOM EXCEPTION WAS THROWN: ";
let stack = fallbackStack;
let exceptionHandled = false;
let containsCompleteStringifiedException = false;

// 1st need to check that exception is not null or undefined before trying to access the wanted stack information
// otherwise the default fallback stack info will be returned
if (exception) {
const isOfTypeObject = typeof exception === "object";
const isInstanceOfRuntimeError = exception instanceof RuntimeError;
const hasOwnPropertyStack = Object.hasOwnProperty.call(
exception,
"stack",
);
const stackIsOfTypeString =
typeof (exception as Record<string, unknown>).stack === "string";

// scenario 1: exception is an instance of RuntimeError
if (isInstanceOfRuntimeError) {
// handling RuntimeError stack inclusive nested / cascaded stacks
stack = this.safeJsonStringify(exception);
containsCompleteStringifiedException = true;
exceptionHandled = true;
}

// scenario 2: exception is of type object and providing a string stack property
if (
!exceptionHandled &&
isOfTypeObject &&
hasOwnPropertyStack &&
stackIsOfTypeString
) {
stack = (exception as { stack: string }).stack;
exceptionHandled = true;
}

// scenario 3: exception is of type object and providing a non-string stack property
if (
!exceptionHandled &&
isOfTypeObject &&
hasOwnPropertyStack &&
!stackIsOfTypeString
) {
// need to stringify stack information first
stack = this.safeJsonStringify(
(exception as { stack: unknown }).stack,
invalidStack,
);
containsCompleteStringifiedException = true;
exceptionHandled = true;
}

// scenario 4: handling of custom exceptions
if (!exceptionHandled) {
// custom exception is of a different type -> need to stringify it
stack =
customExceptionPrefix +
this.safeJsonStringify(exception, invalidException);
containsCompleteStringifiedException = true;
exceptionHandled = true;
}
}
return {
stack,
containsCompleteStringifiedException,
};
}

private static safeJsonStringify(
input: unknown,
catchMessage = "INVALID_INPUT",
): string {
let message = "";

try {
// use fast-safe-stringify to also handle gracefully circular structures
message = stringify(input);
} catch (error) {
// fast and safe stringify failed
message = catchMessage;
}
return message;
}
}
Loading

0 comments on commit d7469f9

Please sign in to comment.