From dc72ac2bc780259db6037027973741a8e490a54d Mon Sep 17 00:00:00 2001 From: Michael Lange Date: Mon, 26 Feb 2018 12:23:01 -0800 Subject: [PATCH] Fallback to using the nomad server for log streaming Only when the client isn't accessible --- ui/app/components/task-log.js | 27 ++++++++-- ui/app/utils/classes/log.js | 9 ++-- ui/app/utils/classes/poll-logger.js | 10 +++- ui/app/utils/classes/stream-logger.js | 7 ++- ui/tests/integration/task-log-test.js | 73 ++++++++++++++++++--------- 5 files changed, 93 insertions(+), 33 deletions(-) diff --git a/ui/app/components/task-log.js b/ui/app/components/task-log.js index 277023859..fcc911a1c 100644 --- a/ui/app/components/task-log.js +++ b/ui/app/components/task-log.js @@ -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); + }, }, }); diff --git a/ui/app/utils/classes/log.js b/ui/app/utils/classes/log.js index 5e873182a..7791aef49 100644 --- a/ui/app/utils/classes/log.js +++ b/ui/app/utils/classes/log.js @@ -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'); diff --git a/ui/app/utils/classes/poll-logger.js b/ui/app/utils/classes/poll-logger.js index e6c7078a7..4bc73b14d 100644 --- a/ui/app/utils/classes/poll-logger.js +++ b/ui/app/utils/classes/poll-logger.js @@ -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'); diff --git a/ui/app/utils/classes/stream-logger.js b/ui/app/utils/classes/stream-logger.js index ee1018734..da4acc751 100644 --- a/ui/app/utils/classes/stream-logger.js +++ b/ui/app/utils/classes/stream-logger.js @@ -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); diff --git a/ui/tests/integration/task-log-test.js b/ui/tests/integration/task-log-test.js index 62f849ae6..cf60e5999 100644 --- a/ui/tests/integration/task-log-test.js +++ b/ui/tests/integration/task-log-test.js @@ -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' + ); + }); +});