/** * Copyright (c) HashiCorp, Inc. * SPDX-License-Identifier: MPL-2.0 */ import { alias } from '@ember/object/computed'; import { assert } from '@ember/debug'; import { htmlSafe } from '@ember/template'; import Evented from '@ember/object/evented'; import EmberObject, { computed } from '@ember/object'; import { computed as overridable } from 'ember-overridable-computed'; import { assign } from '@ember/polyfills'; import queryString from 'query-string'; import { task } from 'ember-concurrency'; import StreamLogger from 'nomad-ui/utils/classes/stream-logger'; import PollLogger from 'nomad-ui/utils/classes/poll-logger'; import { decode } from 'nomad-ui/utils/stream-frames'; import Anser from 'anser'; import classic from 'ember-classic-decorator'; const MAX_OUTPUT_LENGTH = 50000; // eslint-disable-next-line export const fetchFailure = (url) => () => console.warn(`LOG FETCH: Couldn't connect to ${url}`); @classic class Log extends EmberObject.extend(Evented) { // Parameters url = ''; @overridable(() => ({})) params; plainText = false; logFetch() { assert( 'Log objects need a logFetch method, which should have an interface like window.fetch' ); } // Read-only state @alias('logStreamer.poll.isRunning') isStreaming; logPointer = null; logStreamer = null; // The top of the log head = ''; // The bottom of the log tail = ''; // The top or bottom of the log, depending on whether // the logPointer is pointed at head or tail @computed('logPointer', 'head', 'tail') get output() { let logs = this.logPointer === 'head' ? this.head : this.tail; logs = logs.replace(//g, '>'); let colouredLogs = Anser.ansiToHtml(logs); return htmlSafe(colouredLogs); } init() { super.init(); const args = this.getProperties('url', 'params', 'logFetch'); args.write = (chunk) => { let newTail = this.tail + chunk; if (newTail.length > MAX_OUTPUT_LENGTH) { newTail = newTail.substr(newTail.length - MAX_OUTPUT_LENGTH); } this.set('tail', newTail); this.trigger('tick', chunk); }; if (StreamLogger.isSupported) { this.set('logStreamer', StreamLogger.create(args)); } else { this.set('logStreamer', PollLogger.create(args)); } } destroy() { this.stop(); super.destroy(); } @task(function* () { const logFetch = this.logFetch; const queryParams = queryString.stringify( assign( { origin: 'start', offset: 0, }, this.params ) ); const url = `${this.url}?${queryParams}`; this.stop(); const response = yield logFetch(url).then( (res) => res.text(), fetchFailure(url) ); let text = this.plainText ? response : decode(response).message; 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 ----------'; } this.set('head', text); this.set('logPointer', 'head'); }) gotoHead; @task(function* () { const logFetch = this.logFetch; const queryParams = queryString.stringify( assign( { origin: 'end', offset: MAX_OUTPUT_LENGTH, }, this.params ) ); const url = `${this.url}?${queryParams}`; this.stop(); const response = yield logFetch(url).then( (res) => res.text(), fetchFailure(url) ); let text = this.plainText ? response : decode(response).message; this.set('tail', text); this.set('logPointer', 'tail'); }) gotoTail; startStreaming() { this.set('logPointer', 'tail'); return this.logStreamer.start(); } stop() { this.logStreamer.stop(); } } export default Log; export function logger(urlProp, params, logFetch) { return computed(urlProp, params, function () { return Log.create({ logFetch: logFetch.call(this), params: this.get(params), url: this.get(urlProp), }); }); }