Skip to content
This repository has been archived by the owner on Sep 6, 2022. It is now read-only.

Commit

Permalink
add colors to logs from ANSI control chars. (#1103)
Browse files Browse the repository at this point in the history
  • Loading branch information
snatchev committed Aug 3, 2018
1 parent c8a6e8d commit e42a223
Show file tree
Hide file tree
Showing 16 changed files with 246 additions and 17 deletions.
9 changes: 2 additions & 7 deletions app/features-json/build_json_controller.rb
Expand Up @@ -107,7 +107,7 @@ def self.build_url(project_id:, build_number:)
logger.debug("streaming back artifact: #{build_log_artifact.reference}")
File.open(build_log_artifact.reference, "r") do |file|
file.each_line do |line|
ws.send(convert_ansi_to_plain_text(line.chomp))
ws.send(line.chomp)
end
end
ws.close(1000, "runner complete.")
Expand Down Expand Up @@ -141,7 +141,7 @@ def self.build_url(project_id:, build_number:)
# subscribe the current socket to events from the remote_runner
# as soon as a subscriber is returned, they will receive all historical items as well.
@subscriber = current_build_runner.subscribe do |_topic, payload|
ws.send(convert_ansi_to_plain_text(JSON.dump(payload)))
ws.send(JSON.dump(payload))
end
end

Expand Down Expand Up @@ -188,10 +188,5 @@ def current_project

return current_project
end

# convert .log files that include the color information as ANSI code to plain text
def convert_ansi_to_plain_text(data)
return data.gsub(/\e\[[0-9;]*m/, "")
end
end
end
5 changes: 1 addition & 4 deletions web/app/build/build.component.html
Expand Up @@ -7,10 +7,7 @@
<span *ngIf="build && build.description" class="fci-build-description">{{build.description}}</span>
</div>
<div class="fci-build-logs">
<ng-container *ngIf="logs.length <= 0">Connecting...</ng-container>
<ng-container *ngIf="logs.length > 0">
<ng-container *ngFor="let log of logs"><pre>{{log.message}}</pre></ng-container>
</ng-container>
<fci-log-viewer [logLines]="logs"></fci-log-viewer>
</div>
</div>
<div class="fci-right-container">
Expand Down
3 changes: 0 additions & 3 deletions web/app/build/build.component.scss
Expand Up @@ -17,9 +17,6 @@ $TOOLBAR_HEIGHT:80px;
min-width: 926px;
max-width: 926px;
overflow-x: auto;
pre {
margin: 0;
}
}
.fci-build-header {
margin-bottom: 16px;
Expand Down
4 changes: 3 additions & 1 deletion web/app/build/build.component.spec.ts
Expand Up @@ -11,6 +11,7 @@ import {Subject} from 'rxjs/Subject';

import {StatusIconModule} from '../common/components/status-icon/status-icon.module';
import {ToolbarModule} from '../common/components/toolbar/toolbar.module';
import {LogViewerModule} from '../common/components/log-viewer/log-viewer.module';
import {BuildStatus} from '../common/constants';
import {expectElementNotToExist, expectElementToExist, getAllElements, getElement} from '../common/test_helpers/element_helper_functions';
import {mockBuild, mockBuildResponse} from '../common/test_helpers/mock_build_data';
Expand Down Expand Up @@ -50,12 +51,13 @@ describe('BuildComponent', () => {

TestBed
.configureTestingModule({

declarations: [BuildComponent, DummyComponent],
imports: [
ToolbarModule, StatusIconModule, RouterTestingModule.withRoutes(
[{path: '404', component: DummyComponent} ]
),
MatCardModule, MatProgressSpinnerModule, MomentModule
MatCardModule, MatProgressSpinnerModule, MomentModule, LogViewerModule
],
providers: [
{
Expand Down
3 changes: 2 additions & 1 deletion web/app/build/build.module.ts
Expand Up @@ -8,6 +8,7 @@ import {ToolbarModule} from '../common/components/toolbar/toolbar.module';
import {BuildLogWebsocketService} from '../services/build-log-websocket.service';

import {BuildComponent} from './build.component';
import {LogViewerModule} from '../common/components/log-viewer/log-viewer.module';

@NgModule({
declarations: [BuildComponent],
Expand All @@ -16,7 +17,7 @@ import {BuildComponent} from './build.component';
/** Angular Library Imports */
CommonModule,
/** Internal Imports */
ToolbarModule, StatusIconModule,
ToolbarModule, StatusIconModule, LogViewerModule,
/** Angular Material Imports */
MatCardModule, MatProgressSpinnerModule,
/** Third-Party Module Imports */
Expand Down
@@ -0,0 +1 @@
<div class="fci-log-line" #logLine></div>
@@ -0,0 +1,17 @@
.fci-log-line {
display: block;
font-family: monospace;
white-space: pre;
margin: 0;
}
.fci-ansi-1 { font-weight: bold;}
.fci-ansi-3 { font-style: italic; }
.fci-ansi-4 { text-decoration: underline; }
.fci-ansi-30 { color: #262626; }
.fci-ansi-31 { color: #d30102; }
.fci-ansi-32 { color: #859900; }
.fci-ansi-33 { color: #b58900; }
.fci-ansi-34 { color: #268bd2; }
.fci-ansi-35 { color: #d33682; }
.fci-ansi-36 { color: #2aa198; }
.fci-ansi-37 { color: #e4e4e4; }
@@ -0,0 +1,37 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { LogLineComponent, LogLine } from './log-line.component';
import { Component, DebugElement } from '@angular/core';
import { getElement, getElementText } from '../../../test_helpers/element_helper_functions';

describe('LogLineComponent', () => {
let component: LogLineComponent;
let fixture: ComponentFixture<LogLineComponent>;
let fixtureEl: DebugElement;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [LogLineComponent, LogLineComponent]
}).compileComponents();

fixture = TestBed.createComponent(LogLineComponent);
fixtureEl = fixture.debugElement;
component = fixture.componentInstance;
component.log = {
message: '[16:12:08]: \u001b[33m▸\u001b[0m \u001b[39;1mCompiling\u001b[0m Category.swift\n',
level: 'DEBUG',
status: 0,
timestamp: 1531944769
};

fixture.detectChanges(); // onInit
}));

it('should create the correct span dom tree of ansi codes', () => {
const parentEl = getElement(fixtureEl, '.fci-log-line');

expect(parentEl.nativeElement.innerHTML).toBe(
'[16:12:08]: <span class="fci-ansi-33">▸</span> ' +
'<span class="fci-ansi-39 fci-ansi-1">Compiling</span>' +
' Category.swift\n');
});
});
@@ -0,0 +1,83 @@
import { DOCUMENT } from '@angular/common';
import { Component, AfterViewInit, ViewChild, ElementRef, Input, Inject, ViewEncapsulation } from '@angular/core';

const ANSI_PATTERN = /\u001b\[([0-9;]+)?m/;
const ANSI_RESET_CODE = '0';

type AnsiCode = string;
type AnsiTuple = [AnsiCode, string];

export interface LogLine {
timestamp: number;
message: string;
level: string;
status: number;
}

@Component({
selector: 'fci-log-line',
templateUrl: './log-line.component.html',
styleUrls: ['./log-line.component.scss'],
encapsulation: ViewEncapsulation.None
})

export class LogLineComponent implements AfterViewInit {
@Input() log: LogLine;
@ViewChild('logLine', { read: ElementRef }) logLineEl: ElementRef;

constructor(@Inject(DOCUMENT) private readonly document: any) {}

ngAfterViewInit() {
const parentEl = this.logLineEl.nativeElement;
let currentSpanEl = parentEl;
const stack = this.tokenize(this.log.message);

for (const tuple of stack) {
const [code, text] = tuple;
if (code === ANSI_RESET_CODE) {
parentEl.innerHTML += text;
currentSpanEl = parentEl;
} else {
currentSpanEl = this.injectSpan(currentSpanEl, text, code);
}
}
}

/**
* tokenization works by splitting the string by a regex that has a capture group
* this will return an flat array of tuples (string, capture)
* this method will split and group the result into an array of tuples [ansi code, text]
* NOTE: that the capture comes after the string, so we transpose them.
**/
private tokenize(ansiText: string): AnsiTuple[] {
const tuples = ansiText.split(ANSI_PATTERN);

// if the text starts with a match capture (and thus returning empty string as [0]),
// use that captured ansi code as the beginning style
if (tuples[0] === '') {
tuples.shift();
} else {
// otherwise, we must assume we are starting each line as the default '0'
tuples.unshift(ANSI_RESET_CODE);
}

const stack: AnsiTuple[] = [];

for (let i = 0; i < tuples.length; i += 2) {
const code = tuples[i];
const text = tuples[i + 1];
stack.push([code, text]);
}
return stack;
}

private injectSpan(parent: HTMLElement, text: string, ansiCode: AnsiCode): HTMLSpanElement {
const span = this.document.createElement('span');
const classNames = ansiCode.split(';').map(c => `fci-ansi-${c}`).join(' ');
span.className = classNames;
span.innerText = text;
parent.appendChild(span);

return span;
}
}
19 changes: 19 additions & 0 deletions web/app/common/components/log-viewer/log-line/log-line.module.ts
@@ -0,0 +1,19 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';

import {LogLineComponent} from './log-line.component';

@NgModule({
declarations: [LogLineComponent],
imports: [
/** Angular Library Imports */
CommonModule
/** Internal Imports */
/** Angular Material Imports */
/** Third-Party Module Imports */
],
exports: [LogLineComponent]
})

export class LogLineModule {
}
@@ -0,0 +1,4 @@
<ng-container *ngIf="logLines.length <= 0">Connecting...</ng-container>
<ng-container class="fci-log-viewer" *ngFor="let log of logLines">
<fci-log-line [log]="log"></fci-log-line>
</ng-container>
@@ -0,0 +1 @@

39 changes: 39 additions & 0 deletions web/app/common/components/log-viewer/log-viewer.component.spec.ts
@@ -0,0 +1,39 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';

import { LogViewerComponent } from './log-viewer.component';
import { LogLineModule } from './log-line/log-line.module';
import { LogLine } from './log-line/log-line.component';
import { getElement } from '../../test_helpers/element_helper_functions';

describe('LogViewerComponent', () => {
let component: LogViewerComponent;
let fixture: ComponentFixture<LogViewerComponent>;

beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ LogViewerComponent ],
imports: [LogLineModule]
})
.compileComponents();
}));

beforeEach(() => {
fixture = TestBed.createComponent(LogViewerComponent);
component = fixture.componentInstance;
component.logLines = [{
timestamp: 1533326666,
message: 'this was a message',
level: 'DEBUG',
status: 0,
}];

fixture.detectChanges();
});

/*
it('should create a log viewer component with log lines', () => {
const element = getElement(fixture.debugElement, '.fci-log-viewer');
expect(element.nativeElement.innerHTML).toEqual('');
});
*/
});
16 changes: 16 additions & 0 deletions web/app/common/components/log-viewer/log-viewer.component.ts
@@ -0,0 +1,16 @@
import {Component, OnInit, Input} from '@angular/core';
import {LogLine} from './log-line/log-line.component';

@Component({
selector: 'fci-log-viewer',
templateUrl: './log-viewer.component.html',
styleUrls: ['./log-viewer.component.scss']
})
export class LogViewerComponent implements OnInit {
@Input() logLines: LogLine[] = [];
constructor() { }

ngOnInit() {
}

}
20 changes: 20 additions & 0 deletions web/app/common/components/log-viewer/log-viewer.module.ts
@@ -0,0 +1,20 @@
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';

import {LogViewerComponent} from './log-viewer.component';
import {LogLineModule} from './log-line/log-line.module';

@NgModule({
declarations: [LogViewerComponent],
imports: [
/** Angular Library Imports */
CommonModule,
/** Internal Imports */
LogLineModule,
/** Angular Material Imports */
/** Third-Party Module Imports */
],
exports: [LogViewerComponent]
})
export class LogViewerModule {
}
2 changes: 1 addition & 1 deletion web/app/services/build-log-websocket.service.spec.ts
Expand Up @@ -23,7 +23,7 @@ describe('BuildLogWebsocketService', () => {
it('should attempt to connect to correct socket', () => {
const socket = buildLogWebsocketService.createSocket('pId', 3);
socket.close();
expect(socket.url).toBe('ws://host/data/projects/pId/build/3/log.ws?bearer_token=null')
expect(socket.url).toBe('ws://host/data/projects/pId/build/3/log.ws?bearer_token=null');
});

describe('socket connection', () => {
Expand Down

0 comments on commit e42a223

Please sign in to comment.