mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #15305 from avaly/feature/ansi-colors
Support ANSI colors codes in Loki logs
This commit is contained in:
commit
2c92365969
@ -147,6 +147,7 @@
|
||||
"angular-native-dragdrop": "1.2.2",
|
||||
"angular-route": "1.6.6",
|
||||
"angular-sanitize": "1.6.6",
|
||||
"ansicolor": "1.1.78",
|
||||
"baron": "^3.0.3",
|
||||
"brace": "^0.10.0",
|
||||
"classnames": "^2.2.6",
|
||||
|
@ -68,3 +68,7 @@ export function sanitize(unsanitizedString: string): string {
|
||||
return unsanitizedString;
|
||||
}
|
||||
}
|
||||
|
||||
export function hasAnsiCodes(input: string): boolean {
|
||||
return /\u001b\[\d{1,2}m/.test(input);
|
||||
}
|
||||
|
24
public/app/features/explore/LogMessageAnsi.test.tsx
Normal file
24
public/app/features/explore/LogMessageAnsi.test.tsx
Normal file
@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { LogMessageAnsi } from './LogMessageAnsi';
|
||||
|
||||
describe('<LogMessageAnsi />', () => {
|
||||
it('renders string without ANSI codes', () => {
|
||||
const wrapper = shallow(<LogMessageAnsi value="Lorem ipsum" />);
|
||||
|
||||
expect(wrapper.find('span').exists()).toBe(false);
|
||||
expect(wrapper.text()).toBe('Lorem ipsum');
|
||||
});
|
||||
|
||||
it('renders string with ANSI codes', () => {
|
||||
const value = 'Lorem \u001B[31mipsum\u001B[0m et dolor';
|
||||
const wrapper = shallow(<LogMessageAnsi value={value} />);
|
||||
|
||||
expect(wrapper.find('span')).toHaveLength(1);
|
||||
expect(wrapper.find('span').first().prop('style')).toMatchObject(expect.objectContaining({
|
||||
color: expect.any(String)
|
||||
}));
|
||||
expect(wrapper.find('span').first().text()).toBe('ipsum');
|
||||
});
|
||||
});
|
70
public/app/features/explore/LogMessageAnsi.tsx
Normal file
70
public/app/features/explore/LogMessageAnsi.tsx
Normal file
@ -0,0 +1,70 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import ansicolor from 'ansicolor';
|
||||
|
||||
interface Style {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
interface ParsedChunk {
|
||||
style: Style;
|
||||
text: string;
|
||||
}
|
||||
|
||||
function convertCSSToStyle(css: string): Style {
|
||||
return css.split(/;\s*/).reduce((accumulated, line) => {
|
||||
const match = line.match(/([^:\s]+)\s*:\s*(.+)/);
|
||||
|
||||
if (match && match[1] && match[2]) {
|
||||
const key = match[1].replace(/-(a-z)/g, (_, character) => character.toUpperCase());
|
||||
accumulated[key] = match[2];
|
||||
}
|
||||
|
||||
return accumulated;
|
||||
}, {});
|
||||
}
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
chunks: ParsedChunk[];
|
||||
prevValue: string;
|
||||
}
|
||||
|
||||
export class LogMessageAnsi extends PureComponent<Props, State> {
|
||||
state = {
|
||||
chunks: [],
|
||||
prevValue: '',
|
||||
};
|
||||
|
||||
static getDerivedStateFromProps(props, state) {
|
||||
if (props.value === state.prevValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const parsed = ansicolor.parse(props.value);
|
||||
|
||||
return {
|
||||
chunks: parsed.spans.map((span) => {
|
||||
return span.css ?
|
||||
{
|
||||
style: convertCSSToStyle(span.css),
|
||||
text: span.text
|
||||
} :
|
||||
{ text: span.text };
|
||||
}),
|
||||
prevValue: props.value
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
const { chunks } = this.state;
|
||||
|
||||
return chunks.map(
|
||||
(chunk, index) => chunk.style ?
|
||||
<span key={index} style={chunk.style}>{chunk.text}</span> :
|
||||
chunk.text
|
||||
);
|
||||
}
|
||||
}
|
@ -5,8 +5,9 @@ import classnames from 'classnames';
|
||||
|
||||
import { LogRowModel, LogLabelStatsModel, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model';
|
||||
import { LogLabels } from './LogLabels';
|
||||
import { findHighlightChunksInText } from 'app/core/utils/text';
|
||||
import { findHighlightChunksInText, hasAnsiCodes } from 'app/core/utils/text';
|
||||
import { LogLabelStats } from './LogLabelStats';
|
||||
import { LogMessageAnsi } from './LogMessageAnsi';
|
||||
|
||||
interface Props {
|
||||
highlighterExpressions?: string[];
|
||||
@ -135,6 +136,8 @@ export class LogRow extends PureComponent<Props, State> {
|
||||
const highlightClassName = classnames('logs-row__match-highlight', {
|
||||
'logs-row__match-highlight--preview': previewHighlights,
|
||||
});
|
||||
const containsAnsiCodes = hasAnsiCodes(row.entry);
|
||||
|
||||
return (
|
||||
<div className="logs-row">
|
||||
{showDuplicates && (
|
||||
@ -157,16 +160,19 @@ export class LogRow extends PureComponent<Props, State> {
|
||||
</div>
|
||||
)}
|
||||
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
|
||||
{parsed && (
|
||||
<Highlighter
|
||||
autoEscape
|
||||
highlightTag={FieldHighlight(this.onClickHighlight)}
|
||||
textToHighlight={row.entry}
|
||||
searchWords={parsedFieldHighlights}
|
||||
highlightClassName="logs-row__field-highlight"
|
||||
/>
|
||||
)}
|
||||
{!parsed &&
|
||||
{containsAnsiCodes && <LogMessageAnsi value={row.entry} />}
|
||||
{!containsAnsiCodes &&
|
||||
parsed && (
|
||||
<Highlighter
|
||||
autoEscape
|
||||
highlightTag={FieldHighlight(this.onClickHighlight)}
|
||||
textToHighlight={row.entry}
|
||||
searchWords={parsedFieldHighlights}
|
||||
highlightClassName="logs-row__field-highlight"
|
||||
/>
|
||||
)}
|
||||
{!containsAnsiCodes &&
|
||||
!parsed &&
|
||||
needsHighlighter && (
|
||||
<Highlighter
|
||||
textToHighlight={row.entry}
|
||||
@ -175,7 +181,7 @@ export class LogRow extends PureComponent<Props, State> {
|
||||
highlightClassName={highlightClassName}
|
||||
/>
|
||||
)}
|
||||
{!parsed && !needsHighlighter && row.entry}
|
||||
{!containsAnsiCodes && !parsed && !needsHighlighter && row.entry}
|
||||
{showFieldStats && (
|
||||
<div className="logs-row__stats">
|
||||
<LogLabelStats
|
||||
|
@ -2536,6 +2536,11 @@ ansi-styles@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
|
||||
integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg=
|
||||
|
||||
ansicolor@1.1.78:
|
||||
version "1.1.78"
|
||||
resolved "https://registry.yarnpkg.com/ansicolor/-/ansicolor-1.1.78.tgz#4c1f1dbef81ff3e1292e6f95b4bfb8ba51212db9"
|
||||
integrity sha512-mdNo/iRwUyb4Z0L8AthEV4BZ3TlSWr6YakKtItA48ufGBzYYtTVp+gX6bkweKTfs7wGpUepOz+qHrTPqfBus2Q==
|
||||
|
||||
ansicolors@~0.3.2:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979"
|
||||
|
Loading…
Reference in New Issue
Block a user