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]>
  • Loading branch information
tonyghiani and Marco Antonio Ghiani authored Nov 6, 2023
1 parent 1f2b07c commit 09ff2c4
Show file tree
Hide file tree
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
Expand Up @@ -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';

Expand All @@ -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();
Expand All @@ -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>
);
},
Expand Down Expand Up @@ -111,6 +100,10 @@ export const LogEntryFlyout = ({
onSetFieldFilter,
logViewReference,
}: LogEntryFlyoutProps) => {
const {
services: { observabilityAIAssistant },
} = useKibanaContextForPlugin();

const {
cancelRequest: cancelLogEntryRequest,
errors: logEntryErrors,
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand All @@ -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',
});
Expand Down
4 changes: 4 additions & 0 deletions x-pack/plugins/logs_shared/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
13 changes: 10 additions & 3 deletions x-pack/plugins/logs_shared/public/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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,
};
}

Expand Down
2 changes: 2 additions & 0 deletions x-pack/plugins/logs_shared/public/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -34,6 +35,7 @@ export interface LogsSharedClientSetupDeps {}
export interface LogsSharedClientStartDeps {
data: DataPublicPluginStart;
dataViews: DataViewsPublicPluginStart;
observabilityAIAssistant: ObservabilityAIAssistantPluginStart;
uiActions: UiActionsStart;
}

Expand Down

0 comments on commit 09ff2c4

Please sign in to comment.