diff --git a/web/locales/en/plugin__distributed-tracing-console-plugin.json b/web/locales/en/plugin__distributed-tracing-console-plugin.json index 8498ea1..21b7d8d 100644 --- a/web/locales/en/plugin__distributed-tracing-console-plugin.json +++ b/web/locales/en/plugin__distributed-tracing-console-plugin.json @@ -11,6 +11,7 @@ "Trace details": "Trace details", "Trace": "Trace", "Tracing": "Tracing", + "Ask OpenShift Lightspeed": "Ask OpenShift Lightspeed", "Limit traces": "Limit traces", "Hide graph": "Hide graph", "Show graph": "Show graph", diff --git a/web/package-lock.json b/web/package-lock.json index 8fdb517..e823a46 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -39,6 +39,7 @@ "@openshift-console/dynamic-plugin-sdk": "^4.19.0", "@openshift-console/dynamic-plugin-sdk-webpack": "^4.19.0", "@types/jest": "^28.1.4", + "@types/js-yaml": "^4.0.9", "@types/node": "^18.0.0", "@types/react": "^17.0.37", "@types/react-helmet": "^6.1.4", @@ -54,6 +55,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.29.1", "eslint-plugin-react-hooks": "^4.6.2", + "js-yaml": "^4.1.0", "mocha-junit-reporter": "^2.2.0", "mochawesome": "^7.1.3", "mochawesome-merge": "^4.3.0", @@ -6862,6 +6864,13 @@ "pretty-format": "^28.0.0" } }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", diff --git a/web/package.json b/web/package.json index 7bc1cf6..49b83d8 100644 --- a/web/package.json +++ b/web/package.json @@ -25,6 +25,7 @@ "@openshift-console/dynamic-plugin-sdk": "^4.19.0", "@openshift-console/dynamic-plugin-sdk-webpack": "^4.19.0", "@types/jest": "^28.1.4", + "@types/js-yaml": "^4.0.9", "@types/node": "^18.0.0", "@types/react": "^17.0.37", "@types/react-helmet": "^6.1.4", @@ -40,6 +41,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-react": "^7.29.1", "eslint-plugin-react-hooks": "^4.6.2", + "js-yaml": "^4.1.0", "mocha-junit-reporter": "^2.2.0", "mochawesome": "^7.1.3", "mochawesome-merge": "^4.3.0", diff --git a/web/src/components/PersesWrapper.css b/web/src/components/PersesWrapper.css index 7fe19d7..2a1ade1 100644 --- a/web/src/components/PersesWrapper.css +++ b/web/src/components/PersesWrapper.css @@ -31,3 +31,7 @@ .MuiMenu-root .MuiMenuItem-root:hover { background-color: var(--pf-t--global--background--color--action--plain--hover); } + +.MuiSvgIcon-root { + color: var(--pf-t--global--text--color--regular); +} diff --git a/web/src/hooks/ols.ts b/web/src/hooks/ols.ts new file mode 100644 index 0000000..d1d204f --- /dev/null +++ b/web/src/hooks/ols.ts @@ -0,0 +1,27 @@ +import { Extension, ExtensionDeclaration } from '@openshift-console/dynamic-plugin-sdk/lib/types'; + +export type Attachment = { + attachmentType: AttachmentTypes; + isEditable?: boolean; + kind: string; + name: string; + namespace: string; + originalValue?: string; + ownerName?: string; + value: string; +}; + +export enum AttachmentTypes { + YAML = 'YAML', +} + +export type OpenOLSHandlerProps = { + contextId: string; + provider: () => (prompt?: string, attachments?: Attachment[]) => void; +}; + +type OpenOLSHandlerExtension = ExtensionDeclaration<'console.action/provider', OpenOLSHandlerProps>; + +// Type guard for OpenShift Lightspeed open handler extensions +export const isOpenOLSHandlerExtension = (e: Extension): e is OpenOLSHandlerExtension => + e.type === 'console.action/provider' && e.properties?.contextId === 'ols-open-handler'; diff --git a/web/src/pages/TraceDetailPage/TraceDetailPage.tsx b/web/src/pages/TraceDetailPage/TraceDetailPage.tsx index 73fa927..cd33830 100644 --- a/web/src/pages/TraceDetailPage/TraceDetailPage.tsx +++ b/web/src/pages/TraceDetailPage/TraceDetailPage.tsx @@ -1,5 +1,13 @@ import * as React from 'react'; -import { Breadcrumb, BreadcrumbItem, Divider, PageSection, Title } from '@patternfly/react-core'; +import { + Breadcrumb, + BreadcrumbItem, + Button, + Divider, + PageSection, + Title, + Tooltip, +} from '@patternfly/react-core'; import { Helmet } from 'react-helmet'; import { useTranslation } from 'react-i18next'; import { Link, useLocation, useParams } from 'react-router-dom-v5-compat'; @@ -16,6 +24,10 @@ import { memo } from 'react'; import { linkToSpan, linkToTrace, spanAttributeLinks } from '../../links'; import { StringParam, useQueryParam } from 'use-query-params'; import './TraceDetailPage.css'; +import { MagicIcon } from '@patternfly/react-icons'; +import { dump as dumpYAML } from 'js-yaml'; +import { useResolvedExtensions } from '@openshift-console/dynamic-plugin-sdk'; +import { AttachmentTypes, isOpenOLSHandlerExtension, OpenOLSHandlerProps } from '../../hooks/ols'; function TraceDetailPage() { return ( @@ -37,6 +49,10 @@ function TraceDetailPageBody() { const [tempo] = useTempoInstance(); const location = useLocation(); const [selectedSpanId] = useQueryParam('selectSpan', StringParam); + const [extensions, resolved] = useResolvedExtensions(isOpenOLSHandlerExtension); + const useOpenOLS = resolved + ? (extensions[0]?.properties?.provider as OpenOLSHandlerProps['provider']) + : undefined; return (
: undefined, + }} definition={{ kind: 'Panel', spec: { @@ -130,3 +149,63 @@ function useTraceName(): string { // return traceId if span is not loaded or root span is not found return traceId ?? ''; } + +const MAX_TRACE_SIZE_MB = 1; + +interface LightspeedButtonProps { + useOpenOLS: OpenOLSHandlerProps['provider']; +} + +function LightspeedButton({ useOpenOLS }: LightspeedButtonProps) { + const { t } = useTranslation('plugin__distributed-tracing-console-plugin'); + const { queryResults } = useDataQueries('TraceQuery'); + const traceName = useTraceName(); + const trace = queryResults[0]?.data?.trace ?? ''; + const traceYaml = React.useMemo(() => { + return dumpYAML(trace, { lineWidth: -1 }).trim(); + }, [trace]); + const openOLS = useOpenOLS(); + + const handleTraceAISummaryClick = () => { + const traceAttachment = { + attachmentType: AttachmentTypes.YAML, + kind: 'Trace', + name: traceName, + value: traceYaml, + namespace: '', + }; + openOLS('Analyze this trace in my OpenShift cluster and highlight any errors and outliers.', [ + traceAttachment, + ]); + + // Workaround to trigger resizing of Lightspeed UI input field + setTimeout(() => { + window.dispatchEvent(new Event('resize')); + }, 0); + }; + + // Sanity check to avoid sending large traces to the LLM + if (traceYaml.length > MAX_TRACE_SIZE_MB * 1024 * 1024) { + const errorMsg = t( + 'Trace is too large to be analyzed by OpenShift Lightspeed. Max size is {{max}} MB.', + { max: MAX_TRACE_SIZE_MB }, + ); + + return ( + +