Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7aa8157
Initial implementation of thread dumps
Josh-Matsuoka May 30, 2025
0715c12
Various fixes
Josh-Matsuoka Jun 2, 2025
67d3b99
Refresh on delete, rename uuid
Josh-Matsuoka Jun 2, 2025
778f3ed
formatting, eslint
Josh-Matsuoka Jun 2, 2025
d960856
formatting
Josh-Matsuoka Jun 3, 2025
da6c526
Update for removal of embedded thread dump content, addition of lastM…
Josh-Matsuoka Jun 18, 2025
17c728b
Fix deletion warning modal
Josh-Matsuoka Jun 19, 2025
64e9eff
Merge changes from upstream, add notification handler
Josh-Matsuoka Jun 27, 2025
c66811a
Review feedback, add tests
Josh-Matsuoka Jul 4, 2025
185707e
Render thread dump redirect button, enable if thread dump is ready
Josh-Matsuoka Jul 4, 2025
b80fca1
eslint, prettier
Josh-Matsuoka Jul 4, 2025
27a80c0
Review feedback
Josh-Matsuoka Jul 18, 2025
5d13a1c
prettier, eslint, fix test
Josh-Matsuoka Jul 30, 2025
cec4129
cleanup
Josh-Matsuoka Jul 30, 2025
6763f43
use newer test assertion function
andrewazores Jul 31, 2025
08eee91
Review feedback
Josh-Matsuoka Aug 1, 2025
e3f4df1
Merge remote-tracking branch 'jmatsuok/thread-dumps' into thread-dumps
Josh-Matsuoka Aug 1, 2025
1779334
Set filename query parameter
Josh-Matsuoka Aug 28, 2025
e909abd
Merge remote-tracking branch 'upstream/main' into thread-dumps
Josh-Matsuoka Aug 28, 2025
5bdd34e
prettier
Josh-Matsuoka Aug 28, 2025
bfe7d18
Account for existing query parameters in download url handling
Josh-Matsuoka Aug 28, 2025
7c206c8
Add missing dependency array to fix unnecessary API queries
Josh-Matsuoka Aug 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions locales/en/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -364,14 +364,29 @@
},
"DATETIME": "Date and Time"
},
"Diagnostics": {
"THREAD_DUMPS_TAB_TITLE": "Thread Dumps"
},
"DiagnosticsCard": {
"DIAGNOSTICS_ACTION_FAILURE": "Diagnostics Failure: {{kind}}",
"DIAGNOSTICS_CARD_DESCRIPTION": "Perform diagnostic operations on the target.",
"DIAGNOSTICS_CARD_DESCRIPTION_FULL": "Perform diagonstic operations from a list of supported operations on the target.",
"DIAGNOSTICS_CARD_TITLE": "Diagnostics",
"DIAGNOSTICS_GC_BUTTON": "Start Garbage Collection",
"DIAGNOSTICS_GC_BUTTON": "Invoke Garbage Collection",
"DIAGNOSTICS_THREAD_DUMP_BUTTON": "Invoke Thread Dump",
"DIAGONSTICS_THREAD_REDIRECT_BUTTON": "View collected Thread Dumps",
"KINDS": {
"GC": "Garbage Collection"
"GC": "Garbage Collection",
"THREAD_DUMP": "Thread Dump"
}
},
"ThreadDumps": {
"SEARCH_PLACEHOLDER": "Search Thread Dumps",
"DELETION_FAILURE_CATEGORY": "Thread Dump Deletion Failure",
"DELETION_FAILURE_MESSAGE": "No Thread Dump to delete.",
"ARIA_LABELS": {
"ROW_ACTION": "thread-dump-action-menu",
"SEARCH_INPUT": "thread-dump-search-input"
}
},
"DurationFilter": {
Expand Down Expand Up @@ -543,7 +558,8 @@
"NavGroups": {
"CONSOLE": "Console",
"FLIGHT_RECORDER": "Flight Recorder",
"OVERVIEW": "Overview"
"OVERVIEW": "Overview",
"DIAGNOSTICS": "Diagnostics"
}
},
"RuleDeleteWarningModal": {
Expand Down
84 changes: 74 additions & 10 deletions src/app/Dashboard/Diagnostics/DiagnosticsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import {
DashboardCardSizes,
DashboardCardDescriptor,
} from '@app/Dashboard/types';
import { CryostatLink } from '@app/Shared/Components/CryostatLink';
import { NotificationCategory } from '@app/Shared/Services/api.types';
import { NotificationsContext } from '@app/Shared/Services/Notifications.service';
import { FeatureLevel } from '@app/Shared/Services/service.types';
import { ServiceContext } from '@app/Shared/Services/Services';
Expand All @@ -37,9 +39,12 @@ import {
EmptyStateVariant,
EmptyStateHeader,
EmptyStateFooter,
ActionList,
Tooltip,
} from '@patternfly/react-core';
import { WrenchIcon } from '@patternfly/react-icons';
import { ListIcon, WrenchIcon } from '@patternfly/react-icons';
import * as React from 'react';
import { concatMap, filter, first } from 'rxjs/operators';
import { DashboardCard } from '../DashboardCard';

export interface DiagnosticsCardProps extends DashboardCardTypeProps {}
Expand All @@ -50,6 +55,7 @@ export const DiagnosticsCard: DashboardCardFC<DiagnosticsCardProps> = (props) =>
const notifications = React.useContext(NotificationsContext);
const addSubscription = useSubscriptions();
const [running, setRunning] = React.useState(false);
const [threadDumpReady, setThreadDumpReady] = React.useState(false);

const handleError = React.useCallback(
(kind, error) => {
Expand All @@ -58,6 +64,30 @@ export const DiagnosticsCard: DashboardCardFC<DiagnosticsCardProps> = (props) =>
[notifications, t],
);

React.useEffect(() => {
addSubscription(
serviceContext.target
.target()
.pipe(
filter((target) => !!target),
first(),
concatMap(() => serviceContext.api.getThreadDumps()),
)
.subscribe({
next: (dumps) => (dumps.length > 0 ? setThreadDumpReady(true) : setThreadDumpReady(false)),
error: () => setThreadDumpReady(false),
}),
);
}, [addSubscription, serviceContext.api, serviceContext.target, setThreadDumpReady]);

React.useEffect(() => {
addSubscription(
serviceContext.notificationChannel.messages(NotificationCategory.ThreadDumpSuccess).subscribe(() => {
setThreadDumpReady(true);
}),
);
}, [addSubscription, serviceContext.notificationChannel, setThreadDumpReady]);

const handleGC = React.useCallback(() => {
setRunning(true);
addSubscription(
Expand All @@ -68,6 +98,19 @@ export const DiagnosticsCard: DashboardCardFC<DiagnosticsCardProps> = (props) =>
);
}, [addSubscription, serviceContext.api, handleError, setRunning, t]);

const handleThreadDump = React.useCallback(() => {
setRunning(true);
addSubscription(
serviceContext.api.runThreadDump(true).subscribe({
error: (err) => handleError(t('DiagnosticsCard.KINDS.THREADS'), err),
complete: () => {
setRunning(false);
setThreadDumpReady(true);
},
}),
);
}, [addSubscription, serviceContext.api, handleError, setRunning, t]);

const header = React.useMemo(() => {
return (
<CardHeader actions={{ actions: <>{...props.actions || []}</>, hasNoOffset: false, className: undefined }}>
Expand Down Expand Up @@ -98,15 +141,36 @@ export const DiagnosticsCard: DashboardCardFC<DiagnosticsCardProps> = (props) =>
/>
<EmptyStateBody>{t('DiagnosticsCard.DIAGNOSTICS_CARD_DESCRIPTION')}</EmptyStateBody>
<EmptyStateFooter>
<Button
variant="primary"
onClick={handleGC}
spinnerAriaValueText="Invoke GC"
spinnerAriaLabel="invoke-gc"
isLoading={running}
>
{t('DiagnosticsCard.DIAGNOSTICS_GC_BUTTON')}
</Button>
<ActionList>
<Button
variant="primary"
onClick={handleGC}
spinnerAriaValueText="Invoke GC"
spinnerAriaLabel="invoke-gc"
isLoading={running}
>
{t('DiagnosticsCard.DIAGNOSTICS_GC_BUTTON')}
</Button>
</ActionList>
<ActionList>
<Button
variant="primary"
onClick={handleThreadDump}
spinnerAriaValueText="Invoke Thread Dump"
spinnerAriaLabel="invoke-thread-dump"
isLoading={running}
>
{t('DiagnosticsCard.DIAGNOSTICS_THREAD_DUMP_BUTTON')}
</Button>
<Tooltip content={t('DiagnosticsCard.DIAGNOSTICS_THREAD_DUMP_TABLE_TOOLTIP')}>
<Button
variant="primary"
isAriaDisabled={!threadDumpReady}
component={(props) => <CryostatLink {...props} to="/diagnostics" />}
icon={<ListIcon />}
/>
</Tooltip>
</ActionList>
</EmptyStateFooter>
</EmptyState>
</Bullseye>
Expand Down
69 changes: 69 additions & 0 deletions src/app/Diagnostics/Diagnostics.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright The Cryostat Authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TargetView } from '@app/TargetView/TargetView';
import { getActiveTab, switchTab } from '@app/utils/utils';
import { Card, CardBody, Tab, Tabs, TabTitleText } from '@patternfly/react-core';
import { t } from 'i18next';
import * as React from 'react';
import { useLocation, useNavigate } from 'react-router-dom-v5-compat';
import { ThreadDumpsTable } from './ThreadDumpsTable';

enum DiagnosticsTab {
THREAD_DUMPS = 'thread-dumps',
}

export interface DiagnosticsProps {}

export const Diagnostics: React.FC<DiagnosticsProps> = ({ ...props }) => {
const { search, pathname } = useLocation();
const navigate = useNavigate();

const activeTab = React.useMemo(() => {
return getActiveTab(search, 'tab', Object.values(DiagnosticsTab), DiagnosticsTab.THREAD_DUMPS);
}, [search]);

const onTabSelect = React.useCallback(
(_: React.MouseEvent, key: string | number) =>
switchTab(navigate, pathname, search, { tabKey: 'tab', tabValue: `${key}` }),
[navigate, pathname, search],
);

const cardBody = React.useMemo(
() => (
<Tabs id="threadDumps" activeKey={activeTab} onSelect={onTabSelect} unmountOnExit>
<Tab
id="threadDumps"
eventKey={DiagnosticsTab.THREAD_DUMPS}
title={<TabTitleText>{t('Diagnostics.THREAD_DUMPS_TAB_TITLE')}</TabTitleText>}
data-quickstart-id="thread-dumps-tab"
>
<ThreadDumpsTable />
</Tab>
</Tabs>
),
[activeTab, onTabSelect],
);

return (
<TargetView {...props} pageTitle="Diagnostics">
<Card isFullHeight>
<CardBody isFilled>{cardBody}</CardBody>
</Card>
</TargetView>
);
};

export default Diagnostics;
Loading
Loading