瀏覽代碼

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

Support ANSI colors codes in Loki logs
David 6 年之前
父節點
當前提交
2c92365969

+ 1 - 0
package.json

@@ -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",

+ 4 - 0
public/app/core/utils/text.ts

@@ -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 - 0
public/app/features/explore/LogMessageAnsi.test.tsx

@@ -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 - 0
public/app/features/explore/LogMessageAnsi.tsx

@@ -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
+    );
+  }
+}

+ 18 - 12
public/app/features/explore/LogRow.tsx

@@ -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

+ 5 - 0
yarn.lock

@@ -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"