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 @@ ...@@ -147,6 +147,7 @@
"angular-native-dragdrop": "1.2.2", "angular-native-dragdrop": "1.2.2",
"angular-route": "1.6.6", "angular-route": "1.6.6",
"angular-sanitize": "1.6.6", "angular-sanitize": "1.6.6",
"ansicolor": "1.1.78",
"baron": "^3.0.3", "baron": "^3.0.3",
"brace": "^0.10.0", "brace": "^0.10.0",
"classnames": "^2.2.6", "classnames": "^2.2.6",
......
...@@ -68,3 +68,7 @@ export function sanitize(unsanitizedString: string): string { ...@@ -68,3 +68,7 @@ export function sanitize(unsanitizedString: string): string {
return unsanitizedString; 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'; ...@@ -5,8 +5,9 @@ import classnames from 'classnames';
import { LogRowModel, LogLabelStatsModel, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model'; import { LogRowModel, LogLabelStatsModel, LogsParser, calculateFieldStats, getParser } from 'app/core/logs_model';
import { LogLabels } from './LogLabels'; import { LogLabels } from './LogLabels';
import { findHighlightChunksInText } from 'app/core/utils/text'; import { findHighlightChunksInText, hasAnsiCodes } from 'app/core/utils/text';
import { LogLabelStats } from './LogLabelStats'; import { LogLabelStats } from './LogLabelStats';
import { LogMessageAnsi } from './LogMessageAnsi';
interface Props { interface Props {
highlighterExpressions?: string[]; highlighterExpressions?: string[];
...@@ -135,6 +136,8 @@ export class LogRow extends PureComponent<Props, State> { ...@@ -135,6 +136,8 @@ export class LogRow extends PureComponent<Props, State> {
const highlightClassName = classnames('logs-row__match-highlight', { const highlightClassName = classnames('logs-row__match-highlight', {
'logs-row__match-highlight--preview': previewHighlights, 'logs-row__match-highlight--preview': previewHighlights,
}); });
const containsAnsiCodes = hasAnsiCodes(row.entry);
return ( return (
<div className="logs-row"> <div className="logs-row">
{showDuplicates && ( {showDuplicates && (
...@@ -157,16 +160,19 @@ export class LogRow extends PureComponent<Props, State> { ...@@ -157,16 +160,19 @@ export class LogRow extends PureComponent<Props, State> {
</div> </div>
)} )}
<div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}> <div className="logs-row__message" onMouseEnter={this.onMouseOverMessage} onMouseLeave={this.onMouseOutMessage}>
{parsed && ( {containsAnsiCodes && <LogMessageAnsi value={row.entry} />}
<Highlighter {!containsAnsiCodes &&
autoEscape parsed && (
highlightTag={FieldHighlight(this.onClickHighlight)} <Highlighter
textToHighlight={row.entry} autoEscape
searchWords={parsedFieldHighlights} highlightTag={FieldHighlight(this.onClickHighlight)}
highlightClassName="logs-row__field-highlight" textToHighlight={row.entry}
/> searchWords={parsedFieldHighlights}
)} highlightClassName="logs-row__field-highlight"
{!parsed && />
)}
{!containsAnsiCodes &&
!parsed &&
needsHighlighter && ( needsHighlighter && (
<Highlighter <Highlighter
textToHighlight={row.entry} textToHighlight={row.entry}
...@@ -175,7 +181,7 @@ export class LogRow extends PureComponent<Props, State> { ...@@ -175,7 +181,7 @@ export class LogRow extends PureComponent<Props, State> {
highlightClassName={highlightClassName} highlightClassName={highlightClassName}
/> />
)} )}
{!parsed && !needsHighlighter && row.entry} {!containsAnsiCodes && !parsed && !needsHighlighter && row.entry}
{showFieldStats && ( {showFieldStats && (
<div className="logs-row__stats"> <div className="logs-row__stats">
<LogLabelStats <LogLabelStats
......
...@@ -2536,6 +2536,11 @@ ansi-styles@~1.0.0: ...@@ -2536,6 +2536,11 @@ ansi-styles@~1.0.0:
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
integrity sha1-yxAt8cVvUSPquLZ817mAJ6AnkXg= 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: ansicolors@~0.3.2:
version "0.3.2" version "0.3.2"
resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.3.2.tgz#665597de86a9ffe3aa9bfbe6cae5c6ea426b4979" 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