Loki: Added support to show label types in Log Details (#97284)
* Log Details: add support to resolve label types if supported * Update public/app/features/logs/utils.ts Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> * LogDetailsRow: create component to display label type * Remove log * Add missing index resolution for label type values * Make type icon less prominent * Update new icon styles * Formatting * Update public/app/features/logs/utils.ts Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com> * Update public/app/features/logs/utils.ts Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com> * LogDetails: update test * Formatting --------- Co-authored-by: Galen Kistler <109082771+gtk-grafana@users.noreply.github.com> Co-authored-by: Sven Grossmann <sven.grossmann@grafana.com>
This commit is contained in:
co-authored by
Galen Kistler
Sven Grossmann
parent
7b934f0d2b
commit
ebfa06e040
@@ -295,4 +295,89 @@ describe('LogDetails', () => {
|
|||||||
expect(screen.getByText('shouldShowLinkName')).toBeInTheDocument();
|
expect(screen.getByText('shouldShowLinkName')).toBeInTheDocument();
|
||||||
expect(screen.getByText('shouldShowLinkValue')).toBeInTheDocument();
|
expect(screen.getByText('shouldShowLinkValue')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Label types', () => {
|
||||||
|
const entry = 'test';
|
||||||
|
const labels = {
|
||||||
|
label1: 'value1',
|
||||||
|
label2: 'value2',
|
||||||
|
label3: 'value3',
|
||||||
|
};
|
||||||
|
const dataFrame = createDataFrame({
|
||||||
|
fields: [
|
||||||
|
{ name: 'timestamp', config: {}, type: FieldType.time, values: [1] },
|
||||||
|
{ name: 'body', type: FieldType.string, values: [entry] },
|
||||||
|
{ name: 'id', type: FieldType.string, values: ['1'] },
|
||||||
|
{
|
||||||
|
name: 'labels',
|
||||||
|
type: FieldType.other,
|
||||||
|
values: [labels],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'labelTypes',
|
||||||
|
type: FieldType.other,
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
label1: 'I',
|
||||||
|
label2: 'S',
|
||||||
|
label3: 'P',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
meta: {
|
||||||
|
type: DataFrameType.LogLines,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
it('should show label types if they are available and supported', () => {
|
||||||
|
setup(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
entry,
|
||||||
|
dataFrame,
|
||||||
|
entryFieldIndex: 0,
|
||||||
|
rowIndex: 0,
|
||||||
|
labels,
|
||||||
|
datasourceType: 'loki',
|
||||||
|
rowId: '1',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show labels and links
|
||||||
|
expect(screen.getByText('label1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('label2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('value2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('label3')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('value3')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('I')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('S')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('P')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it('should not show label types if they are unavailable or not supported', () => {
|
||||||
|
setup(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
entry,
|
||||||
|
dataFrame,
|
||||||
|
entryFieldIndex: 0,
|
||||||
|
rowIndex: 0,
|
||||||
|
labels,
|
||||||
|
datasourceType: 'other datasource',
|
||||||
|
rowId: '1',
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Show labels and links
|
||||||
|
expect(screen.getByText('label1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('value1')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('label2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('value2')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('label3')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('value3')).toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('I')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('S')).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText('P')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,9 +15,18 @@ import {
|
|||||||
LogRowModel,
|
LogRowModel,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { ClipboardButton, DataLinkButton, IconButton, PopoverContent, Themeable2, withTheme2 } from '@grafana/ui';
|
import {
|
||||||
|
ClipboardButton,
|
||||||
|
DataLinkButton,
|
||||||
|
IconButton,
|
||||||
|
PopoverContent,
|
||||||
|
Themeable2,
|
||||||
|
Tooltip,
|
||||||
|
withTheme2,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import { logRowToSingleRowDataFrame } from '../logsModel';
|
import { logRowToSingleRowDataFrame } from '../logsModel';
|
||||||
|
import { getLabelTypeFromRow } from '../utils';
|
||||||
|
|
||||||
import { LogLabelStats } from './LogLabelStats';
|
import { LogLabelStats } from './LogLabelStats';
|
||||||
import { getLogRowStyles } from './getLogRowStyles';
|
import { getLogRowStyles } from './getLogRowStyles';
|
||||||
@@ -50,6 +59,19 @@ interface State {
|
|||||||
|
|
||||||
const getStyles = memoizeOne((theme: GrafanaTheme2) => {
|
const getStyles = memoizeOne((theme: GrafanaTheme2) => {
|
||||||
return {
|
return {
|
||||||
|
labelType: css({
|
||||||
|
border: `solid 1px ${theme.colors.text.secondary}`,
|
||||||
|
color: theme.colors.text.secondary,
|
||||||
|
borderRadius: theme.shape.radius.circle,
|
||||||
|
fontSize: theme.spacing(1),
|
||||||
|
lineHeight: theme.spacing(1.25),
|
||||||
|
height: theme.spacing(1.5),
|
||||||
|
width: theme.spacing(1.5),
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
verticalAlign: 'middle',
|
||||||
|
marginLeft: theme.spacing(1),
|
||||||
|
}),
|
||||||
wordBreakAll: css({
|
wordBreakAll: css({
|
||||||
label: 'wordBreakAll',
|
label: 'wordBreakAll',
|
||||||
wordBreak: 'break-all',
|
wordBreak: 'break-all',
|
||||||
@@ -275,6 +297,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
const singleVal = parsedValues == null ? false : parsedValues.length === 1;
|
const singleVal = parsedValues == null ? false : parsedValues.length === 1;
|
||||||
const hasFilteringFunctionality = !disableActions && onClickFilterLabel && onClickFilterOutLabel;
|
const hasFilteringFunctionality = !disableActions && onClickFilterLabel && onClickFilterOutLabel;
|
||||||
const refIdTooltip = app === CoreApp.Explore && row.dataFrame?.refId ? ` in query ${row.dataFrame?.refId}` : '';
|
const refIdTooltip = app === CoreApp.Explore && row.dataFrame?.refId ? ` in query ${row.dataFrame?.refId}` : '';
|
||||||
|
const labelType = singleKey ? getLabelTypeFromRow(parsedKeys[0], row) : null;
|
||||||
|
|
||||||
const isMultiParsedValueWithNoContent =
|
const isMultiParsedValueWithNoContent =
|
||||||
!singleVal && parsedValues != null && !parsedValues.every((val) => val === '');
|
!singleVal && parsedValues != null && !parsedValues.every((val) => val === '');
|
||||||
@@ -321,6 +344,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
|
<td>{labelType && <LabelTypeBadge type={labelType} styles={styles} />}</td>
|
||||||
{/* Key - value columns */}
|
{/* Key - value columns */}
|
||||||
<td className={rowStyles.logDetailsLabel}>{singleKey ? parsedKeys[0] : this.generateMultiVal(parsedKeys)}</td>
|
<td className={rowStyles.logDetailsLabel}>{singleKey ? parsedKeys[0] : this.generateMultiVal(parsedKeys)}</td>
|
||||||
<td className={cx(styles.wordBreakAll, wrapLogMessage && styles.wrapLine)}>
|
<td className={cx(styles.wordBreakAll, wrapLogMessage && styles.wrapLine)}>
|
||||||
@@ -360,7 +384,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
</tr>
|
</tr>
|
||||||
{showFieldsStats && singleKey && singleVal && (
|
{showFieldsStats && singleKey && singleVal && (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td colSpan={2}>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant={showFieldsStats ? 'primary' : 'secondary'}
|
variant={showFieldsStats ? 'primary' : 'secondary'}
|
||||||
name="signal"
|
name="signal"
|
||||||
@@ -386,6 +410,16 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function LabelTypeBadge({ type, styles }: { type: string; styles: ReturnType<typeof getStyles> }) {
|
||||||
|
return (
|
||||||
|
<Tooltip content={type}>
|
||||||
|
<div className={styles.labelType}>
|
||||||
|
<span>{type.substring(0, 1)}</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface AsyncIconButtonProps extends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
|
interface AsyncIconButtonProps extends Pick<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'> {
|
||||||
name: IconName;
|
name: IconName;
|
||||||
isActive(): Promise<boolean>;
|
isActive(): Promise<boolean>;
|
||||||
|
|||||||
@@ -343,3 +343,48 @@ export function createLogRowsMap() {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLabelTypeFromFrame(labelKey: string, frame: DataFrame, index: number): null | string {
|
||||||
|
const typeField = frame.fields.find((field) => field.name === 'labelTypes')?.values[index];
|
||||||
|
if (!typeField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return typeField[labelKey] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLabelTypeFromRow(label: string, row: LogRowModel) {
|
||||||
|
if (!row.datasourceType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const idField = row.dataFrame.fields.find((field) => field.name === 'id');
|
||||||
|
if (!idField) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const rowIndex = idField.values.findIndex((id) => id === row.rowId);
|
||||||
|
if (rowIndex < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const labelType = getLabelTypeFromFrame(label, row.dataFrame, rowIndex);
|
||||||
|
if (!labelType) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getDataSourceLabelType(labelType, row.datasourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDataSourceLabelType(labelType: string, datasourceType: string) {
|
||||||
|
switch (datasourceType) {
|
||||||
|
case 'loki':
|
||||||
|
switch (labelType) {
|
||||||
|
case 'I':
|
||||||
|
return 'Indexed label';
|
||||||
|
case 'S':
|
||||||
|
return 'Structured metadata';
|
||||||
|
case 'P':
|
||||||
|
return 'Parsed label';
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user