Fallback to using the nomad server for log streaming

Only when the client isn't accessible
This commit is contained in:
Michael Lange 2018-02-26 12:23:01 -08:00
parent 470b8131bd
commit dc72ac2bc7
5 changed files with 93 additions and 33 deletions

View file

@ -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(),
@ -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');
@ -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() {
@ -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*() {
@ -100,5 +114,8 @@ export default Component.extend(WindowResizable, {
this.get('stream').perform();
}
},
failoverToServer() {
this.set('useServer', true);
},
},
});

View file

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

View file

@ -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,
@ -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');

View file

@ -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,
@ -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);

View file

@ -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() {
@ -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'
);
});
});