Skip to content

Commit

Permalink
MassTransit retry functionality (#2220)
Browse files Browse the repository at this point in the history
* don't show unmonitored endpoints on the monitoring screen, even if they have failed messages
* show masstransit message if pending retries is enabled
* Adding masstransit connector tab
* Hide "Open in ServiceInsight" and "Flow diagram" in ServicePulse
* replace non-production references for masstransit with early access
* allow for no monitoring url in settings
* Don't add proxy config for monitoring if monitoring is disabled

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: John Simons <john.simons@particular.net>
  • Loading branch information
3 people authored Jan 24, 2025
1 parent d6a0b3a commit 97543c0
Show file tree
Hide file tree
Showing 26 changed files with 449 additions and 132 deletions.
7 changes: 6 additions & 1 deletion src/Frontend/src/components/LicenseNotifications.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ import { useShowToast } from "@/composables/toast";
import { TYPE } from "vue-toastification";
import routeLinks from "@/router/routeLinks";
import { useRouter } from "vue-router";
import { useConfiguration } from "@/composables/configuration";
const router = useRouter();
const { license, getOrUpdateLicenseStatus } = useLicense();
const configuration = useConfiguration();
function displayWarningMessage(licenseStatus: LicenseStatus) {
const configurationRootLink = router.resolve(routeLinks.configuration.root).href;
switch (licenseStatus) {
Expand All @@ -19,7 +22,9 @@ function displayWarningMessage(licenseStatus: LicenseStatus) {
break;
}
case "ValidWithExpiringTrial": {
const trialExpiring = `<div><strong>Non-production development license expiring</strong><div>Your non-production development license will expire soon. To continue using the Particular Service Platform you'll need to extend your license.</div><a href="${license.license_extension_url}" class="btn btn-warning"><i class="fa fa-external-link-alt"></i> Extend your license</a><a href="${configurationRootLink}" class="btn btn-light">View license details</a></div>`;
const trialExpiring = configuration.value?.mass_transit_connector
? `<div><strong>Early Access license expiring</strong><div>Your Early Access license will expire soon. To continue using the Particular Service Platform you'll need to extend your license.</div><a href="${license.license_extension_url}" class="btn btn-warning"><i class="fa fa-external-link-alt"></i> Extend your license</a><a href="${configurationRootLink}" class="btn btn-light">View license details</a></div>`
: `<div><strong>Non-production development license expiring</strong><div>Your non-production development license will expire soon. To continue using the Particular Service Platform you'll need to extend your license.</div><a href="${license.license_extension_url}" class="btn btn-warning"><i class="fa fa-external-link-alt"></i> Extend your license</a><a href="${configurationRootLink}" class="btn btn-light">View license details</a></div>`;
useShowToast(TYPE.WARNING, "", trialExpiring, true);
break;
}
Expand Down
5 changes: 4 additions & 1 deletion src/Frontend/src/components/PageFooter.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { monitoringUrl, serviceControlUrl } from "../composables/serviceServiceC
import { license, licenseStatus } from "../composables/serviceLicense";
import { LicenseStatus } from "@/resources/LicenseInfo";
import routeLinks from "@/router/routeLinks";
import { useConfiguration } from "@/composables/configuration";
const isMonitoringEnabled = computed(() => {
return monitoringUrl.value !== "!" && monitoringUrl.value !== "" && monitoringUrl.value !== null && monitoringUrl.value !== undefined;
Expand All @@ -17,6 +18,8 @@ const scAddressTooltip = computed(() => {
const scMonitoringAddressTooltip = computed(() => {
return `Monitoring URL ${monitoringUrl.value}`;
});
const configuration = useConfiguration();
</script>

<template>
Expand Down Expand Up @@ -64,7 +67,7 @@ const scMonitoringAddressTooltip = computed(() => {
</template>
</div>
</div>
<template v-if="license.license_status !== LicenseStatus.Unavailable && licenseStatus.isTrialLicense">
<template v-if="license.license_status !== LicenseStatus.Unavailable && !configuration?.mass_transit_connector && licenseStatus.isTrialLicense">
<div class="row trialLicenseBar">
<div role="status" aria-label="trial license bar information">
<RouterLink :to="routeLinks.configuration.license.link">{{ license.license_type }} license</RouterLink>, non-production use only
Expand Down
99 changes: 99 additions & 0 deletions src/Frontend/src/components/configuration/MassTransitConnector.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<script setup lang="ts">
import { useConfiguration } from "@/composables/configuration";
import moment from "moment";
const configuration = useConfiguration();
// "Wed, Jan 15th 2025 10:56:21 +10:00",
function formatDate(date: string) {
return moment(date).local().format("LLLL"); //.format("ddd, MMM Do YYYY HH:mm:ss Z");
}
</script>

<template>
<div class="box" v-if="configuration?.mass_transit_connector !== undefined">
<div class="row margin-bottom-10">
<h4>
Connector Version: <span class="version-format">{{ configuration.mass_transit_connector.version }}</span>
</h4>
</div>
<div class="row margin-bottom-10">
<h4>List of error queues configured in the connector.</h4>
<div class="queues-container">
<div class="row margin-gap hover-highlight" v-for="queue in configuration.mass_transit_connector.error_queues" :key="queue.name">
<div :title="queue.name">{{ queue.name }}</div>
<div class="error-color" v-if="!queue.ingesting">Not ingesting</div>
<div class="ok-color" v-else>Ok</div>
</div>
</div>
</div>
<div class="row">
<h4>The entries below are the most recent warning and error-level events recorded on the ServiceControl Connector.</h4>
<div class="logs-container">
<div class="row margin-gap hover-highlight" v-for="log in [...configuration.mass_transit_connector.logs].reverse()" :key="log.date">
<div class="col-2">{{ formatDate(log.date) }}</div>
<div class="col-1" :class="`${log.level.toLowerCase()}-color`">{{ log.level }}</div>
<div class="col-9" :class="`${log.level.toLowerCase()}-color`">
<pre>{{ log.message }}</pre>
</div>
</div>
</div>
</div>
</div>
<div class="box" v-else>
<p>MassTransit Connector for ServiceControl is not configured.</p>
<p><a target="_blank" href="https://particular.net/learn-more-about-masstransit-connector">Learn more about the MassTransit Connector.</a></p>
</div>
</template>

<style scoped>
.hover-highlight:hover {
background-color: #ededed;
}
.margin-gap {
margin-bottom: 3px;
}
.queues-container {
max-width: 100%;
width: fit-content;
padding: 0.75rem;
}
.queues-container .row {
display: grid;
grid-template-columns: 5fr minmax(10em, 1fr);
}
.queues-container .row div {
overflow-wrap: anywhere;
}
.logs-container {
padding: 0.75rem;
}
.version-format {
font-weight: bold;
}
.box > .row:not(:last-child) {
padding-bottom: 0.5rem;
border-bottom: 1px solid #ccc;
margin-bottom: 0.5rem;
}
.logs-container pre {
all: revert;
margin: 0;
font-size: 0.9rem;
overflow-wrap: break-word;
text-wrap: auto;
}
.warning-color {
color: var(--bs-warning);
}
.error-color {
color: var(--bs-danger);
}
.ok-color {
color: var(--bs-success);
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { connectionState } from "@/composables/serviceServiceControl";
import BusyIndicator from "../BusyIndicator.vue";
import ExclamationMark from "./../../components/ExclamationMark.vue";
import convertToWarningLevel from "@/components/configuration/convertToWarningLevel";
import { useConfiguration } from "@/composables/configuration";
import { typeText } from "@/resources/LicenseInfo";
// This is needed because the ConfigurationView.vue routerView expects this event.
// The event is only actually emitted on the RetryRedirects.vue component
Expand All @@ -18,6 +20,8 @@ defineEmits<{
const loading = computed(() => {
return !license;
});
const configuration = useConfiguration();
</script>

<template>
Expand All @@ -31,7 +35,7 @@ const loading = computed(() => {
<div class="box">
<div class="row">
<div class="license-info">
<div><b>Platform license type:</b> {{ license.license_type }}{{ license.licenseEdition }}</div>
<div><b>Platform license type:</b> {{ typeText(license, configuration) }}{{ license.licenseEdition }}</div>

<template v-if="licenseStatus.isSubscriptionLicense">
<div>
Expand Down
15 changes: 3 additions & 12 deletions src/Frontend/src/components/failedmessages/DeletedMessages.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onBeforeMount, onMounted, onUnmounted, ref, watch } from "vue";
import { onMounted, onUnmounted, ref, watch } from "vue";
import { licenseStatus } from "../../composables/serviceLicense";
import { connectionState } from "../../composables/serviceServiceControl";
import { usePatchToServiceControl, useTypedFetchFromServiceControl } from "../../composables/serviceServiceControlUrls";
Expand All @@ -13,14 +13,13 @@ import ConfirmDialog from "../ConfirmDialog.vue";
import PaginationStrip from "../../components/PaginationStrip.vue";
import moment from "moment";
import { ExtendedFailedMessage } from "@/resources/FailedMessage";
import Configuration from "@/resources/Configuration";
import { TYPE } from "vue-toastification";
import FailureGroup from "@/resources/FailureGroup";
import { useConfiguration } from "@/composables/configuration";
let pollingFaster = false;
let refreshInterval: number | undefined;
const perPage = 50;
const configuration = ref<Configuration | null>(null);
const route = useRoute();
const groupId = ref<string>(route.params.groupId as string);
Expand All @@ -36,6 +35,7 @@ const messageList = ref<IMessageList | undefined>();
const messages = ref<ExtendedFailedMessage[]>([]);
watch(pageNumber, () => loadMessages());
const configuration = useConfiguration();
function loadMessages() {
let startDate = new Date(0);
Expand Down Expand Up @@ -162,11 +162,6 @@ function periodChanged(period: PeriodOption) {
loadMessages();
}
async function getConfiguration() {
const [, data] = await useTypedFetchFromServiceControl<Configuration>("configuration");
configuration.value = data;
}
function isRestoreInProgress() {
return messages.value.some((message) => message.restoreInProgress);
}
Expand Down Expand Up @@ -196,10 +191,6 @@ onBeforeRouteLeave(() => {
groupName.value = "";
});
onBeforeMount(() => {
getConfiguration();
});
onUnmounted(() => {
if (refreshInterval != null) {
window.clearInterval(refreshInterval);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { computed, onMounted, ref, watch } from "vue";
interface MessageHeader {
key: string;
value: string;
value?: string;
isChanged: boolean;
isMarkedAsRemoved: boolean;
isLocked: boolean;
Expand All @@ -14,7 +14,7 @@ const settings = defineProps<{
header: MessageHeader;
}>();
let origHeaderValue: string;
let origHeaderValue: string | undefined;
const header = ref<MessageHeader>(settings.header);
const headerValue = computed(() => settings.header.value);
Expand Down
68 changes: 51 additions & 17 deletions src/Frontend/src/components/failedmessages/EditRetryDialog.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,30 @@ const props = defineProps<{
configuration: EditAndRetryConfig;
}>();
const panel = ref();
const localMessage = ref();
interface LocalMessageState {
isBodyChanged: boolean;
isBodyEmpty: boolean;
isContentTypeSupported: boolean;
bodyContentType: string | undefined;
bodyUnavailable: boolean;
isEvent: boolean;
retried: boolean;
headers: HeaderWithEditing[];
messageBody: string;
}
const panel = ref(0);
const localMessage = ref<LocalMessageState>({
isBodyChanged: false,
isBodyEmpty: false,
isContentTypeSupported: false,
bodyContentType: undefined,
bodyUnavailable: true,
isEvent: false,
retried: false,
headers: [],
messageBody: "",
});
let origMessageBody: string;
const showEditAndRetryConfirmation = ref(false);
Expand Down Expand Up @@ -79,10 +101,12 @@ function findHeadersByKey(key: string) {
function getContentType() {
const header = findHeadersByKey("NServiceBus.ContentType");
return header.value;
return header?.value;
}
function isContentTypeSupported(contentType: string) {
function isContentTypeSupported(contentType: string | undefined) {
if (contentType === undefined) return false;
if (contentType.startsWith("text/")) return true;
const charsetUtf8 = "; charset=utf-8";
Expand All @@ -106,7 +130,7 @@ function isContentTypeSupported(contentType: string) {
function getMessageIntent() {
const intent = findHeadersByKey("NServiceBus.MessageIntent");
return intent.value;
return intent?.value;
}
function removeHeadersMarkedAsRemoved() {
Expand All @@ -127,7 +151,17 @@ async function retryEditedMessage() {
function initializeMessageBodyAndHeaders() {
origMessageBody = props.message.messageBody;
localMessage.value = props.message;
localMessage.value = {
isBodyChanged: false,
isBodyEmpty: false,
isContentTypeSupported: false,
bodyContentType: undefined,
bodyUnavailable: props.message.bodyUnavailable,
isEvent: false,
retried: props.message.retried,
headers: props.message.headers.map((header: Header) => ({ ...header })) as HeaderWithEditing[],
messageBody: props.message.messageBody,
};
localMessage.value.isBodyEmpty = false;
localMessage.value.isBodyChanged = false;
Expand All @@ -136,7 +170,7 @@ function initializeMessageBodyAndHeaders() {
localMessage.value.isContentTypeSupported = isContentTypeSupported(contentType);
const messageIntent = getMessageIntent();
localMessage.value.isEvent = messageIntent.value === "Publish";
localMessage.value.isEvent = messageIntent === "Publish";
for (let index = 0; index < props.message.headers.length; index++) {
const header: HeaderWithEditing = props.message.headers[index] as HeaderWithEditing;
Expand Down Expand Up @@ -191,15 +225,15 @@ onMounted(() => {
</div>
<div class="row msg-editor-content">
<div class="col-sm-12 no-side-padding">
<div class="alert alert-warning" v-if="localMessage?.isEvent">
<div class="alert alert-warning" v-if="localMessage.isEvent">
<div class="col-sm-12">
<i class="fa fa-exclamation-circle"></i> This message is an event. If it was already successfully handled by other subscribers, editing it now has the risk of changing the semantic meaning of the event and may result in
altering the system behavior.
</div>
</div>
<div class="alert alert-warning" v-if="!localMessage?.isContentTypeSupported || localMessage?.bodyUnavailable">
<div class="alert alert-warning" v-if="!localMessage.isContentTypeSupported || localMessage.bodyUnavailable">
<div role="status" aria-label="cannot edit message body" class="col-sm-12">
<i class="fa fa-exclamation-circle"></i> Message body cannot be edited because content type "{{ localMessage?.bodyContentType }}" is not supported. Only messages with content types "application/json" and "text/xml" can be
<i class="fa fa-exclamation-circle"></i> Message body cannot be edited because content type "{{ localMessage.bodyContentType }}" is not supported. Only messages with content types "application/json" and "text/xml" can be
edited.
</div>
</div>
Expand All @@ -208,16 +242,16 @@ onMounted(() => {
</div>
<table role="tabpanel" class="table" v-if="panel === 1">
<tbody>
<tr class="interactiveList" v-for="header in localMessage?.headers" :key="header.key">
<tr class="interactiveList" v-for="header in localMessage.headers" :key="header.key">
<MessageHeader :header="header"></MessageHeader>
</tr>
</tbody>
</table>
<div role="tabpanel" v-if="panel === 2 && !localMessage?.bodyUnavailable" style="height: calc(100% - 260px)">
<textarea aria-label="message body" class="form-control" :disabled="!localMessage?.isContentTypeSupported" v-model="localMessage.messageBody"></textarea>
<span class="empty-error" v-if="localMessage?.isBodyEmpty"><i class="fa fa-exclamation-triangle"></i> Message body cannot be empty</span>
<span class="reset-body" v-if="localMessage?.isBodyChanged"><i class="fa fa-undo" v-tippy="`Reset changes`"></i> <a @click="resetBodyChanges()" href="javascript:void(0)">Reset changes</a></span>
<div class="alert alert-info" v-if="localMessage?.panel === 2 && localMessage.bodyUnavailable">{{ localMessage.bodyUnavailable }}</div>
<div role="tabpanel" v-if="panel === 2 && !localMessage.bodyUnavailable" style="height: calc(100% - 260px)">
<textarea aria-label="message body" class="form-control" :disabled="!localMessage.isContentTypeSupported" v-model="localMessage.messageBody"></textarea>
<span class="empty-error" v-if="localMessage.isBodyEmpty"><i class="fa fa-exclamation-triangle"></i> Message body cannot be empty</span>
<span class="reset-body" v-if="localMessage.isBodyChanged"><i class="fa fa-undo" v-tippy="`Reset changes`"></i> <a @click="resetBodyChanges()" href="javascript:void(0)">Reset changes</a></span>
<div class="alert alert-info" v-if="panel === 2 && localMessage.bodyUnavailable">{{ localMessage.bodyUnavailable }}</div>
</div>
</div>
</div>
Expand All @@ -226,7 +260,7 @@ onMounted(() => {
</div>
<div class="modal-footer" v-if="!showEditAndRetryConfirmation && !showCancelConfirmation">
<button class="btn btn-default" @click="confirmCancel()">Cancel</button>
<button class="btn btn-primary" :disabled="localMessage?.isBodyEmpty || localMessage?.bodyUnavailable" @click="confirmEditAndRetry()">Retry</button>
<button class="btn btn-primary" :disabled="localMessage.isBodyEmpty || localMessage.bodyUnavailable" @click="confirmEditAndRetry()">Retry</button>
</div>
<div class="modal-footer cancel-confirmation" v-if="showCancelConfirmation">
<div>Are you sure you want to cancel? Any changes you made will be lost.</div>
Expand Down
Loading

0 comments on commit 97543c0

Please sign in to comment.