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:
Matias Chomicki
2024-12-12 18:49:59 +02:00
committed by GitHub
co-authored by Galen Kistler Sven Grossmann
parent 7b934f0d2b
commit ebfa06e040
3 changed files with 166 additions and 2 deletions
@@ -295,4 +295,89 @@ describe('LogDetails', () => {
expect(screen.getByText('shouldShowLinkName')).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,
} from '@grafana/data';
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 { getLabelTypeFromRow } from '../utils';
import { LogLabelStats } from './LogLabelStats';
import { getLogRowStyles } from './getLogRowStyles';
@@ -50,6 +59,19 @@ interface State {
const getStyles = memoizeOne((theme: GrafanaTheme2) => {
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({
label: 'wordBreakAll',
wordBreak: 'break-all',
@@ -275,6 +297,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
const singleVal = parsedValues == null ? false : parsedValues.length === 1;
const hasFilteringFunctionality = !disableActions && onClickFilterLabel && onClickFilterOutLabel;
const refIdTooltip = app === CoreApp.Explore && row.dataFrame?.refId ? ` in query ${row.dataFrame?.refId}` : '';
const labelType = singleKey ? getLabelTypeFromRow(parsedKeys[0], row) : null;
const isMultiParsedValueWithNoContent =
!singleVal && parsedValues != null && !parsedValues.every((val) => val === '');
@@ -321,6 +344,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
</div>
</td>
<td>{labelType && <LabelTypeBadge type={labelType} styles={styles} />}</td>
{/* Key - value columns */}
<td className={rowStyles.logDetailsLabel}>{singleKey ? parsedKeys[0] : this.generateMultiVal(parsedKeys)}</td>
<td className={cx(styles.wordBreakAll, wrapLogMessage && styles.wrapLine)}>
@@ -360,7 +384,7 @@ class UnThemedLogDetailsRow extends PureComponent<Props, State> {
</tr>
{showFieldsStats && singleKey && singleVal && (
<tr>
<td>
<td colSpan={2}>
<IconButton
variant={showFieldsStats ? 'primary' : 'secondary'}
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'> {
name: IconName;
isActive(): Promise<boolean>;
+45
View File
@@ -343,3 +343,48 @@ export function createLogRowsMap() {
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;
}
}