Skip to content

Commit

Permalink
[Logs Shared] Extract AI Assistant into reusable component (elastic#1…
Browse files Browse the repository at this point in the history
…70496)

## 📓 Summary

Part of elastic#169506 

The reason behind exposing this component is that we'll use the same
configuration and prompts to generate insights about log entries on
different touchpoints:
- Currently implemented, show AI insights on the LogStream flyout detail
- To implement (follow up PR), show AI insights on the Log Explorer
flyout detail

These changes expose a new LogAIAssistant component in 2 ways:
- Consume the component from the `logs-shared` plugin start contract.
- Import the component from the plugin bundle.

In both ways the component come lazy-loaded, the main difference is that
consuming it from the start contract will pre-inject the aiAssistant
dependency in the component.

```ts
// Usage from plugin contract
const {services} = useKibana()

const { LogAIAssistant } = services.logsShared
<LogAIAssistant doc={logEntry} />

// Usage from component import
import { LogAIAssistant } from '@kbn/logs-shared-plugin/public';

const {services} = useKibana()

<LogAIAssistant aiAssistant={services.observabilityAIAssistant} doc={logEntry} />
```

To avoid mixing the registration of external components into the Log
Explorer, I decided to split this work into different PRs to keep the
changes scoped.

---------

Co-authored-by: Marco Antonio Ghiani <[email protected]>
tonyghiani and Marco Antonio Ghiani authored Nov 6, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 1f2b07c commit 09ff2c4
Showing 7 changed files with 166 additions and 94 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React from 'react';
import { Optional } from '@kbn/utility-types';
import { dynamic } from '../../../common/dynamic';
import type { LogAIAssistantProps } from './log_ai_assistant';

export const LogAIAssistant = dynamic(() => import('./log_ai_assistant'));

interface LogAIAssistantFactoryDeps {
observabilityAIAssistant: LogAIAssistantProps['aiAssistant'];
}

export function createLogAIAssistant({ observabilityAIAssistant }: LogAIAssistantFactoryDeps) {
return ({
aiAssistant = observabilityAIAssistant,
...props
}: Optional<LogAIAssistantProps, 'aiAssistant'>) => (
<LogAIAssistant aiAssistant={aiAssistant} {...props} />
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import React, { useMemo } from 'react';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import {
ContextualInsight,
type Message,
ObservabilityAIAssistantPluginStart,
MessageRole,
} from '@kbn/observability-ai-assistant-plugin/public';
import { LogEntryField } from '../../../common';
import { explainLogMessageTitle, similarLogMessagesTitle } from './translations';

export interface LogAIAssistantDocument {
fields: LogEntryField[];
}

export interface LogAIAssistantProps {
aiAssistant: ObservabilityAIAssistantPluginStart;
doc: LogAIAssistantDocument | undefined;
}

export function LogAIAssistant({ aiAssistant, doc }: LogAIAssistantProps) {
const explainLogMessageMessages = useMemo<Message[] | undefined>(() => {
if (!doc) {
return undefined;
}

const now = new Date().toISOString();

return [
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I'm looking at a log entry. Can you explain me what the log message means? Where it could be coming from, whether it is expected and whether it is an issue. Here's the context, serialized: ${JSON.stringify(
{ logEntry: { fields: doc.fields } }
)} `,
},
},
];
}, [doc]);

const similarLogMessageMessages = useMemo<Message[] | undefined>(() => {
if (!doc) {
return undefined;
}

const now = new Date().toISOString();

const message = doc.fields.find((field) => field.field === 'message')?.value[0];

return [
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I'm looking at a log entry. Can you construct a Kibana KQL query that I can enter in the search bar that gives me similar log entries, based on the \`message\` field: ${message}`,
},
},
];
}, [doc]);

return (
<EuiFlexGroup direction="column" gutterSize="m">
{aiAssistant.isEnabled() && explainLogMessageMessages ? (
<EuiFlexItem grow={false}>
<ContextualInsight title={explainLogMessageTitle} messages={explainLogMessageMessages} />
</EuiFlexItem>
) : null}
{aiAssistant.isEnabled() && similarLogMessageMessages ? (
<EuiFlexItem grow={false}>
<ContextualInsight title={similarLogMessagesTitle} messages={similarLogMessageMessages} />
</EuiFlexItem>
) : null}
</EuiFlexGroup>
);
}

// eslint-disable-next-line import/no-default-export
export default LogAIAssistant;
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';

export const explainLogMessageTitle = i18n.translate(
'xpack.logsShared.logFlyout.explainLogMessageTitle',
{
defaultMessage: "What's this message?",
}
);

export const similarLogMessagesTitle = i18n.translate(
'xpack.logsShared.logFlyout.similarLogMessagesTitle',
{
defaultMessage: 'How do I find similar log messages?',
}
);
Original file line number Diff line number Diff line change
@@ -16,26 +16,19 @@ import {
EuiTitle,
} from '@elastic/eui';
import { OverlayRef } from '@kbn/core/public';
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { Query } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n-react';
import { createKibanaReactContext, useKibana } from '@kbn/kibana-react-plugin/public';
import {
ContextualInsight,
MessageRole,
ObservabilityAIAssistantPluginStart,
ObservabilityAIAssistantProvider,
useObservabilityAIAssistant,
type Message,
} from '@kbn/observability-ai-assistant-plugin/public';
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public';
import React, { useCallback, useEffect, useRef } from 'react';
import { useKibanaContextForPlugin } from '../../../hooks/use_kibana';
import { LogViewReference } from '../../../../common/log_views';
import { TimeKey } from '../../../../common/time';
import { useLogEntry } from '../../../containers/logs/log_entry';
import { CenteredEuiFlyoutBody } from '../../centered_flyout_body';
import { DataSearchErrorCallout } from '../../data_search_error_callout';
import { DataSearchProgress } from '../../data_search_progress';
import LogAIAssistant from '../../log_ai_assistant/log_ai_assistant';
import { LogEntryActionsMenu } from './log_entry_actions_menu';
import { LogEntryFieldsTable } from './log_entry_fields_table';

@@ -51,10 +44,7 @@ export const useLogEntryFlyout = (logViewReference: LogViewReference) => {
const {
services: { http, data, uiSettings, application, observabilityAIAssistant },
overlays: { openFlyout },
} = useKibana<{
data: DataPublicPluginStart;
observabilityAIAssistant?: ObservabilityAIAssistantPluginStart;
}>();
} = useKibanaContextForPlugin();

const closeLogEntryFlyout = useCallback(() => {
flyoutRef.current?.close();
@@ -67,17 +57,16 @@ export const useLogEntryFlyout = (logViewReference: LogViewReference) => {
data,
uiSettings,
application,
observabilityAIAssistant,
});

flyoutRef.current = openFlyout(
<KibanaReactContextProvider>
<ObservabilityAIAssistantProvider value={observabilityAIAssistant}>
<LogEntryFlyout
logEntryId={logEntryId}
onCloseFlyout={closeLogEntryFlyout}
logViewReference={logViewReference}
/>
</ObservabilityAIAssistantProvider>
<LogEntryFlyout
logEntryId={logEntryId}
onCloseFlyout={closeLogEntryFlyout}
logViewReference={logViewReference}
/>
</KibanaReactContextProvider>
);
},
@@ -111,6 +100,10 @@ export const LogEntryFlyout = ({
onSetFieldFilter,
logViewReference,
}: LogEntryFlyoutProps) => {
const {
services: { observabilityAIAssistant },
} = useKibanaContextForPlugin();

const {
cancelRequest: cancelLogEntryRequest,
errors: logEntryErrors,
@@ -130,48 +123,6 @@ export const LogEntryFlyout = ({
}
}, [fetchLogEntry, logViewReference, logEntryId]);

const explainLogMessageMessages = useMemo<Message[] | undefined>(() => {
if (!logEntry) {
return undefined;
}

const now = new Date().toISOString();

return [
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I'm looking at a log entry. Can you explain me what the log message means? Where it could be coming from, whether it is expected and whether it is an issue. Here's the context, serialized: ${JSON.stringify(
{ logEntry: { fields: logEntry.fields } }
)} `,
},
},
];
}, [logEntry]);

const similarLogMessageMessages = useMemo<Message[] | undefined>(() => {
if (!logEntry) {
return undefined;
}

const now = new Date().toISOString();

const message = logEntry.fields.find((field) => field.field === 'message')?.value[0];

return [
{
'@timestamp': now,
message: {
role: MessageRole.User,
content: `I'm looking at a log entry. Can you construct a Kibana KQL query that I can enter in the search bar that gives me similar log entries, based on the \`message\` field: ${message}`,
},
},
];
}, [logEntry]);

const aiAssistant = useObservabilityAIAssistant();

return (
<EuiFlyout onClose={onCloseFlyout} size="m">
<EuiFlyoutHeader hasBorder>
@@ -232,22 +183,9 @@ export const LogEntryFlyout = ({
}
>
<EuiFlexGroup direction="column" gutterSize="m">
{aiAssistant.isEnabled() && explainLogMessageMessages ? (
<EuiFlexItem grow={false}>
<ContextualInsight
title={explainLogMessageTitle}
messages={explainLogMessageMessages}
/>
</EuiFlexItem>
) : null}
{aiAssistant.isEnabled() && similarLogMessageMessages ? (
<EuiFlexItem grow={false}>
<ContextualInsight
title={similarLogMessagesTitle}
messages={similarLogMessageMessages}
/>
</EuiFlexItem>
) : null}
<EuiFlexItem grow={false}>
<LogAIAssistant aiAssistant={observabilityAIAssistant} doc={logEntry} />
</EuiFlexItem>
<EuiFlexItem grow={false}>
<LogEntryFieldsTable logEntry={logEntry} onSetFieldFilter={onSetFieldFilter} />
</EuiFlexItem>
@@ -268,17 +206,6 @@ export const LogEntryFlyout = ({
);
};

const explainLogMessageTitle = i18n.translate('xpack.logsShared.logFlyout.explainLogMessageTitle', {
defaultMessage: "What's this message?",
});

const similarLogMessagesTitle = i18n.translate(
'xpack.logsShared.logFlyout.similarLogMessagesTitle',
{
defaultMessage: 'How do I find similar log messages?',
}
);

const loadingProgressMessage = i18n.translate('xpack.logsShared.logFlyout.loadingMessage', {
defaultMessage: 'Searching log entry in shards',
});
4 changes: 4 additions & 0 deletions x-pack/plugins/logs_shared/public/index.ts
Original file line number Diff line number Diff line change
@@ -46,8 +46,12 @@ export {
useColumnWidths,
} from './components/logging/log_text_stream/log_entry_column';
export { LogEntryFlyout } from './components/logging/log_entry_flyout';
export type { LogAIAssistantProps } from './components/log_ai_assistant/log_ai_assistant';
export type { LogStreamProps } from './components/log_stream/log_stream';

export const LogAIAssistant = dynamic(
() => import('./components/log_ai_assistant/log_ai_assistant')
);
export const LogStream = dynamic(() => import('./components/log_stream/log_stream'));
export const LogColumnHeader = dynamic(
() => import('./components/logging/log_text_stream/column_headers')
13 changes: 10 additions & 3 deletions x-pack/plugins/logs_shared/public/plugin.ts
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@
*/

import { CoreStart } from '@kbn/core/public';
import { createLogAIAssistant } from './components/log_ai_assistant';
import { LogViewsService } from './services/log_views';
import { LogsSharedClientPluginClass, LogsSharedClientStartDeps } from './types';

@@ -23,14 +24,20 @@ export class LogsSharedPlugin implements LogsSharedClientPluginClass {
}

public start(core: CoreStart, plugins: LogsSharedClientStartDeps) {
const { http } = core;
const { data, dataViews, observabilityAIAssistant } = plugins;

const logViews = this.logViews.start({
http: core.http,
dataViews: plugins.dataViews,
search: plugins.data.search,
http,
dataViews,
search: data.search,
});

const LogAIAssistant = createLogAIAssistant({ observabilityAIAssistant });

return {
logViews,
LogAIAssistant,
};
}

2 changes: 2 additions & 0 deletions x-pack/plugins/logs_shared/public/types.ts
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
import type { CoreSetup, CoreStart, Plugin as PluginClass } from '@kbn/core/public';
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
import { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public';
import { UiActionsStart } from '@kbn/ui-actions-plugin/public';
// import type { OsqueryPluginStart } from '../../osquery/public';
import { LogViewsServiceSetup, LogViewsServiceStart } from './services/log_views';
@@ -34,6 +35,7 @@ export interface LogsSharedClientSetupDeps {}
export interface LogsSharedClientStartDeps {
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
uiActions: UiActionsStart;
}

0 comments on commit 09ff2c4

Please sign in to comment.