The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/plugins/panel/logs/LogsPanel.tsx

304 lines
9.4 KiB

import { css, cx } from '@emotion/css';
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import {
CoreApp,
DataHoverClearEvent,
DataHoverEvent,
DataQueryResponse,
Field,
GrafanaTheme2,
hasLogsContextSupport,
Labels,
LogRowContextOptions,
LogRowModel,
LogsSortOrder,
PanelProps,
TimeRange,
toUtc,
urlUtil,
} from '@grafana/data';
import { CustomScrollbar, usePanelContext, useStyles2 } from '@grafana/ui';
import { getFieldLinksForExplore } from 'app/features/explore/utils/links';
import { LogRowContextModal } from 'app/features/logs/components/log-context/LogRowContextModal';
import { PanelDataErrorView } from 'app/features/panel/components/PanelDataErrorView';
import { createAndCopyShortLink } from '../../../core/utils/shortLinks';
import { LogLabels } from '../../../features/logs/components/LogLabels';
import { LogRows } from '../../../features/logs/components/LogRows';
import { COMMON_LABELS, dataFrameToLogsModel, dedupLogRows } from '../../../features/logs/logsModel';
import { Options } from './types';
import { useDatasourcesFromTargets } from './useDatasourcesFromTargets';
interface LogsPanelProps extends PanelProps<Options> {}
interface LogsPermalinkUrlState {
logs?: {
id?: string;
};
}
export const LogsPanel = ({
data,
timeZone,
fieldConfig,
Explore: Add switch to restructure logs for better readability (#36324) * Add prettifyLogMessage to select components in test file * Change entry depending on the value of prettifyLogMessage * Add prettifyLogMessage to state * Fix merge conflict * Fixe bug where the log message wasn't parsed as JSON * Implement function to restructure all logs * Change elstic image version back to 7.7.1 * Add showCommonLabels that was missing * Remove comment * Put import of getParser together with the other imports * Logs: fix bug where message isn't restructured if it contains ANSI code * Logs: change label for switch to Restructure * Remove unnecessary file * Logs: added divider before switch component * Add dividers between the different log options * Remove unintentional changes * Explore: remove dividers in log settings * Explore: refactor for LogRowMessage for better readability * remove unnecessary change * Logs: fix bug where logs aren't restructured if they have highlights * Logs: minor refactoring * Logs: use memoizeOne to prevent parsing on every re-render * Logs: calculate needsHilight inside renderLogMessage instead of outside * Logs: fix bug where logs aren't prettified when wrap logs is disabled * Explore: change name to prettify * Remove console.log Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * Dashboards: add switch to prettify log messages to the Logs fields * Logs: make prettify only work for JSON logs * Logs: fix bug with tests for logs * Update public/app/plugins/panel/logs/module.tsx Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
4 years ago
options: {
showLabels,
showTime,
wrapLogMessage,
showCommonLabels,
prettifyLogMessage,
sortOrder,
dedupStrategy,
enableLogDetails,
showLogContextToggle,
Explore: Add switch to restructure logs for better readability (#36324) * Add prettifyLogMessage to select components in test file * Change entry depending on the value of prettifyLogMessage * Add prettifyLogMessage to state * Fix merge conflict * Fixe bug where the log message wasn't parsed as JSON * Implement function to restructure all logs * Change elstic image version back to 7.7.1 * Add showCommonLabels that was missing * Remove comment * Put import of getParser together with the other imports * Logs: fix bug where message isn't restructured if it contains ANSI code * Logs: change label for switch to Restructure * Remove unnecessary file * Logs: added divider before switch component * Add dividers between the different log options * Remove unintentional changes * Explore: remove dividers in log settings * Explore: refactor for LogRowMessage for better readability * remove unnecessary change * Logs: fix bug where logs aren't restructured if they have highlights * Logs: minor refactoring * Logs: use memoizeOne to prevent parsing on every re-render * Logs: calculate needsHilight inside renderLogMessage instead of outside * Logs: fix bug where logs aren't prettified when wrap logs is disabled * Explore: change name to prettify * Remove console.log Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * Dashboards: add switch to prettify log messages to the Logs fields * Logs: make prettify only work for JSON logs * Logs: fix bug with tests for logs * Update public/app/plugins/panel/logs/module.tsx Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
4 years ago
},
id,
}: LogsPanelProps) => {
const isAscending = sortOrder === LogsSortOrder.Ascending;
const style = useStyles2(getStyles);
const [scrollTop, setScrollTop] = useState(0);
const logsContainerRef = useRef<HTMLDivElement>(null);
const [contextRow, setContextRow] = useState<LogRowModel | null>(null);
const [closeCallback, setCloseCallback] = useState<(() => void) | null>(null);
const timeRange = data.timeRange;
const dataSourcesMap = useDatasourcesFromTargets(data.request?.targets);
const { eventBus } = usePanelContext();
const onLogRowHover = useCallback(
(row?: LogRowModel) => {
if (!row) {
eventBus.publish(new DataHoverClearEvent());
} else {
eventBus.publish(
new DataHoverEvent({
point: {
time: row.timeEpochMs,
},
})
);
}
},
[eventBus]
);
const onCloseContext = useCallback(() => {
setContextRow(null);
if (closeCallback) {
closeCallback();
}
}, [closeCallback]);
const onOpenContext = useCallback((row: LogRowModel, onClose: () => void) => {
setContextRow(row);
setCloseCallback(onClose);
}, []);
const onPermalinkClick = useCallback(
async (row: LogRowModel) => {
return await copyDashboardUrl(row, timeRange);
},
[timeRange]
);
const showContextToggle = useCallback(
(row: LogRowModel): boolean => {
if (
!row.dataFrame.refId ||
!dataSourcesMap ||
(!showLogContextToggle &&
data.request?.app !== CoreApp.Dashboard &&
data.request?.app !== CoreApp.PanelEditor &&
data.request?.app !== CoreApp.PanelViewer)
) {
return false;
}
const dataSource = dataSourcesMap.get(row.dataFrame.refId);
return hasLogsContextSupport(dataSource);
},
[dataSourcesMap, showLogContextToggle, data.request?.app]
);
const showPermaLink = useCallback(() => {
return !(
data.request?.app !== CoreApp.Dashboard &&
data.request?.app !== CoreApp.PanelEditor &&
data.request?.app !== CoreApp.PanelViewer
);
}, [data.request?.app]);
const getLogRowContext = useCallback(
async (row: LogRowModel, origRow: LogRowModel, options: LogRowContextOptions): Promise<DataQueryResponse> => {
if (!origRow.dataFrame.refId || !dataSourcesMap) {
return Promise.resolve({ data: [] });
}
const query = data.request?.targets[0];
if (!query) {
return Promise.resolve({ data: [] });
}
const dataSource = dataSourcesMap.get(origRow.dataFrame.refId);
if (!hasLogsContextSupport(dataSource)) {
return Promise.resolve({ data: [] });
}
return dataSource.getLogRowContext(row, options, query);
},
[data.request?.targets, dataSourcesMap]
);
// Important to memoize stuff here, as panel rerenders a lot for example when resizing.
const [logRows, deduplicatedRows, commonLabels] = useMemo(() => {
const logs = data
? dataFrameToLogsModel(data.series, data.request?.intervalMs, undefined, data.request?.targets)
: null;
const logRows = logs?.rows || [];
const commonLabels = logs?.meta?.find((m) => m.label === COMMON_LABELS);
const deduplicatedRows = dedupLogRows(logRows, dedupStrategy);
return [logRows, deduplicatedRows, commonLabels];
}, [data, dedupStrategy]);
useLayoutEffect(() => {
if (isAscending && logsContainerRef.current) {
setScrollTop(logsContainerRef.current.offsetHeight);
} else {
setScrollTop(0);
}
}, [isAscending, logRows]);
const getFieldLinks = useCallback(
(field: Field, rowIndex: number) => {
return getFieldLinksForExplore({ field, rowIndex, range: data.timeRange });
},
[data]
);
/**
* Scrolls the given row into view.
*/
const scrollIntoView = useCallback((row: HTMLElement) => {
row.scrollIntoView(true);
}, []);
if (!data || logRows.length === 0) {
return <PanelDataErrorView fieldConfig={fieldConfig} panelId={id} data={data} needsStringField />;
}
const renderCommonLabels = () => (
<div className={cx(style.labelContainer, isAscending && style.labelContainerAscending)}>
<span className={style.label}>Common labels:</span>
<LogLabels labels={commonLabels ? (commonLabels.value as Labels) : { labels: '(no common labels)' }} />
</div>
);
return (
<>
{contextRow && (
<LogRowContextModal
open={contextRow !== null}
row={contextRow}
onClose={onCloseContext}
getRowContext={(row, options) => getLogRowContext(row, contextRow, options)}
logsSortOrder={sortOrder}
timeZone={timeZone}
/>
)}
<CustomScrollbar autoHide scrollTop={scrollTop}>
<div className={style.container} ref={logsContainerRef}>
{showCommonLabels && !isAscending && renderCommonLabels()}
<LogRows
containerRendered={logsContainerRef.current !== null}
scrollIntoView={scrollIntoView}
permalinkedRowId={getLogsPanelState()?.logs?.id ?? undefined}
onPermalinkClick={showPermaLink() ? onPermalinkClick : undefined}
logRows={logRows}
showContextToggle={showContextToggle}
deduplicatedRows={deduplicatedRows}
dedupStrategy={dedupStrategy}
showLabels={showLabels}
showTime={showTime}
wrapLogMessage={wrapLogMessage}
prettifyLogMessage={prettifyLogMessage}
timeZone={timeZone}
getFieldLinks={getFieldLinks}
logsSortOrder={sortOrder}
enableLogDetails={enableLogDetails}
previewLimit={isAscending ? logRows.length : undefined}
onLogRowHover={onLogRowHover}
app={CoreApp.Dashboard}
onOpenContext={onOpenContext}
/>
{showCommonLabels && isAscending && renderCommonLabels()}
</div>
</CustomScrollbar>
</>
);
};
const getStyles = (theme: GrafanaTheme2) => ({
container: css({
marginBottom: theme.spacing(1.5),
}),
labelContainer: css({
margin: theme.spacing(0, 0, 0.5, 0.5),
display: 'flex',
alignItems: 'center',
}),
labelContainerAscending: css({
margin: theme.spacing(0.5, 0, 0.5, 0),
}),
label: css({
marginRight: theme.spacing(0.5),
fontSize: theme.typography.bodySmall.fontSize,
fontWeight: theme.typography.fontWeightMedium,
}),
});
function getLogsPanelState(): LogsPermalinkUrlState | undefined {
const urlParams = urlUtil.getUrlSearchParams();
const panelStateEncoded = urlParams?.panelState;
if (
panelStateEncoded &&
Array.isArray(panelStateEncoded) &&
panelStateEncoded?.length > 0 &&
typeof panelStateEncoded[0] === 'string'
) {
try {
return JSON.parse(panelStateEncoded[0]);
} catch (e) {
console.error('error parsing logsPanelState', e);
}
}
return undefined;
}
async function copyDashboardUrl(row: LogRowModel, timeRange: TimeRange) {
// this is an extra check, to be sure that we are not
// creating permalinks for logs without an id-field.
// normally it should never happen, because we do not
// display the permalink button in such cases.
if (row.rowId === undefined || !row.dataFrame.refId) {
return;
}
// get panel state, add log-row-id
const panelState = {
logs: { id: row.uid },
};
// Grab the current dashboard URL
const currentURL = new URL(window.location.href);
// Add panel state containing the rowId, and absolute time range from the current query, but leave everything else the same, if the user is in edit mode when grabbing the link, that's what will be linked to, etc.
currentURL.searchParams.set('panelState', JSON.stringify(panelState));
currentURL.searchParams.set('from', toUtc(timeRange.from).valueOf().toString(10));
currentURL.searchParams.set('to', toUtc(timeRange.to).valueOf().toString(10));
await createAndCopyShortLink(currentURL.toString());
return Promise.resolve();
}