Skip to content

Commit

Permalink
Fallback to using the nomad server for log streaming
Browse files Browse the repository at this point in the history
Only when the client isn't accessible
  • Loading branch information
DingoEatingFuzz committed Feb 26, 2018
1 parent 39f9914 commit 5346f65
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 33 deletions.
27 changes: 22 additions & 5 deletions ui/app/components/task-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ import { inject as service } from '@ember/service';
import Component from '@ember/component';
import { computed } from '@ember/object';
import { run } from '@ember/runloop';
import RSVP from 'rsvp';
import { task } from 'ember-concurrency';
import { logger } from 'nomad-ui/utils/classes/log';
import WindowResizable from 'nomad-ui/mixins/window-resizable';
import timeout from 'nomad-ui/utils/timeout';

export default Component.extend(WindowResizable, {
token: service(),
Expand All @@ -14,6 +16,8 @@ export default Component.extend(WindowResizable, {
allocation: null,
task: null,

useServer: false,

didReceiveAttrs() {
if (this.get('allocation') && this.get('task')) {
this.send('toggleStream');
Expand All @@ -37,11 +41,12 @@ export default Component.extend(WindowResizable, {

mode: 'stdout',

logUrl: computed('allocation.id', 'allocation.node.httpAddr', function() {
logUrl: computed('allocation.id', 'allocation.node.httpAddr', 'useServer', function() {
const address = this.get('allocation.node.httpAddr');
const allocation = this.get('allocation.id');

return `//${address}/v1/client/fs/logs/${allocation}`;
const url = `/v1/client/fs/logs/${allocation}`;
return this.get('useServer') ? url : `//${address}${url}`;
}),

logParams: computed('task', 'mode', function() {
Expand All @@ -51,9 +56,18 @@ export default Component.extend(WindowResizable, {
};
}),

logger: logger('logUrl', 'logParams', function() {
const token = this.get('token');
return token.authorizedRequest.bind(token);
logger: logger('logUrl', 'logParams', function logFetch() {
// If the log request can't settle in one second, the client
// must be unavailable and the server should be used instead
return url =>
RSVP.race([this.get('token').authorizedRequest(url), timeout(1000)]).then(
response => response,
error => {
this.send('failoverToServer');
this.get('stream').perform();
throw error;
}
);
}),

head: task(function*() {
Expand Down Expand Up @@ -100,5 +114,8 @@ export default Component.extend(WindowResizable, {
this.get('stream').perform();
}
},
failoverToServer() {
this.set('useServer', true);
},
},
});
9 changes: 6 additions & 3 deletions ui/app/utils/classes/log.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Ember from 'ember';
import { alias } from '@ember/object/computed';
import { assert } from '@ember/debug';
import Evented from '@ember/object/evented';
Expand All @@ -10,6 +11,8 @@ import PollLogger from 'nomad-ui/utils/classes/poll-logger';

const MAX_OUTPUT_LENGTH = 50000;

export const fetchFailure = url => () => Ember.Logger.warn(`LOG FETCH: Couldn't connect to ${url}`);

const Log = EmberObject.extend(Evented, {
// Parameters

Expand Down Expand Up @@ -74,9 +77,9 @@ const Log = EmberObject.extend(Evented, {
const url = `${this.get('url')}?${queryParams}`;

this.stop();
let text = yield logFetch(url).then(res => res.text());
let text = yield logFetch(url).then(res => res.text(), fetchFailure(url));

if (text.length > MAX_OUTPUT_LENGTH) {
if (text && text.length > MAX_OUTPUT_LENGTH) {
text = text.substr(0, MAX_OUTPUT_LENGTH);
text += '\n\n---------- TRUNCATED: Click "tail" to view the bottom of the log ----------';
}
Expand All @@ -96,7 +99,7 @@ const Log = EmberObject.extend(Evented, {
const url = `${this.get('url')}?${queryParams}`;

this.stop();
let text = yield logFetch(url).then(res => res.text());
let text = yield logFetch(url).then(res => res.text(), fetchFailure(url));

this.set('tail', text);
this.set('logPointer', 'tail');
Expand Down
10 changes: 9 additions & 1 deletion ui/app/utils/classes/poll-logger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import EmberObject from '@ember/object';
import { task, timeout } from 'ember-concurrency';
import AbstractLogger from './abstract-logger';
import { fetchFailure } from './log';

export default EmberObject.extend(AbstractLogger, {
interval: 1000,
Expand All @@ -18,7 +19,14 @@ export default EmberObject.extend(AbstractLogger, {
poll: task(function*() {
const { interval, logFetch } = this.getProperties('interval', 'logFetch');
while (true) {
let text = yield logFetch(this.get('fullUrl')).then(res => res.text());
const url = this.get('fullUrl');
let response = yield logFetch(url).then(res => res, fetchFailure(url));

if (!response) {
return;
}

let text = yield response.text();

if (text) {
const lines = text.replace(/\}\{/g, '}\n{').split('\n');
Expand Down
7 changes: 6 additions & 1 deletion ui/app/utils/classes/stream-logger.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import EmberObject, { computed } from '@ember/object';
import { task } from 'ember-concurrency';
import TextDecoder from 'nomad-ui/utils/classes/text-decoder';
import AbstractLogger from './abstract-logger';
import { fetchFailure } from './log';

export default EmberObject.extend(AbstractLogger, {
reader: null,
Expand Down Expand Up @@ -30,7 +31,11 @@ export default EmberObject.extend(AbstractLogger, {
let buffer = '';

const decoder = new TextDecoder();
const reader = yield logFetch(url).then(res => res.body.getReader());
const reader = yield logFetch(url).then(res => res.body.getReader(), fetchFailure(url));

if (!reader) {
return;
}

this.set('reader', reader);

Expand Down
73 changes: 50 additions & 23 deletions ui/tests/integration/task-log-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,33 @@ let streamPointer = 0;
moduleForComponent('task-log', 'Integration | Component | task log', {
integration: true,
beforeEach() {
const handler = ({ queryParams }) => {
const { origin, offset, plain, follow } = queryParams;

let frames;
let data;

if (origin === 'start' && offset === '0' && plain && !follow) {
frames = logHead;
} else if (origin === 'end' && plain && !follow) {
frames = logTail;
} else {
frames = streamFrames;
}

if (frames === streamFrames) {
data = queryParams.plain ? frames[streamPointer] : logEncode(frames, streamPointer);
streamPointer++;
} else {
data = queryParams.plain ? frames.join('') : logEncode(frames, frames.length - 1);
}

return [200, {}, data];
};

this.server = new Pretender(function() {
this.get(`http://${HOST}/v1/client/fs/logs/:allocation_id`, ({ queryParams }) => {
const { origin, offset, plain, follow } = queryParams;

let frames;
let data;

if (origin === 'start' && offset === '0' && plain && !follow) {
frames = logHead;
} else if (origin === 'end' && plain && !follow) {
frames = logTail;
} else {
frames = streamFrames;
}

if (frames === streamFrames) {
data = queryParams.plain ? frames[streamPointer] : logEncode(frames, streamPointer);
streamPointer++;
} else {
data = queryParams.plain ? frames.join('') : logEncode(frames, frames.length - 1);
}

return [200, {}, data];
});
this.get(`http://${HOST}/v1/client/fs/logs/:allocation_id`, handler);
this.get('/v1/client/fs/logs/:allocation_id', handler);
});
},
afterEach() {
Expand Down Expand Up @@ -174,3 +177,27 @@ test('Clicking stderr switches the log to standard error', function(assert) {
);
});
});

test('When the client is inaccessible, task-log falls back to requesting logs through the server', function(assert) {
run.later(run, run.cancelTimers, 2000);

// override client response to timeout
this.server.get(`http://${HOST}/v1/client/fs/logs/:allocation_id`, () => [400, {}, ''], 2000);

this.setProperties(commonProps);
this.render(hbs`{{task-log allocation=allocation task=task}}`);

const clientUrlRegex = new RegExp(`${HOST}/v1/client/fs/logs/${commonProps.allocation.id}`);
assert.ok(
this.server.handledRequests.filter(req => clientUrlRegex.test(req.url)).length,
'Log request was initially made directly to the client'
);

return wait().then(() => {
const serverUrl = `/v1/client/fs/logs/${commonProps.allocation.id}`;
assert.ok(
this.server.handledRequests.filter(req => req.url.startsWith(serverUrl)).length,
'Log request was later made to the server'
);
});
});

0 comments on commit 5346f65

Please sign in to comment.