Skip to content

Commit fdce4e1

Browse files
author
Alejandro Fernández Gómez
authored
[7.x] [Logs UI] Shared <LogStream /> component (#76262) (#76879)
1 parent 14ce8a3 commit fdce4e1

File tree

8 files changed

+351
-94
lines changed

8 files changed

+351
-94
lines changed
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Embeddable `<LogStream />` component
2+
3+
The purpose of this component is to allow you, the developer, to have your very own Log Stream in your plugin.
4+
5+
The plugin is exposed through `infra/public`. Since Kibana uses relative paths is up to you to find how to import it (sorry).
6+
7+
```tsx
8+
import { LogStream } from '../../../../../../infra/public';
9+
```
10+
11+
## Prerequisites
12+
13+
To use the component, there are several things you need to ensure in your plugin:
14+
15+
- In your plugin's `kibana.json` plugin, add `"infra"` to `requiredPlugins`.
16+
- The component needs to be mounted inside the hiearchy of a [`kibana-react` provider](https://github.com/elastic/kibana/blob/b2d0aa7b7fae1c89c8f9e8854ae73e71be64e765/src/plugins/kibana_react/README.md#L45).
17+
18+
## Usage
19+
20+
The simplest way to use the component is with a date range, passed with the `startTimestamp` and `endTimestamp` props.
21+
22+
```tsx
23+
const endTimestamp = Date.now();
24+
const startTimestamp = endTimestamp - 15 * 60 * 1000; // 15 minutes
25+
26+
<LogStream startTimestamp={startTimestamp} endTimestamp={endTimestamp} />;
27+
```
28+
29+
This will show a list of log entries between the time range, in ascending order (oldest first), but with the scroll position all the way to the bottom (showing the newest entries)
30+
31+
### Filtering data
32+
33+
You might want to show specific data for the purpose of your plugin. Maybe you want to show log lines from a specific host, or for an APM trace. You can pass a KQL expression via the `query` prop.
34+
35+
```tsx
36+
<LogStream
37+
startTimestamp={startTimestamp}
38+
endTimestamp={endTimestamp}
39+
query="trace.id: 18fabada9384abd4"
40+
/>
41+
```
42+
43+
### Modifying rendering
44+
45+
By default the component will initially load at the bottom of the list, showing the newest entries. You can change what log line is shown in the center via the `center` prop. The prop takes a [`LogEntriesCursor`](https://github.com/elastic/kibana/blob/0a6c748cc837c016901f69ff05d81395aa2d41c8/x-pack/plugins/infra/common/http_api/log_entries/common.ts#L9-L13).
46+
47+
```tsx
48+
<LogStream
49+
startTimestamp={startTimestamp}
50+
endTimestamp={endTimestamp}
51+
center={{ time: ..., tiebreaker: ... }}
52+
/>
53+
```
54+
55+
If you want to highlight a specific log line, you can do so by passing its ID in the `highlight` prop.
56+
57+
```tsx
58+
<LogStream startTimestamp={startTimestamp} endTimestamp={endTimestamp} highlight="abcde12345" />
59+
```
60+
61+
### Source configuration
62+
63+
The infra plugin has the concept of "source configuration" to store settings for the logs UI. The component will use the source configuration to determine which indices to query or what columns to show.
64+
65+
By default the `<LogStream />` uses the `"default"` source confiuration, but if your plugin uses a different one you can specify it via the `sourceId` prop.
66+
67+
```tsx
68+
<LogStream startTimestamp={startTimestamp} endTimestamp={endTimestamp} sourceId="my_source" />
69+
```
70+
71+
### Considerations
72+
73+
As mentioned in the prerequisites, the component relies on `kibana-react` to access kibana's core services. If this is not the case the component will throw an exception when rendering. We advise to use an `<EuiErrorBoundary>` in your component hierarchy to catch this error if necessary.
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { useMemo } from 'react';
8+
import { noop } from 'lodash';
9+
import { useMount } from 'react-use';
10+
import { euiStyled } from '../../../../observability/public';
11+
12+
import { LogEntriesCursor } from '../../../common/http_api';
13+
14+
import { useKibana } from '../../../../../../src/plugins/kibana_react/public';
15+
import { useLogSource } from '../../containers/logs/log_source';
16+
import { useLogStream } from '../../containers/logs/log_stream';
17+
18+
import { ScrollableLogTextStreamView } from '../logging/log_text_stream';
19+
20+
export interface LogStreamProps {
21+
sourceId?: string;
22+
startTimestamp: number;
23+
endTimestamp: number;
24+
query?: string;
25+
center?: LogEntriesCursor;
26+
highlight?: string;
27+
height?: string | number;
28+
}
29+
30+
export const LogStream: React.FC<LogStreamProps> = ({
31+
sourceId = 'default',
32+
startTimestamp,
33+
endTimestamp,
34+
query,
35+
center,
36+
highlight,
37+
height = '400px',
38+
}) => {
39+
// source boilerplate
40+
const { services } = useKibana();
41+
if (!services?.http?.fetch) {
42+
throw new Error(
43+
`<LogStream /> cannot access kibana core services.
44+
45+
Ensure the component is mounted within kibana-react's <KibanaContextProvider> hierarchy.
46+
Read more at https://github.com/elastic/kibana/blob/master/src/plugins/kibana_react/README.md"
47+
`
48+
);
49+
}
50+
51+
const {
52+
sourceConfiguration,
53+
loadSourceConfiguration,
54+
isLoadingSourceConfiguration,
55+
} = useLogSource({
56+
sourceId,
57+
fetch: services.http.fetch,
58+
});
59+
60+
// Internal state
61+
const { loadingState, entries, fetchEntries } = useLogStream({
62+
sourceId,
63+
startTimestamp,
64+
endTimestamp,
65+
query,
66+
center,
67+
});
68+
69+
// Derived state
70+
const isReloading =
71+
isLoadingSourceConfiguration || loadingState === 'uninitialized' || loadingState === 'loading';
72+
73+
const columnConfigurations = useMemo(() => {
74+
return sourceConfiguration ? sourceConfiguration.configuration.logColumns : [];
75+
}, [sourceConfiguration]);
76+
77+
const streamItems = useMemo(
78+
() =>
79+
entries.map((entry) => ({
80+
kind: 'logEntry' as const,
81+
logEntry: entry,
82+
highlights: [],
83+
})),
84+
[entries]
85+
);
86+
87+
// Component lifetime
88+
useMount(() => {
89+
loadSourceConfiguration();
90+
fetchEntries();
91+
});
92+
93+
const parsedHeight = typeof height === 'number' ? `${height}px` : height;
94+
95+
return (
96+
<LogStreamContent height={parsedHeight}>
97+
<ScrollableLogTextStreamView
98+
target={center ? center : entries.length ? entries[entries.length - 1].cursor : null}
99+
columnConfigurations={columnConfigurations}
100+
items={streamItems}
101+
scale="medium"
102+
wrap={false}
103+
isReloading={isReloading}
104+
isLoadingMore={false}
105+
hasMoreBeforeStart={false}
106+
hasMoreAfterEnd={false}
107+
isStreaming={false}
108+
lastLoadedTime={null}
109+
jumpToTarget={noop}
110+
reportVisibleInterval={noop}
111+
loadNewerItems={noop}
112+
reloadItems={fetchEntries}
113+
highlightedItem={highlight ?? null}
114+
currentHighlightKey={null}
115+
startDateExpression={''}
116+
endDateExpression={''}
117+
updateDateRange={noop}
118+
startLiveStreaming={noop}
119+
hideScrollbar={false}
120+
/>
121+
</LogStreamContent>
122+
);
123+
};
124+
125+
const LogStreamContent = euiStyled.div<{ height: string }>`
126+
display: flex;
127+
background-color: ${(props) => props.theme.eui.euiColorEmptyShade};
128+
height: ${(props) => props.height};
129+
`;
130+
131+
// Allow for lazy loading
132+
// eslint-disable-next-line import/no-default-export
133+
export default LogStream;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React from 'react';
8+
import type { LogStreamProps } from './';
9+
10+
const LazyLogStream = React.lazy(() => import('./'));
11+
12+
export const LazyLogStreamWrapper: React.FC<LogStreamProps> = (props) => (
13+
<React.Suspense fallback={<div />}>
14+
<LazyLogStream {...props} />
15+
</React.Suspense>
16+
);

x-pack/plugins/infra/public/components/logging/log_text_stream/scrollable_log_text_stream_view.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ interface ScrollableLogTextStreamViewProps {
6060
endDateExpression: string;
6161
updateDateRange: (range: { startDateExpression?: string; endDateExpression?: string }) => void;
6262
startLiveStreaming: () => void;
63+
hideScrollbar?: boolean;
6364
}
6465

6566
interface ScrollableLogTextStreamViewState {
@@ -146,6 +147,7 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
146147
setFlyoutVisibility,
147148
setContextEntry,
148149
} = this.props;
150+
const hideScrollbar = this.props.hideScrollbar ?? true;
149151

150152
const { targetId, items, isScrollLocked } = this.state;
151153
const hasItems = items.length > 0;
@@ -196,7 +198,7 @@ export class ScrollableLogTextStreamView extends React.PureComponent<
196198
width={width}
197199
onVisibleChildrenChange={this.handleVisibleChildrenChange}
198200
target={targetId}
199-
hideScrollbar={true}
201+
hideScrollbar={hideScrollbar}
200202
data-test-subj={'logStream'}
201203
isLocked={isScrollLocked}
202204
entriesCount={items.length}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import { useState, useMemo } from 'react';
8+
import { esKuery } from '../../../../../../../src/plugins/data/public';
9+
import { fetchLogEntries } from '../log_entries/api/fetch_log_entries';
10+
import { useTrackedPromise } from '../../../utils/use_tracked_promise';
11+
import { LogEntry, LogEntriesCursor } from '../../../../common/http_api';
12+
13+
interface LogStreamProps {
14+
sourceId: string;
15+
startTimestamp: number;
16+
endTimestamp: number;
17+
query?: string;
18+
center?: LogEntriesCursor;
19+
}
20+
21+
interface LogStreamState {
22+
entries: LogEntry[];
23+
fetchEntries: () => void;
24+
loadingState: 'uninitialized' | 'loading' | 'success' | 'error';
25+
}
26+
27+
export function useLogStream({
28+
sourceId,
29+
startTimestamp,
30+
endTimestamp,
31+
query,
32+
center,
33+
}: LogStreamProps): LogStreamState {
34+
const [entries, setEntries] = useState<LogStreamState['entries']>([]);
35+
36+
const parsedQuery = useMemo(() => {
37+
return query
38+
? JSON.stringify(esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(query)))
39+
: null;
40+
}, [query]);
41+
42+
// Callbacks
43+
const [entriesPromise, fetchEntries] = useTrackedPromise(
44+
{
45+
cancelPreviousOn: 'creation',
46+
createPromise: () => {
47+
setEntries([]);
48+
const fetchPosition = center ? { center } : { before: 'last' };
49+
50+
return fetchLogEntries({
51+
sourceId,
52+
startTimestamp,
53+
endTimestamp,
54+
query: parsedQuery,
55+
...fetchPosition,
56+
});
57+
},
58+
onResolve: ({ data }) => {
59+
setEntries(data.entries);
60+
},
61+
},
62+
[sourceId, startTimestamp, endTimestamp, query]
63+
);
64+
65+
const loadingState = useMemo(() => convertPromiseStateToLoadingState(entriesPromise.state), [
66+
entriesPromise.state,
67+
]);
68+
69+
return {
70+
entries,
71+
fetchEntries,
72+
loadingState,
73+
};
74+
}
75+
76+
function convertPromiseStateToLoadingState(
77+
state: 'uninitialized' | 'pending' | 'resolved' | 'rejected'
78+
): LogStreamState['loadingState'] {
79+
switch (state) {
80+
case 'uninitialized':
81+
return 'uninitialized';
82+
case 'pending':
83+
return 'loading';
84+
case 'resolved':
85+
return 'success';
86+
case 'rejected':
87+
return 'error';
88+
}
89+
}

0 commit comments

Comments
 (0)