diff --git a/package.json b/package.json
index dd6ba11bc16..54a1d756b6c 100644
--- a/package.json
+++ b/package.json
@@ -271,6 +271,7 @@
"@formatjs/intl-durationformat": "^0.7.0",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/alerting": "workspace:*",
+ "@grafana/assistant": "0.0.7",
"@grafana/aws-sdk": "0.7.1",
"@grafana/azure-sdk": "0.0.7",
"@grafana/data": "workspace:*",
diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx
index e31ba38cabf..89803a457bb 100644
--- a/public/app/features/logs/components/panel/LogLine.test.tsx
+++ b/public/app/features/logs/components/panel/LogLine.test.tsx
@@ -14,6 +14,11 @@ import { defaultProps, defaultValue } from './__mocks__/LogListContext';
import { LogListModel } from './processing';
import { LogLineVirtualization } from './virtualization';
+jest.mock('@grafana/assistant', () => ({
+ ...jest.requireActual('@grafana/assistant'),
+ useAssistant: jest.fn(() => [true, jest.fn()]),
+}));
+
jest.mock('./LogListContext');
jest.mock('../LogDetails');
diff --git a/public/app/features/logs/components/panel/LogLineDetails.test.tsx b/public/app/features/logs/components/panel/LogLineDetails.test.tsx
index 2ce960fdd86..112ea2531ba 100644
--- a/public/app/features/logs/components/panel/LogLineDetails.test.tsx
+++ b/public/app/features/logs/components/panel/LogLineDetails.test.tsx
@@ -23,6 +23,13 @@ import { LogLineDetails, Props } from './LogLineDetails';
import { LogListContext, LogListContextData } from './LogListContext';
import { defaultValue } from './__mocks__/LogListContext';
+jest.mock('@grafana/assistant', () => {
+ return {
+ ...jest.requireActual('@grafana/assistant'),
+ useAssistant: jest.fn().mockReturnValue([true, jest.fn()]),
+ };
+});
+
jest.mock('@grafana/runtime', () => {
return {
...jest.requireActual('@grafana/runtime'),
diff --git a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx
index a71b64fc31b..9a4e6eedce0 100644
--- a/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx
+++ b/public/app/features/logs/components/panel/LogLineDetailsHeader.tsx
@@ -37,6 +37,8 @@ export const LogLineDetailsHeader = ({ focusLogLine, log, search, onSearch }: Pr
onPinLine,
onUnpinLine,
wrapLogMessage,
+ isAssistantAvailable,
+ openAssistantByLog,
} = useLogListContext();
const pinned = useLogIsPinned(log);
const styles = useStyles2(getStyles, detailsMode, wrapLogMessage);
@@ -149,6 +151,16 @@ export const LogLineDetailsHeader = ({ focusLogLine, log, search, onSearch }: Pr
suffix={search !== '' ? clearSearch : undefined}
/>
+ {isAssistantAvailable && (
+
openAssistantByLog?.(log)}
+ tabIndex={0}
+ />
+ )}
{focusLogLine && (
({
+ ...jest.requireActual('@grafana/assistant'),
+ useAssistant: jest.fn(() => [true, jest.fn()]),
+}));
+
+jest.mock('@grafana/runtime', () => ({
+ ...jest.requireActual('@grafana/runtime'),
+ isAssistantAvailable: true,
+}));
+
const theme = createTheme();
const styles = getStyles(theme);
const contextProps = {
diff --git a/public/app/features/logs/components/panel/LogLineMenu.tsx b/public/app/features/logs/components/panel/LogLineMenu.tsx
index 37a0df589d6..49b55b368d1 100644
--- a/public/app/features/logs/components/panel/LogLineMenu.tsx
+++ b/public/app/features/logs/components/panel/LogLineMenu.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useMemo, useRef, MouseEvent } from 'react';
+import { MouseEvent, useCallback, useMemo, useRef } from 'react';
import { LogRowContextOptions, LogRowModel } from '@grafana/data';
import { t } from '@grafana/i18n';
@@ -45,6 +45,8 @@ export const LogLineMenu = ({ log, styles }: Props) => {
logLineMenuCustomItems = [],
logSupportsContext,
toggleDetails,
+ isAssistantAvailable,
+ openAssistantByLog,
} = useLogListContext();
const pinned = useLogIsPinned(log);
const menuRef = useRef(null);
@@ -108,6 +110,13 @@ export const LogLineMenu = ({ log, styles }: Props) => {
{onPermalinkClick && log.rowId !== undefined && log.uid && (
)}
+ {isAssistantAvailable && (
+ openAssistantByLog?.(log)}
+ icon="ai-sparkle"
+ label={t('logs.log-line-menu.open-assistant', 'Explain this log line in Assistant')}
+ />
+ )}
{logLineMenuCustomItems.map((item, i) => {
if (isDivider(item)) {
return ;
@@ -134,6 +143,8 @@ export const LogLineMenu = ({ log, styles }: Props) => {
showContext,
toggleLogDetails,
togglePinning,
+ isAssistantAvailable,
+ openAssistantByLog,
]
);
diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx
index 3d76e63e413..4f740d8059b 100644
--- a/public/app/features/logs/components/panel/LogList.test.tsx
+++ b/public/app/features/logs/components/panel/LogList.test.tsx
@@ -9,6 +9,11 @@ import { createLogRow } from '../mocks/logRow';
import { LogList, Props } from './LogList';
+jest.mock('@grafana/assistant', () => ({
+ ...jest.requireActual('@grafana/assistant'),
+ useAssistant: jest.fn(() => [true, jest.fn()]),
+}));
+
jest.mock('@grafana/runtime', () => {
return {
...jest.requireActual('@grafana/runtime'),
diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx
index 153c53a0a80..f2c7455fddb 100644
--- a/public/app/features/logs/components/panel/LogListContext.tsx
+++ b/public/app/features/logs/components/panel/LogListContext.tsx
@@ -11,6 +11,7 @@ import {
useState,
} from 'react';
+import { createContext as createAssistantContext, ItemDataType, useAssistant } from '@grafana/assistant';
import {
CoreApp,
DataFrame,
@@ -22,10 +23,11 @@ import {
shallowCompare,
store,
} from '@grafana/data';
-import { config, reportInteraction } from '@grafana/runtime';
+import { t } from '@grafana/i18n';
+import { config, getDataSourceSrv, reportInteraction } from '@grafana/runtime';
import { PopoverContent } from '@grafana/ui';
-import { DownloadFormat, checkLogsError, checkLogsSampled, downloadLogs as download } from '../../utils';
+import { checkLogsError, checkLogsSampled, downloadLogs as download, DownloadFormat } from '../../utils';
import { getDisplayedFieldsForLogs } from '../otel/formats';
import { LogLineDetailsMode } from './LogLineDetails';
@@ -63,6 +65,8 @@ export interface LogListContextData extends Omit void;
showDetails: LogListModel[];
toggleDetails: (log: LogListModel) => void;
+ isAssistantAvailable: boolean;
+ openAssistantByLog: ((log: LogListModel) => void) | undefined;
}
export const LogListContext = createContext({
@@ -100,6 +104,8 @@ export const LogListContext = createContext({
syntaxHighlighting: true,
toggleDetails: () => {},
wrapLogMessage: false,
+ isAssistantAvailable: false,
+ openAssistantByLog: () => {},
});
export const useLogListContextData = (key: keyof LogListContextData) => {
@@ -239,6 +245,47 @@ export const LogListContextProvider = ({
const [showDetails, setShowDetails] = useState([]);
const [detailsWidth, setDetailsWidthState] = useState(getDetailsWidth(containerElement, logOptionsStorageKey));
const [detailsMode, setDetailsMode] = useState(detailsModeProp ?? 'sidebar');
+ const [isAssistantAvailable, openAssistant] = useAssistant();
+
+ const openAssistantByLog = useCallback(
+ async (log: LogListModel) => {
+ if (!openAssistant) {
+ return;
+ }
+
+ const datasource = await getDataSourceSrv().get(log.datasourceUid);
+ const context = [];
+ if (datasource) {
+ context.push(
+ createAssistantContext(ItemDataType.Datasource, {
+ datasourceUid: datasource.uid,
+ datasourceName: datasource.name,
+ datasourceType: datasource.type,
+ })
+ );
+ }
+ openAssistant({
+ prompt: `${t('logs.log-line-menu.log-line-explainer', 'Explain this log line in a concise way')}:
+
+ \`\`\`
+${log.entry.replaceAll('`', '\\`')}
+ \`\`\`
+ `,
+ context: [
+ ...context,
+ createAssistantContext(ItemDataType.Structured, {
+ title: t('logs.log-line-menu.log-line', 'Log line'),
+ data: {
+ labels: log.labels,
+ value: log.entry,
+ timestamp: log.timestamp,
+ },
+ }),
+ ],
+ });
+ },
+ [openAssistant]
+ );
useEffect(() => {
if (noInteractions) {
@@ -569,6 +616,8 @@ export const LogListContextProvider = ({
syntaxHighlighting: logListState.syntaxHighlighting,
toggleDetails,
wrapLogMessage: logListState.wrapLogMessage,
+ isAssistantAvailable,
+ openAssistantByLog,
}}
>
{children}
diff --git a/public/app/features/logs/components/panel/LogListControls.test.tsx b/public/app/features/logs/components/panel/LogListControls.test.tsx
index db8e59134b0..2acdf38abe0 100644
--- a/public/app/features/logs/components/panel/LogListControls.test.tsx
+++ b/public/app/features/logs/components/panel/LogListControls.test.tsx
@@ -17,6 +17,13 @@ jest.mock('../../utils', () => ({
downloadLogs: jest.fn(),
}));
+jest.mock('@grafana/assistant', () => {
+ return {
+ ...jest.requireActual('@grafana/assistant'),
+ useAssistant: jest.fn().mockReturnValue([true, jest.fn()]),
+ };
+});
+
const fontSize: LogListFontSize = 'default';
const contextProps = {
app: CoreApp.Unknown,
@@ -31,6 +38,8 @@ const contextProps = {
sortOrder: LogsSortOrder.Ascending,
syntaxHighlighting: false,
wrapLogMessage: false,
+ isAssistantAvailable: false,
+ openAssistantByLog: () => {},
};
describe('LogListControls', () => {
diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx
index 131afaa32cb..53b020bbe88 100644
--- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx
+++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx
@@ -7,6 +7,13 @@ import { LogLineDetailsMode } from '../LogLineDetails';
import { LogListContextData, Props } from '../LogListContext';
import { LogListModel } from '../processing';
+jest.mock('@grafana/assistant', () => {
+ return {
+ ...jest.requireActual('@grafana/assistant'),
+ useAssistant: jest.fn().mockReturnValue([true, jest.fn()]),
+ };
+});
+
export const LogListContext = createContext({
app: CoreApp.Unknown,
closeDetails: () => {},
@@ -43,6 +50,8 @@ export const LogListContext = createContext({
setDetailsMode: function (mode: LogLineDetailsMode): void {
throw new Error('Function not implemented.');
},
+ isAssistantAvailable: false,
+ openAssistantByLog: () => {},
});
export const useLogListContextData = (key: keyof LogListContextData) => {
@@ -97,6 +106,8 @@ export const defaultValue: LogListContextData = {
showTime: false,
sortOrder: LogsSortOrder.Ascending,
wrapLogMessage: false,
+ isAssistantAvailable: false,
+ openAssistantByLog: () => {},
};
export const defaultProps: Props = {
diff --git a/public/app/plugins/panel/logs-new/LogsPanel.test.tsx b/public/app/plugins/panel/logs-new/LogsPanel.test.tsx
index 77570bf19ab..09997a66047 100644
--- a/public/app/plugins/panel/logs-new/LogsPanel.test.tsx
+++ b/public/app/plugins/panel/logs-new/LogsPanel.test.tsx
@@ -18,6 +18,11 @@ import { LogsPanel } from './LogsPanel';
type LogsPanelProps = ComponentProps;
+jest.mock('@grafana/assistant', () => ({
+ ...jest.requireActual('@grafana/assistant'),
+ useAssistant: jest.fn(() => [true, jest.fn()]),
+}));
+
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getAppEvents: jest.fn(),
diff --git a/public/app/plugins/panel/logs/LogsPanel.test.tsx b/public/app/plugins/panel/logs/LogsPanel.test.tsx
index 6a299326f85..64ef4a60a44 100644
--- a/public/app/plugins/panel/logs/LogsPanel.test.tsx
+++ b/public/app/plugins/panel/logs/LogsPanel.test.tsx
@@ -57,6 +57,13 @@ jest.mock('@grafana/data', () => ({
hasLogsContextSupport: (ds: MockDataSourceApi) => hasLogsContextSupport(ds),
}));
+jest.mock('@grafana/assistant', () => {
+ return {
+ ...jest.requireActual('@grafana/assistant'),
+ useAssistant: jest.fn().mockReturnValue([true, jest.fn()]),
+ };
+});
+
const defaultProps = {
data: {
error: undefined,
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 7a1a46a7138..180039f2b38 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -9331,6 +9331,7 @@
"move-displayed-field-down": "Move down",
"move-displayed-field-up": "Move up",
"no-details": "No fields to display.",
+ "open-assistant": "Explain this log line in Assistant",
"pin-line": "Pin log",
"remove-displayed-field": "Remove field",
"remove-log": "Remove log",
@@ -9349,6 +9350,9 @@
"copy-log": "Copy log line",
"hide-details": "Show log details",
"icon-label": "Log menu",
+ "log-line": "Log line",
+ "log-line-explainer": "Explain this log line in a concise way",
+ "open-assistant": "Explain this log line in Assistant",
"pin-to-outline": "Pin log",
"show-context": "Show context",
"show-details": "Hide log details",
diff --git a/yarn.lock b/yarn.lock
index 866a76ae882..7bcd86a7b5a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3064,6 +3064,20 @@ __metadata:
languageName: unknown
linkType: soft
+"@grafana/assistant@npm:0.0.7":
+ version: 0.0.7
+ resolution: "@grafana/assistant@npm:0.0.7"
+ peerDependencies:
+ "@grafana/data": ">=12.0.0"
+ "@grafana/runtime": ">=12.0.0"
+ "@grafana/scenes": ">=5.41.0"
+ "@grafana/ui": ">=12.0.0"
+ react: ">=18.0.0"
+ rxjs: ">=7.0.0"
+ checksum: 10/c83523a032cb748867f683e87ad5b00a04bb4918ae5c7aafe5587b418b73b769a92de948335f0d9055d06036004e7a9794722f627cab3111b076eb27423d4344
+ languageName: node
+ linkType: hard
+
"@grafana/async-query-data@npm:0.4.1":
version: 0.4.1
resolution: "@grafana/async-query-data@npm:0.4.1"
@@ -17832,6 +17846,7 @@ __metadata:
"@formatjs/intl-durationformat": "npm:^0.7.0"
"@glideapps/glide-data-grid": "npm:^6.0.0"
"@grafana/alerting": "workspace:*"
+ "@grafana/assistant": "npm:0.0.7"
"@grafana/aws-sdk": "npm:0.7.1"
"@grafana/azure-sdk": "npm:0.0.7"
"@grafana/data": "workspace:*"