Commit 2c923659 by David Committed by GitHub

Merge pull request #15305 from avaly/feature/ansi-colors

Support ANSI colors codes in Loki logs
parents cd8f5835 ec0e03e5
......@@ -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);
}
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');
});
});
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"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment