Skip to content

Commit

Permalink
feat: forward Zeebe client exceptions to the web UI (#353)
Browse files Browse the repository at this point in the history
* new: forwarding/pushing ZeebeClient errors to the web UI
new: allow multiple errors, info and success messages displayed at once
fix: closing error panel also closes info panel (now each panel must be closed individually)
change: when during incident resolving the job can't be changed, there's still an attempt to resolve the incident
  • Loading branch information
nitram509 authored Jan 18, 2022
1 parent ad53754 commit f338b9e
Show file tree
Hide file tree
Showing 11 changed files with 312 additions and 96 deletions.
51 changes: 33 additions & 18 deletions src/main/java/io/zeebe/monitor/rest/ExceptionHandler.java
Original file line number Diff line number Diff line change
@@ -1,37 +1,52 @@
package io.zeebe.monitor.rest;

import io.camunda.zeebe.client.api.command.ClientException;
import io.zeebe.monitor.rest.ui.ErrorMessage;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;

@ControllerAdvice
public class ExceptionHandler {

private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandler.class);
private static final Logger LOG = LoggerFactory.getLogger(ExceptionHandler.class);

private final WhitelabelProperties whitelabelProperties;
private final WhitelabelProperties whitelabelProperties;

public ExceptionHandler(WhitelabelProperties whitelabelProperties) {
this.whitelabelProperties = whitelabelProperties;
}
public ExceptionHandler(WhitelabelProperties whitelabelProperties) {
this.whitelabelProperties = whitelabelProperties;
}

@org.springframework.web.bind.annotation.ExceptionHandler(RuntimeException.class)
public String handleRuntimeException(RuntimeException exc, final Model model) {
LOG.error(exc.getMessage(), exc);
@org.springframework.web.bind.annotation.ExceptionHandler(value = {ClientException.class})
protected ResponseEntity<Object> handleZeebeClientException(final RuntimeException ex, final WebRequest request) {
LOG.debug("Zeebe Client Exception caught and forwarding to UI.", ex);
return ResponseEntity
.status(HttpStatus.FAILED_DEPENDENCY)
.contentType(MediaType.APPLICATION_JSON)
.body(new ErrorMessage(ex.getMessage()));
}

model.addAttribute("error", exc.getClass().getSimpleName());
model.addAttribute("message", exc.getMessage());
model.addAttribute("trace", ExceptionUtils.getStackTrace(exc));
@org.springframework.web.bind.annotation.ExceptionHandler(RuntimeException.class)
public String handleRuntimeException(final RuntimeException exc, final Model model) {
LOG.error(exc.getMessage(), exc);

model.addAttribute("custom-title", whitelabelProperties.getCustomTitle());
model.addAttribute("context-path", whitelabelProperties.getBasePath());
model.addAttribute("logo-path", whitelabelProperties.getLogoPath());
model.addAttribute("custom-css-path", whitelabelProperties.getCustomCssPath());
model.addAttribute("custom-js-path", whitelabelProperties.getCustomCssPath());
model.addAttribute("error", exc.getClass().getSimpleName());
model.addAttribute("message", exc.getMessage());
model.addAttribute("trace", ExceptionUtils.getStackTrace(exc));

return "error";
}
model.addAttribute("custom-title", whitelabelProperties.getCustomTitle());
model.addAttribute("context-path", whitelabelProperties.getBasePath());
model.addAttribute("logo-path", whitelabelProperties.getLogoPath());
model.addAttribute("custom-css-path", whitelabelProperties.getCustomCssPath());
model.addAttribute("custom-js-path", whitelabelProperties.getCustomCssPath());

return "error";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package io.zeebe.monitor.rest;

import io.camunda.zeebe.client.ZeebeClient;
import io.zeebe.monitor.zeebe.ZeebeNotificationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
Expand All @@ -28,6 +29,7 @@
public class ProcessInstanceResource {

@Autowired private ZeebeClient zeebeClient;
@Autowired private ZeebeNotificationService zeebeNotificationService;

@RequestMapping(path = "/{key}", method = RequestMethod.DELETE)
public void cancelProcessInstance(@PathVariable("key") final long key) throws Exception {
Expand Down Expand Up @@ -56,6 +58,13 @@ public void resolveIncident(
.newUpdateRetriesCommand(dto.getJobKey())
.retries(dto.getRemainingRetries())
.send()
.exceptionally(e -> {
// catch this exception and forward to the user
zeebeNotificationService.sendZeebeClusterError(e.getMessage());
// AND continue with second Zeebe command below
return null;
})
.toCompletableFuture()
.join();
}

Expand Down
18 changes: 18 additions & 0 deletions src/main/java/io/zeebe/monitor/rest/ui/ErrorMessage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package io.zeebe.monitor.rest.ui;

public class ErrorMessage {

private String message;

public ErrorMessage(String message) {
this.message = message;
}

public String getMessage() {
return message;
}

public void setMessage(final String message) {
this.message = message;
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.zeebe.monitor.rest;
package io.zeebe.monitor.rest.ui;

public class ProcessInstanceNotification {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.zeebe.monitor.rest.ui;

public class ZeebeClusterNotification {

private Type type;
private String message;

public enum Type {
INFORMATION,
SUCCESS,
ERROR
}

public Type getType() {
return type;
}

public void setType(final Type type) {
this.type = type;
}

public String getMessage() {
return message;
}

public void setMessage(final String message) {
this.message = message;
}
}
17 changes: 15 additions & 2 deletions src/main/java/io/zeebe/monitor/zeebe/ZeebeNotificationService.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package io.zeebe.monitor.zeebe;

import io.zeebe.monitor.rest.ProcessInstanceNotification;
import io.zeebe.monitor.rest.ProcessInstanceNotification.Type;
import io.zeebe.monitor.rest.ui.ProcessInstanceNotification;
import io.zeebe.monitor.rest.ui.ProcessInstanceNotification.Type;
import io.zeebe.monitor.rest.ui.ZeebeClusterNotification;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.messaging.simp.SimpMessagingTemplate;
Expand Down Expand Up @@ -48,8 +49,20 @@ public void sendEndedProcessInstance(
sendNotification(notification);
}

public void sendZeebeClusterError(final String message) {
final ZeebeClusterNotification notification = new ZeebeClusterNotification();
notification.setMessage(message);
notification.setType(ZeebeClusterNotification.Type.ERROR);
sendNotification(notification);
}

private void sendNotification(final ProcessInstanceNotification notification) {
final var destination = basePath + "notifications/process-instance";
webSocket.convertAndSend(destination, notification);
}

private void sendNotification(final ZeebeClusterNotification notification) {
final var destination = basePath + "notifications/zeebe-cluster";
webSocket.convertAndSend(destination, notification);
}
}
95 changes: 86 additions & 9 deletions src/main/resources/public/js/app.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,85 @@
/**
* @typedef ErrorMessage
* @type {object}
* @property {string} message
*/

/**
* @typedef ProcessInstanceNotification
* @type {object}
* @property {number} processInstanceKey
* @property {number} processDefinitionKey
* @property {string} type
*/

/**
* @typedef ZeebeClusterNotification
* @type {object}
* @property {string} message
* @property {string} type
*/

/**
* @param {string} elementId - the HTML element ID to attach the text to
* @param {string} title - the title for the message
* @param {string} message - the actual text message
*/
function appendAndSowMessageToElement(elementId, title, message) {
var dataTexts = jQuery("#" + elementId + " [data-text]");
var length = dataTexts.length;
// skip duplicates
if (length > 0) {
if (jQuery(dataTexts[length - 1]).text().endsWith(message)) {
return;
}
}
// drop the top of the stack == the oldest entries
for (var i = 0; i < Math.max(0, length - 3); i++) {
jQuery(dataTexts[i]).fadeOut(dataTexts[i].remove);
}
var newTextElement = jQuery("<div/>");
newTextElement.hide();
newTextElement.attr("data-text", "");
newTextElement.append(jQuery("<strong>" + title + "</strong>"));
var textSpanElement = jQuery("<span/>");
textSpanElement.text(message);
newTextElement.append(textSpanElement);
var panelElement = jQuery("#" + elementId);
if (length === 0) {
panelElement.prepend(newTextElement);
} else {
jQuery(dataTexts[dataTexts.length - 1]).after(newTextElement);
}
panelElement.show();
newTextElement.fadeIn();
}

/**
* @param {string} message - the actual text message
*/
function showError(message) {
document.getElementById("errorText").innerHTML = message;
$('#errorPanel').show();
appendAndSowMessageToElement("errorPanel", "Error: ", message);
}

/**
* @param {string} message - the actual text message
*/
function showSuccess(message) {
document.getElementById("successText").innerHTML = message;
$('#successPanel').show();
appendAndSowMessageToElement("successPanel", "Success: ", message);
}

/**
* @param {string} message - the actual text message
*/
function showInfo(message) {
document.getElementById("infoText").innerHTML = message;
$('#infoPanel').show();
appendAndSowMessageToElement("infoPanel", "Info: ", message);
}

function showErrorResonse(xhr, ajaxOptions, thrownError) {
if (xhr.responseJSON) {
showError(xhr.responseJSON.message);
/** @type {ErrorMessage} */
let errorMessage = xhr.responseJSON;
showError(errorMessage.message);
} else {
showError(thrownError);
}
Expand Down Expand Up @@ -43,7 +107,10 @@ function connect() {
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
stompClient.subscribe(buildPath('notifications/process-instance'), function (message) {
handleMessage(JSON.parse(message.body));
handleProcessInstanceNotification(JSON.parse(message.body));
});
stompClient.subscribe(buildPath('notifications/zeebe-cluster'), function (message) {
handleZeebeClusterNotification(JSON.parse(message.body));
});
});
}
Expand All @@ -59,7 +126,10 @@ function sendMessage(msg) {
JSON.stringify(msg));
}

function handleMessage(notification) {
/**
* @param notification {ProcessInstanceNotification}
*/
function handleProcessInstanceNotification(notification) {

if (subscribedProcessInstanceKeys.includes(notification.processInstanceKey)) {
showInfo('Process instance has changed.');
Expand All @@ -70,6 +140,13 @@ function handleMessage(notification) {
}
}

/**
* @param notification {ZeebeClusterNotification}
*/
function handleZeebeClusterNotification(notification) {
showError(notification.message)
}

function subscribeForProcessInstance(key) {
subscribedProcessInstanceKeys.push(key);
}
Expand Down
12 changes: 6 additions & 6 deletions src/main/resources/templates/layout/header.html
Original file line number Diff line number Diff line change
Expand Up @@ -54,24 +54,24 @@
<div class="container-fluid">

<div id="errorPanel" class="alert alert-danger alert-dismissible fade show" style="display:none;" role="alert">
<strong>Error:</strong> <span id="errorText"></span>
<button type="button" class="close" aria-label="Close" onclick="$('.alert').hide()">
<!-- text will be inserted via jQuery at runtime -->
<button type="button" class="close" aria-label="Close" onclick="(function(){jQuery('#errorPanel [data-text]').remove(); jQuery('#errorPanel').hide()}())">
<span aria-hidden="true">&times;</span>
</button>
</div>

<div id="successPanel" class="alert alert-success alert-dismissible fade show" style="display:none;" role="alert">
<strong>Success:</strong> <span id="successText"></span>
<!-- text will be inserted via jQuery at runtime -->
(<a href="#" onClick="reload()">Refresh</a>)
<button type="button" class="close" aria-label="Close" onclick="$('.alert').hide()">
<button type="button" class="close" aria-label="Close" onclick="(function(){jQuery('#successPanel [data-text]').remove(); jQuery('#successPanel').hide()}())">
<span aria-hidden="true">&times;</span>
</button>
</div>

<div id="infoPanel" class="alert alert-primary alert-dismissible fade show" style="display:none;" role="alert">
<strong>Info:</strong> <span id="infoText"></span>
<!-- text will be inserted via jQuery at runtime -->
(<a href="#" onClick="reload()">Refresh</a>)
<button type="button" class="close" aria-label="Close" onclick="$('.alert').hide()">
<button type="button" class="close" aria-label="Close" onclick="(function(){jQuery('#infoPanel [data-text]').remove(); jQuery('#infoPanel').hide()}())">
<span aria-hidden="true">&times;</span>
</button>
</div>
Loading

0 comments on commit f338b9e

Please sign in to comment.