mirror of https://github.com/grafana/grafana.git
				
				
				
			Loki: Visually distinguish error logs for LogQL2 (#28359)
* Loki: Add errored logs and update UI * Update messaging * Add icon and tooltip for errored logs * Update name of variable for more semantic meaning * Add tests * Update test * Refactor, remove unnecessary state * Update packages/grafana-data/src/types/logs.ts * Update packages/grafana-ui/src/components/Logs/LogDetails.tsx Co-authored-by: Giordano Ricci <gio.ricci@grafana.com> Co-authored-by: Giordano Ricci <gio.ricci@grafana.com>
This commit is contained in:
		
							parent
							
								
									62f5641aa9
								
							
						
					
					
						commit
						3f39b4b601
					
				| 
						 | 
					@ -27,10 +27,12 @@ export enum LogLevel {
 | 
				
			||||||
  unknown = 'unknown',
 | 
					  unknown = 'unknown',
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Used for meta information such as common labels or returned log rows in logs view in Explore
 | 
				
			||||||
export enum LogsMetaKind {
 | 
					export enum LogsMetaKind {
 | 
				
			||||||
  Number,
 | 
					  Number,
 | 
				
			||||||
  String,
 | 
					  String,
 | 
				
			||||||
  LabelsMap,
 | 
					  LabelsMap,
 | 
				
			||||||
 | 
					  Error,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum LogsSortOrder {
 | 
					export enum LogsSortOrder {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ import {
 | 
				
			||||||
  calculateStats,
 | 
					  calculateStats,
 | 
				
			||||||
  getLogLevelFromKey,
 | 
					  getLogLevelFromKey,
 | 
				
			||||||
  sortLogsResult,
 | 
					  sortLogsResult,
 | 
				
			||||||
 | 
					  checkLogsError,
 | 
				
			||||||
} from './logs';
 | 
					} from './logs';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('getLoglevel()', () => {
 | 
					describe('getLoglevel()', () => {
 | 
				
			||||||
| 
						 | 
					@ -352,3 +353,15 @@ describe('sortLogsResult', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					describe('checkLogsError()', () => {
 | 
				
			||||||
 | 
					  const log = ({
 | 
				
			||||||
 | 
					    labels: {
 | 
				
			||||||
 | 
					      __error__: 'Error Message',
 | 
				
			||||||
 | 
					      foo: 'boo',
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					  } as any) as LogRowModel;
 | 
				
			||||||
 | 
					  test('should return correct error if error is present', () => {
 | 
				
			||||||
 | 
					    expect(checkLogsError(log)).toStrictEqual({ hasError: true, errorMessage: 'Error Message' });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -210,3 +210,16 @@ export const sortLogsResult = (logsResult: LogsModel | null, sortOrder: LogsSort
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const sortLogRows = (logRows: LogRowModel[], sortOrder: LogsSortOrder) =>
 | 
					export const sortLogRows = (logRows: LogRowModel[], sortOrder: LogsSortOrder) =>
 | 
				
			||||||
  sortOrder === LogsSortOrder.Ascending ? logRows.sort(sortInAscendingOrder) : logRows.sort(sortInDescendingOrder);
 | 
					  sortOrder === LogsSortOrder.Ascending ? logRows.sort(sortInAscendingOrder) : logRows.sort(sortInDescendingOrder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Currently supports only error condition in Loki logs
 | 
				
			||||||
 | 
					export const checkLogsError = (logRow: LogRowModel): { hasError: boolean; errorMessage?: string } => {
 | 
				
			||||||
 | 
					  if (logRow.labels.__error__) {
 | 
				
			||||||
 | 
					    return {
 | 
				
			||||||
 | 
					      hasError: true,
 | 
				
			||||||
 | 
					      errorMessage: logRow.labels.__error__,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    hasError: false,
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,6 +45,12 @@ describe('LogDetails', () => {
 | 
				
			||||||
      expect(wrapper.text().includes('key1label1key2label2')).toBe(true);
 | 
					      expect(wrapper.text().includes('key1label1key2label2')).toBe(true);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					  describe('when log row has error', () => {
 | 
				
			||||||
 | 
					    it('should not render log level border', () => {
 | 
				
			||||||
 | 
					      const wrapper = setup({ hasError: true }, undefined);
 | 
				
			||||||
 | 
					      expect(wrapper.find({ 'aria-label': 'Log level' }).html()).not.toContain('logs-row__level');
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
  describe('when row entry has parsable fields', () => {
 | 
					  describe('when row entry has parsable fields', () => {
 | 
				
			||||||
    it('should render heading ', () => {
 | 
					    it('should render heading ', () => {
 | 
				
			||||||
      const wrapper = setup(undefined, { entry: 'test=successful' });
 | 
					      const wrapper = setup(undefined, { entry: 'test=successful' });
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,6 +28,7 @@ export interface Props extends Themeable {
 | 
				
			||||||
  showDuplicates: boolean;
 | 
					  showDuplicates: boolean;
 | 
				
			||||||
  getRows: () => LogRowModel[];
 | 
					  getRows: () => LogRowModel[];
 | 
				
			||||||
  className?: string;
 | 
					  className?: string;
 | 
				
			||||||
 | 
					  hasError?: boolean;
 | 
				
			||||||
  onMouseEnter?: () => void;
 | 
					  onMouseEnter?: () => void;
 | 
				
			||||||
  onMouseLeave?: () => void;
 | 
					  onMouseLeave?: () => void;
 | 
				
			||||||
  onClickFilterLabel?: (key: string, value: string) => void;
 | 
					  onClickFilterLabel?: (key: string, value: string) => void;
 | 
				
			||||||
| 
						 | 
					@ -70,6 +71,7 @@ class UnThemedLogDetails extends PureComponent<Props> {
 | 
				
			||||||
    const {
 | 
					    const {
 | 
				
			||||||
      row,
 | 
					      row,
 | 
				
			||||||
      theme,
 | 
					      theme,
 | 
				
			||||||
 | 
					      hasError,
 | 
				
			||||||
      onClickFilterOutLabel,
 | 
					      onClickFilterOutLabel,
 | 
				
			||||||
      onClickFilterLabel,
 | 
					      onClickFilterLabel,
 | 
				
			||||||
      getRows,
 | 
					      getRows,
 | 
				
			||||||
| 
						 | 
					@ -88,6 +90,8 @@ class UnThemedLogDetails extends PureComponent<Props> {
 | 
				
			||||||
    const labelsAvailable = Object.keys(labels).length > 0;
 | 
					    const labelsAvailable = Object.keys(labels).length > 0;
 | 
				
			||||||
    const fields = getAllFields(row, getFieldLinks);
 | 
					    const fields = getAllFields(row, getFieldLinks);
 | 
				
			||||||
    const parsedFieldsAvailable = fields && fields.length > 0;
 | 
					    const parsedFieldsAvailable = fields && fields.length > 0;
 | 
				
			||||||
 | 
					    // If logs with error, we are not showing the level color
 | 
				
			||||||
 | 
					    const levelClassName = cx(!hasError && [style.logsRowLevel, styles.logsRowLevelDetails]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <tr
 | 
					      <tr
 | 
				
			||||||
| 
						 | 
					@ -96,7 +100,7 @@ class UnThemedLogDetails extends PureComponent<Props> {
 | 
				
			||||||
        onMouseLeave={onMouseLeave}
 | 
					        onMouseLeave={onMouseLeave}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        {showDuplicates && <td />}
 | 
					        {showDuplicates && <td />}
 | 
				
			||||||
        <td className={cx(style.logsRowLevel, styles.logsRowLevelDetails)} />
 | 
					        <td className={levelClassName} aria-label="Log level" />
 | 
				
			||||||
        <td colSpan={4}>
 | 
					        <td colSpan={4}>
 | 
				
			||||||
          <div className={style.logDetailsContainer}>
 | 
					          <div className={style.logDetailsContainer}>
 | 
				
			||||||
            <table className={style.logDetailsTable}>
 | 
					            <table className={style.logDetailsTable}>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,8 +8,10 @@ import {
 | 
				
			||||||
  DataQueryResponse,
 | 
					  DataQueryResponse,
 | 
				
			||||||
  GrafanaTheme,
 | 
					  GrafanaTheme,
 | 
				
			||||||
  dateTimeFormat,
 | 
					  dateTimeFormat,
 | 
				
			||||||
 | 
					  checkLogsError,
 | 
				
			||||||
} from '@grafana/data';
 | 
					} from '@grafana/data';
 | 
				
			||||||
import { Icon } from '../Icon/Icon';
 | 
					import { Icon } from '../Icon/Icon';
 | 
				
			||||||
 | 
					import { Tooltip } from '../Tooltip/Tooltip';
 | 
				
			||||||
import { cx, css } from 'emotion';
 | 
					import { cx, css } from 'emotion';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
| 
						 | 
					@ -72,6 +74,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
 | 
				
			||||||
      label: hoverBackground;
 | 
					      label: hoverBackground;
 | 
				
			||||||
      background-color: ${bgColor};
 | 
					      background-color: ${bgColor};
 | 
				
			||||||
    `,
 | 
					    `,
 | 
				
			||||||
 | 
					    errorLogRow: css`
 | 
				
			||||||
 | 
					      label: erroredLogRow;
 | 
				
			||||||
 | 
					      color: ${theme.colors.textWeak};
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
| 
						 | 
					@ -160,22 +166,27 @@ class UnThemedLogRow extends PureComponent<Props, State> {
 | 
				
			||||||
    const { showDetails, showContext, hasHoverBackground } = this.state;
 | 
					    const { showDetails, showContext, hasHoverBackground } = this.state;
 | 
				
			||||||
    const style = getLogRowStyles(theme, row.logLevel);
 | 
					    const style = getLogRowStyles(theme, row.logLevel);
 | 
				
			||||||
    const styles = getStyles(theme);
 | 
					    const styles = getStyles(theme);
 | 
				
			||||||
    const hoverBackground = cx(style.logsRow, { [styles.hoverBackground]: hasHoverBackground });
 | 
					    const { errorMessage, hasError } = checkLogsError(row);
 | 
				
			||||||
 | 
					    const logRowBackground = cx(style.logsRow, {
 | 
				
			||||||
 | 
					      [styles.hoverBackground]: hasHoverBackground,
 | 
				
			||||||
 | 
					      [styles.errorLogRow]: hasError,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
      <>
 | 
					      <>
 | 
				
			||||||
        <tr
 | 
					        <tr className={logRowBackground} onClick={this.toggleDetails}>
 | 
				
			||||||
          className={hoverBackground}
 | 
					 | 
				
			||||||
          onMouseEnter={this.addHoverBackground}
 | 
					 | 
				
			||||||
          onMouseLeave={this.clearHoverBackground}
 | 
					 | 
				
			||||||
          onClick={this.toggleDetails}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          {showDuplicates && (
 | 
					          {showDuplicates && (
 | 
				
			||||||
            <td className={style.logsRowDuplicates}>
 | 
					            <td className={style.logsRowDuplicates}>
 | 
				
			||||||
              {row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
 | 
					              {row.duplicates && row.duplicates > 0 ? `${row.duplicates + 1}x` : null}
 | 
				
			||||||
            </td>
 | 
					            </td>
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
          <td className={style.logsRowLevel} />
 | 
					          <td className={cx({ [style.logsRowLevel]: !hasError })}>
 | 
				
			||||||
 | 
					            {hasError && (
 | 
				
			||||||
 | 
					              <Tooltip content={`Error: ${errorMessage}`} placement="right" theme="error">
 | 
				
			||||||
 | 
					                <Icon className={style.logIconError} name="exclamation-triangle" size="sm" />
 | 
				
			||||||
 | 
					              </Tooltip>
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </td>
 | 
				
			||||||
          {!allowDetails && (
 | 
					          {!allowDetails && (
 | 
				
			||||||
            <td title={showDetails ? 'Hide log details' : 'See log details'} className={style.logsRowToggleDetails}>
 | 
					            <td title={showDetails ? 'Hide log details' : 'See log details'} className={style.logsRowToggleDetails}>
 | 
				
			||||||
              <Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
 | 
					              <Icon className={styles.topVerticalAlign} name={showDetails ? 'angle-down' : 'angle-right'} />
 | 
				
			||||||
| 
						 | 
					@ -207,7 +218,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
 | 
				
			||||||
        </tr>
 | 
					        </tr>
 | 
				
			||||||
        {this.state.showDetails && (
 | 
					        {this.state.showDetails && (
 | 
				
			||||||
          <LogDetails
 | 
					          <LogDetails
 | 
				
			||||||
            className={hoverBackground}
 | 
					            className={logRowBackground}
 | 
				
			||||||
            onMouseEnter={this.addHoverBackground}
 | 
					            onMouseEnter={this.addHoverBackground}
 | 
				
			||||||
            onMouseLeave={this.clearHoverBackground}
 | 
					            onMouseLeave={this.clearHoverBackground}
 | 
				
			||||||
            showDuplicates={showDuplicates}
 | 
					            showDuplicates={showDuplicates}
 | 
				
			||||||
| 
						 | 
					@ -218,6 +229,7 @@ class UnThemedLogRow extends PureComponent<Props, State> {
 | 
				
			||||||
            onClickHideParsedField={onClickHideParsedField}
 | 
					            onClickHideParsedField={onClickHideParsedField}
 | 
				
			||||||
            getRows={getRows}
 | 
					            getRows={getRows}
 | 
				
			||||||
            row={row}
 | 
					            row={row}
 | 
				
			||||||
 | 
					            hasError={hasError}
 | 
				
			||||||
            showParsedFields={showParsedFields}
 | 
					            showParsedFields={showParsedFields}
 | 
				
			||||||
          />
 | 
					          />
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,6 +9,7 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
 | 
				
			||||||
  let logColor = selectThemeVariant({ light: theme.palette.gray5, dark: theme.palette.gray2 }, theme.type);
 | 
					  let logColor = selectThemeVariant({ light: theme.palette.gray5, dark: theme.palette.gray2 }, theme.type);
 | 
				
			||||||
  const borderColor = selectThemeVariant({ light: theme.palette.gray5, dark: theme.palette.gray2 }, theme.type);
 | 
					  const borderColor = selectThemeVariant({ light: theme.palette.gray5, dark: theme.palette.gray2 }, theme.type);
 | 
				
			||||||
  const bgColor = selectThemeVariant({ light: theme.palette.gray5, dark: theme.palette.dark4 }, theme.type);
 | 
					  const bgColor = selectThemeVariant({ light: theme.palette.gray5, dark: theme.palette.dark4 }, theme.type);
 | 
				
			||||||
 | 
					  const hoverBgColor = selectThemeVariant({ light: theme.palette.gray7, dark: theme.palette.dark2 }, theme.type);
 | 
				
			||||||
  const context = css`
 | 
					  const context = css`
 | 
				
			||||||
    label: context;
 | 
					    label: context;
 | 
				
			||||||
    visibility: hidden;
 | 
					    visibility: hidden;
 | 
				
			||||||
| 
						 | 
					@ -92,7 +93,7 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      &:hover {
 | 
					      &:hover {
 | 
				
			||||||
        background: ${theme.colors.bodyBg};
 | 
					        background: ${hoverBgColor};
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    `,
 | 
					    `,
 | 
				
			||||||
    logsRowDuplicates: css`
 | 
					    logsRowDuplicates: css`
 | 
				
			||||||
| 
						 | 
					@ -116,6 +117,10 @@ export const getLogRowStyles = stylesFactory((theme: GrafanaTheme, logLevel?: Lo
 | 
				
			||||||
        background-color: ${logColor};
 | 
					        background-color: ${logColor};
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    `,
 | 
					    `,
 | 
				
			||||||
 | 
					    logIconError: css`
 | 
				
			||||||
 | 
					      color: ${theme.palette.red};
 | 
				
			||||||
 | 
					      margin-left: -5px;
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
    logsRowToggleDetails: css`
 | 
					    logsRowToggleDetails: css`
 | 
				
			||||||
      label: logs-row-toggle-details__level;
 | 
					      label: logs-row-toggle-details__level;
 | 
				
			||||||
      position: relative;
 | 
					      position: relative;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -252,6 +252,81 @@ describe('dataFrameToLogsModel', () => {
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('given one series with error should return expected logs model', () => {
 | 
				
			||||||
 | 
					    const series: DataFrame[] = [
 | 
				
			||||||
 | 
					      new MutableDataFrame({
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'time',
 | 
				
			||||||
 | 
					            type: FieldType.time,
 | 
				
			||||||
 | 
					            values: ['2019-04-26T09:28:11.352440161Z', '2019-04-26T14:42:50.991981292Z'],
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'message',
 | 
				
			||||||
 | 
					            type: FieldType.string,
 | 
				
			||||||
 | 
					            values: [
 | 
				
			||||||
 | 
					              't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
 | 
				
			||||||
 | 
					              't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					            labels: {
 | 
				
			||||||
 | 
					              filename: '/var/log/grafana/grafana.log',
 | 
				
			||||||
 | 
					              job: 'grafana',
 | 
				
			||||||
 | 
					              __error__: 'Failed while parsing',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'id',
 | 
				
			||||||
 | 
					            type: FieldType.string,
 | 
				
			||||||
 | 
					            values: ['foo', 'bar'],
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					        meta: {
 | 
				
			||||||
 | 
					          limit: 1000,
 | 
				
			||||||
 | 
					          custom: {
 | 
				
			||||||
 | 
					            error: 'Error when parsing some of the logs',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					    ];
 | 
				
			||||||
 | 
					    const logsModel = dataFrameToLogsModel(series, 1, 'utc');
 | 
				
			||||||
 | 
					    expect(logsModel.hasUniqueLabels).toBeFalsy();
 | 
				
			||||||
 | 
					    expect(logsModel.rows).toHaveLength(2);
 | 
				
			||||||
 | 
					    expect(logsModel.rows).toMatchObject([
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        entry: 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
 | 
				
			||||||
 | 
					        labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana', __error__: 'Failed while parsing' },
 | 
				
			||||||
 | 
					        logLevel: 'info',
 | 
				
			||||||
 | 
					        uniqueLabels: {},
 | 
				
			||||||
 | 
					        uid: 'foo',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        entry: 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
 | 
				
			||||||
 | 
					        labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana', __error__: 'Failed while parsing' },
 | 
				
			||||||
 | 
					        logLevel: 'error',
 | 
				
			||||||
 | 
					        uniqueLabels: {},
 | 
				
			||||||
 | 
					        uid: 'bar',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(logsModel.series).toHaveLength(2);
 | 
				
			||||||
 | 
					    expect(logsModel.meta).toHaveLength(3);
 | 
				
			||||||
 | 
					    expect(logsModel.meta![0]).toMatchObject({
 | 
				
			||||||
 | 
					      label: 'Common labels',
 | 
				
			||||||
 | 
					      value: series[0].fields[1].labels,
 | 
				
			||||||
 | 
					      kind: LogsMetaKind.LabelsMap,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    expect(logsModel.meta![1]).toMatchObject({
 | 
				
			||||||
 | 
					      label: 'Limit',
 | 
				
			||||||
 | 
					      value: `1000 (2 returned)`,
 | 
				
			||||||
 | 
					      kind: LogsMetaKind.String,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    expect(logsModel.meta![2]).toMatchObject({
 | 
				
			||||||
 | 
					      label: '',
 | 
				
			||||||
 | 
					      value: 'Error when parsing some of the logs',
 | 
				
			||||||
 | 
					      kind: LogsMetaKind.Error,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('given one series without labels should return expected logs model', () => {
 | 
					  it('given one series without labels should return expected logs model', () => {
 | 
				
			||||||
    const series: DataFrame[] = [
 | 
					    const series: DataFrame[] = [
 | 
				
			||||||
      new MutableDataFrame({
 | 
					      new MutableDataFrame({
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -419,11 +419,22 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi
 | 
				
			||||||
  // Hack to print loki stats in Explore. Should be using proper stats display via drawer in Explore (rework in 7.1)
 | 
					  // Hack to print loki stats in Explore. Should be using proper stats display via drawer in Explore (rework in 7.1)
 | 
				
			||||||
  let totalBytes = 0;
 | 
					  let totalBytes = 0;
 | 
				
			||||||
  const queriesVisited: { [refId: string]: boolean } = {};
 | 
					  const queriesVisited: { [refId: string]: boolean } = {};
 | 
				
			||||||
 | 
					  // To add just 1 error message
 | 
				
			||||||
 | 
					  let errorMetaAdded = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for (const series of logSeries) {
 | 
					  for (const series of logSeries) {
 | 
				
			||||||
    const totalBytesKey = series.meta?.custom?.lokiQueryStatKey;
 | 
					    const totalBytesKey = series.meta?.custom?.lokiQueryStatKey;
 | 
				
			||||||
    const { refId } = series; // Stats are per query, keeping track by refId
 | 
					    const { refId } = series; // Stats are per query, keeping track by refId
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!errorMetaAdded && series.meta?.custom?.error) {
 | 
				
			||||||
 | 
					      meta.push({
 | 
				
			||||||
 | 
					        label: '',
 | 
				
			||||||
 | 
					        value: series.meta?.custom.error,
 | 
				
			||||||
 | 
					        kind: LogsMetaKind.Error,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      errorMetaAdded = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (refId && !queriesVisited[refId]) {
 | 
					    if (refId && !queriesVisited[refId]) {
 | 
				
			||||||
      if (totalBytesKey && series.meta?.stats) {
 | 
					      if (totalBytesKey && series.meta?.stats) {
 | 
				
			||||||
        const byteStat = series.meta.stats.find(stat => stat.displayName === totalBytesKey);
 | 
					        const byteStat = series.meta.stats.find(stat => stat.displayName === totalBytesKey);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -40,6 +40,8 @@ function renderMetaItem(value: any, kind: LogsMetaKind) {
 | 
				
			||||||
        <LogLabels labels={value} />
 | 
					        <LogLabels labels={value} />
 | 
				
			||||||
      </span>
 | 
					      </span>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					  } else if (kind === LogsMetaKind.Error) {
 | 
				
			||||||
 | 
					    return <span className="logs-meta-item__error">{value}</span>;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return value;
 | 
					  return value;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,10 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => ({
 | 
				
			||||||
    margin-right: ${theme.spacing.d};
 | 
					    margin-right: ${theme.spacing.d};
 | 
				
			||||||
    display: flex;
 | 
					    display: flex;
 | 
				
			||||||
    align-items: baseline;
 | 
					    align-items: baseline;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    .logs-meta-item__error {
 | 
				
			||||||
 | 
					      color: ${theme.palette.red};
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  `,
 | 
					  `,
 | 
				
			||||||
  metaLabel: css`
 | 
					  metaLabel: css`
 | 
				
			||||||
    margin-right: calc(${theme.spacing.d} / 2);
 | 
					    margin-right: calc(${theme.spacing.d} / 2);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -325,6 +325,10 @@ export function lokiStreamsToDataframes(
 | 
				
			||||||
    const dataFrame = lokiStreamResultToDataFrame(stream, reverse);
 | 
					    const dataFrame = lokiStreamResultToDataFrame(stream, reverse);
 | 
				
			||||||
    enhanceDataFrame(dataFrame, config);
 | 
					    enhanceDataFrame(dataFrame, config);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (meta.custom && dataFrame.fields.some(f => f.labels && Object.keys(f.labels).some(l => l === '__error__'))) {
 | 
				
			||||||
 | 
					      meta.custom.error = 'Error when parsing some of the logs';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      ...dataFrame,
 | 
					      ...dataFrame,
 | 
				
			||||||
      refId: target.refId,
 | 
					      refId: target.refId,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue