diff --git a/babel.config.js b/babel.config.js index 6d852034..12174b47 100644 --- a/babel.config.js +++ b/babel.config.js @@ -3,7 +3,8 @@ module.exports = { "@babel/preset-typescript" ], plugins: [ + ["@babel/plugin-proposal-decorators", { legacy: true }], "@babel/plugin-proposal-class-properties", - "@babel/transform-modules-commonjs" + "@babel/transform-modules-commonjs", ] } diff --git a/package.json b/package.json index ce3c4e0a..052211b9 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ } }, "devDependencies": { + "@babel/plugin-proposal-decorators": "^7.4.4", "@babel/preset-typescript": "~7.3.3", "@commitlint/cli": "^8.0.0", "@commitlint/config-conventional": "^8.0.0", diff --git a/packages/@best/agent-frontend/.eslintrc.json b/packages/@best/agent-frontend/.eslintrc.json new file mode 100644 index 00000000..5e16c2bc --- /dev/null +++ b/packages/@best/agent-frontend/.eslintrc.json @@ -0,0 +1,17 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "extends": ["@salesforce/eslint-config-lwc/recommended", "plugin:@typescript-eslint/recommended"], + "rules": { + "@lwc/lwc/no-async-operation": "warn", + "@lwc/lwc/no-inner-html": "warn", + "@lwc/lwc/no-document-query": "warn", + "@typescript-eslint/camelcase": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/explicit-member-accessibility": "off", + "@typescript-eslint/no-explicit-any": "off" + }, + "parserOptions": { + "ecmaVersion": 2018 + } +} diff --git a/packages/@best/agent-frontend/README.md b/packages/@best/agent-frontend/README.md new file mode 100644 index 00000000..072031de --- /dev/null +++ b/packages/@best/agent-frontend/README.md @@ -0,0 +1 @@ +# Aagent Frontend \ No newline at end of file diff --git a/packages/@best/agent-frontend/jest.config.js b/packages/@best/agent-frontend/jest.config.js new file mode 100644 index 00000000..c83f0c70 --- /dev/null +++ b/packages/@best/agent-frontend/jest.config.js @@ -0,0 +1,12 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const COMMON = require('../../../scripts/jest/common.config') + +module.exports = { + ...COMMON, + displayName: 'agent-frontend', + preset: '@lwc/jest-preset', + moduleNameMapper: { + "^component-emitter$": "component-emitter", + "^(component|my|view|store)(.+)$": "/src/modules/$1$2$2" + } +} diff --git a/packages/@best/agent-frontend/jsconfig.json b/packages/@best/agent-frontend/jsconfig.json new file mode 100644 index 00000000..faf280bd --- /dev/null +++ b/packages/@best/agent-frontend/jsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "experimentalDecorators": true + }, + "typeAcquisition": { + "include": ["jest"] + } +} diff --git a/packages/@best/agent-frontend/lwc-services.config.js b/packages/@best/agent-frontend/lwc-services.config.js new file mode 100644 index 00000000..6cf79439 --- /dev/null +++ b/packages/@best/agent-frontend/lwc-services.config.js @@ -0,0 +1,10 @@ +// Find the full example of all available configuration options at +// https://github.com/muenzpraeger/lwc-create-app/blob/master/packages/lwc-services/example/lwc-services.config.js +module.exports = { + resources: [{ from: 'src/client/resources', to: 'dist/resources' }], + sourceDir: './src/client', + moduleDir: './src/client/modules', + server: { + customConfig: './src/server/index.js' + } +}; diff --git a/packages/@best/agent-frontend/package.json b/packages/@best/agent-frontend/package.json new file mode 100644 index 00000000..f4a7cace --- /dev/null +++ b/packages/@best/agent-frontend/package.json @@ -0,0 +1,36 @@ +{ + "name": "@best/agent-frontend", + "version": "4.0.0", + "main": "build/index.js", + "dependencies": { + "@best/agent-logger": "4.0.0", + "express": "^4.17.1", + "lwc-services": "^1", + "socket.io-client": "~2.2.0", + "socket.io": "~2.2.0" + }, + "devDependencies": { + "husky": "^2.3", + "lint-staged": "^8.1.5" + }, + "husky": { + "hooks": { + "pre-commit": "lint-staged" + } + }, + "lint-staged": { + "**/modules/**/*.js": [ + "eslint" + ], + "*": [ + "git add" + ] + }, + "scripts": { + "build": "lwc-services build -m production", + "build:development": "lwc-services build", + "lint": "eslint ./src/**/*.js", + "serve": "lwc-services serve", + "watch": "lwc-services watch" + } +} diff --git a/packages/@best/agent-frontend/src/client/index.html b/packages/@best/agent-frontend/src/client/index.html new file mode 100644 index 00000000..7f907f30 --- /dev/null +++ b/packages/@best/agent-frontend/src/client/index.html @@ -0,0 +1,21 @@ + + + + + Best Agent Dashboard + + + + + + + + diff --git a/packages/@best/agent-frontend/src/client/index.js b/packages/@best/agent-frontend/src/client/index.js new file mode 100644 index 00000000..784e97df --- /dev/null +++ b/packages/@best/agent-frontend/src/client/index.js @@ -0,0 +1,4 @@ +import { buildCustomElementConstructor } from 'lwc'; +import ViewDashboard from 'view/dashboard'; + +customElements.define('view-dashboard', buildCustomElementConstructor(ViewDashboard)); diff --git a/packages/@best/agent-frontend/src/client/modules/component/agent/agent.css b/packages/@best/agent-frontend/src/client/modules/component/agent/agent.css new file mode 100644 index 00000000..e69de29b diff --git a/packages/@best/agent-frontend/src/client/modules/component/agent/agent.html b/packages/@best/agent-frontend/src/client/modules/component/agent/agent.html new file mode 100644 index 00000000..ece4bd25 --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/component/agent/agent.html @@ -0,0 +1,10 @@ + diff --git a/packages/@best/agent-frontend/src/client/modules/component/agent/agent.js b/packages/@best/agent-frontend/src/client/modules/component/agent/agent.js new file mode 100644 index 00000000..7d884469 --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/component/agent/agent.js @@ -0,0 +1,6 @@ +import { LightningElement, api } from 'lwc'; + +export default class ComponentAgent extends LightningElement { + @api jobs = []; + @api name = ''; +} diff --git a/packages/@best/agent-frontend/src/client/modules/component/job/job.css b/packages/@best/agent-frontend/src/client/modules/component/job/job.css new file mode 100644 index 00000000..5ca989eb --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/component/job/job.css @@ -0,0 +1,46 @@ +.job { + margin: 10px 0; +} + +.status { + border-radius: 4px 4px 0 0; + display: block; + padding: 4px 8px; + font-size: 80%; + margin-right: 0; + z-index: 1; +} + +.status.added, .status.queued { background: #cccccc; color: #252525; } +.status.running { background: #adcff0; color: #032546; } +.status.completed { background: #a0dfb5; color: #063b18; } +.status.cancelled { background: #ebcd8c; color: #614700; } +.status.error { background: #eb8c8c; color: #610000; } + +main { + display: grid; + grid-template-columns: auto 200px; + background: #eee; + padding: 12px; + border-radius: 0 0 4px 4px; +} + +.info { + display: flex; + flex-direction: row; + align-items: center; +} + +.info h2 { + display: inline-block; + margin: 0; + font-weight: normal; +} + +.stats { + text-align: right; +} + +.stats p { + margin: 0; +} \ No newline at end of file diff --git a/packages/@best/agent-frontend/src/client/modules/component/job/job.html b/packages/@best/agent-frontend/src/client/modules/component/job/job.html new file mode 100644 index 00000000..7157c574 --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/component/job/job.html @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/packages/@best/agent-frontend/src/client/modules/component/job/job.js b/packages/@best/agent-frontend/src/client/modules/component/job/job.js new file mode 100644 index 00000000..f4b4fb24 --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/component/job/job.js @@ -0,0 +1,33 @@ +import { LightningElement, api } from 'lwc'; + +export default class ComponentJob extends LightningElement { + @api job = {}; + + get statusClass() { + return 'status ' + this.job.status.toLowerCase(); + } + + get isRunning() { + return this.job.status === 'RUNNING'; + } + + get isCompleted() { + return this.job.status === 'COMPLETED'; + } + + get statsText() { + const { time, completedIterations } = this.job; + + return `N: ${completedIterations}, T: ${time}s`; + } + + get hasEstimate() { + return !!this.job.estimatedTime; + } + + get estimatedText() { + const { estimatedTime } = this.job; + + return `${estimatedTime}s`; + } +} diff --git a/packages/@best/agent-frontend/src/client/modules/store/model/model.js b/packages/@best/agent-frontend/src/client/modules/store/model/model.js new file mode 100644 index 00000000..0647b730 --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/store/model/model.js @@ -0,0 +1,55 @@ +export class Job { + agentId; + jobId; + name; + status = 'ADDED'; + + constructor(jobId, name) { + this.jobId = jobId; + this.name = name; + } + + get progressValue() { + if (this._time && this._estimatedTime) { + return (this._time / this.estimatedTime) * 100; + } + + return 0; + } + + _estimatedTime; + get estimatedTime() { + return this._estimatedTime; + } + + _completedIterations; + get completedIterations() { + return this._completedIterations; + } + + _time; + get time() { + return this._time; + } + + update({ state, opts }) { + const { executedIterations, executedTime } = state; + const { iterations, maxDuration } = opts; + const avgIteration = executedTime / executedIterations; + const runtime = parseInt((executedTime / 1000) + '', 10); + const estimated = iterations ? Math.round(iterations * avgIteration / 1000) + 1 : maxDuration / 1000; + + this._time = runtime; + this._estimatedTime = estimated; + } + + results(count) { + this.status = 'COMPLETED'; + this._completedIterations = count; + } + + objectified() { + const { agentId, jobId, name, status, progressValue, estimatedTime, completedIterations, time } = this; + return { agentId, jobId, name, status, progressValue, estimatedTime, completedIterations, time }; + } +} \ No newline at end of file diff --git a/packages/@best/agent-frontend/src/client/modules/store/redux/redux.js b/packages/@best/agent-frontend/src/client/modules/store/redux/redux.js new file mode 100644 index 00000000..e69de29b diff --git a/packages/@best/agent-frontend/src/client/modules/store/socket/socket.js b/packages/@best/agent-frontend/src/client/modules/store/socket/socket.js new file mode 100644 index 00000000..eb9f002a --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/store/socket/socket.js @@ -0,0 +1,5 @@ +import io from 'socket.io-client'; + +export const connect = (...args) => { + return io.connect(...args); +} \ No newline at end of file diff --git a/packages/@best/agent-frontend/src/client/modules/view/dashboard/dashboard.css b/packages/@best/agent-frontend/src/client/modules/view/dashboard/dashboard.css new file mode 100644 index 00000000..28f68e3a --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/view/dashboard/dashboard.css @@ -0,0 +1,15 @@ +.container { + width: 800px; + margin: 40px auto; +} + +header { + box-sizing: border-box; +} + +.no-jobs { + text-align: center; + background: rgb(255, 249, 199); + border-radius: 4px; + padding: 8px 0; +} \ No newline at end of file diff --git a/packages/@best/agent-frontend/src/client/modules/view/dashboard/dashboard.html b/packages/@best/agent-frontend/src/client/modules/view/dashboard/dashboard.html new file mode 100644 index 00000000..49681f42 --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/view/dashboard/dashboard.html @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/packages/@best/agent-frontend/src/client/modules/view/dashboard/dashboard.js b/packages/@best/agent-frontend/src/client/modules/view/dashboard/dashboard.js new file mode 100644 index 00000000..3a6dbaa6 --- /dev/null +++ b/packages/@best/agent-frontend/src/client/modules/view/dashboard/dashboard.js @@ -0,0 +1,149 @@ +import { LightningElement, track } from 'lwc'; + +import { connect } from 'store/socket'; +import { Job } from 'store/model'; + +export default class ViewDashboard extends LightningElement { + config = { host: 'http://localhost:5555', path: '/hub', name: 'Hub 5555' }; + + @track agents = []; + allJobs = []; + + connectedCallback() { + const socket = connect(this.config.host, { path: this.config.path, query: { frontend: true } }); + + socket.on('connect', this.socketConnect.bind(this)) + socket.on('disconnect', this.socketDisconnect.bind(this)) + socket.on('error', this.socketError.bind(this)) + + socket.on('benchmark added', this.added.bind(this)); + socket.on('benchmark queued', this.queued.bind(this)); + socket.on('benchmark start', this.start.bind(this)); + socket.on('benchmark update', this.update.bind(this)); + socket.on('benchmark error', this.error.bind(this)); + socket.on('benchmark cancel', this.cancel.bind(this)); + socket.on('benchmark results', this.results.bind(this)); + } + + // GETTERS + + get hasJobs() { + return this.allJobs.length > 0; + } + + // HELPERS + + addAgent(agentId) { + this.agents.push({ agentId, jobs: [] }) + } + + updateAgentsJob(updatedJob) { + if (! updatedJob.agentId) { return; } + let agentIndex = this.agents.findIndex(a => a.agentId === updatedJob.agentId); + if (agentIndex === -1) { + this.addAgent(updatedJob.agentId); + agentIndex = this.agents.length - 1; + } + + const agent = this.agents[agentIndex]; + const jobIndex = agent.jobs.findIndex(j => j.jobId === updatedJob.jobId); + if (jobIndex === -1) { + agent.jobs.unshift(updatedJob.objectified()); + } else { + agent.jobs[jobIndex] = updatedJob.objectified(); + } + } + + switchAgentForJob(oldAgentId, updatedJob) { + let oldAgent = this.agents.find(a => a.agentId === oldAgentId); + const oldJobIndex = oldAgent.jobs.findIndex(j => j.jobId === updatedJob.jobId); + oldAgent.jobs.splice(oldJobIndex, 1); + + this.updateAgentsJob(updatedJob); + } + + // SOCKET + + socketConnect() { + // console.log('[connect]') + } + + socketDisconnect() { + // console.log('[disconnect]', event) + } + + socketError() { + // console.log('[error]', event) + } + + // BENCHMARK + + added(event) { + const job = new Job(event.jobId, event.packet.benchmarkName); + job.agentId = event.agentId; + this.allJobs.push(job); + this.updateAgentsJob(job); + } + + queued(event) { + const index = this.allJobs.findIndex(j => j.jobId === event.jobId); + const job = this.allJobs[index]; + job.status = 'QUEUED'; + + if (event.agentId !== job.agentId) { + const oldAgentId = job.agentId; + job.agentId = event.agentId; + this.switchAgentForJob(oldAgentId, job); + } else { + job.agentId = event.agentId; + this.updateAgentsJob(job); + } + } + + start(event) { + const index = this.allJobs.findIndex(j => j.jobId === event.jobId); + const job = this.allJobs[index]; + job.status = 'RUNNING'; + + if (event.agentId !== job.agentId) { + const oldAgentId = job.agentId; + job.agentId = event.agentId; + this.switchAgentForJob(oldAgentId, job); + } else { + job.agentId = event.agentId; + this.updateAgentsJob(job); + } + } + + update(event) { + const index = this.allJobs.findIndex(j => j.jobId === event.jobId); + const job = this.allJobs[index]; + job.update(event.packet); + + this.updateAgentsJob(job); + } + + results(event) { + const index = this.allJobs.findIndex(j => j.jobId === event.jobId); + const job = this.allJobs[index]; + job.results(event.packet.resultCount); + + this.updateAgentsJob(job); + } + + error(event) { + const index = this.allJobs.findIndex(j => j.jobId === event.jobId); + const job = this.allJobs[index]; + job.status = 'ERROR'; + + this.updateAgentsJob(job); + } + + cancel(event) { + const index = this.allJobs.findIndex(j => j.jobId === event.jobId); + const job = this.allJobs[index]; + job.status = 'CANCELLED'; + + this.updateAgentsJob(job); + } +} diff --git a/packages/@best/agent-frontend/src/client/resources/favicon.ico b/packages/@best/agent-frontend/src/client/resources/favicon.ico new file mode 100644 index 00000000..83e9b6cd Binary files /dev/null and b/packages/@best/agent-frontend/src/client/resources/favicon.ico differ diff --git a/packages/@best/agent-frontend/src/client/resources/lwc.png b/packages/@best/agent-frontend/src/client/resources/lwc.png new file mode 100644 index 00000000..88dce002 Binary files /dev/null and b/packages/@best/agent-frontend/src/client/resources/lwc.png differ diff --git a/packages/@best/agent-frontend/src/server/index.ts b/packages/@best/agent-frontend/src/server/index.ts new file mode 100644 index 00000000..3e708942 --- /dev/null +++ b/packages/@best/agent-frontend/src/server/index.ts @@ -0,0 +1,22 @@ +import path from 'path'; +import express from 'express'; +import socketIO from 'socket.io'; +import Manager from './manager'; +import AgentLogger from '@best/agent-logger'; + +export const serveFrontend = (app: express.Application) => { + const DIST_DIR = path.resolve(__dirname, '../dist'); + + app.use(express.static(DIST_DIR)); + app.get('*', (req, res) => res.sendFile(path.resolve(DIST_DIR, 'index.html'))); +} + +export const attachMiddleware = (server: socketIO.Server, logger: AgentLogger) => { + const manager = new Manager(logger); + + server.on('connect', (socket: SocketIO.Socket) => { + if (socket.handshake.query && socket.handshake.query.frontend) { + manager.addFrontend(socket); + } + }); +} diff --git a/packages/@best/agent-frontend/src/server/manager.ts b/packages/@best/agent-frontend/src/server/manager.ts new file mode 100644 index 00000000..895210ad --- /dev/null +++ b/packages/@best/agent-frontend/src/server/manager.ts @@ -0,0 +1,49 @@ +import socketIO from 'socket.io'; +import AgentLogger from '@best/agent-logger'; + +// const proxifyWithAfter = (object: any, method: string, fn: Function) => { +// const orig = object[method] +// object[method] = function (...args: any[]) { +// fn.apply(this, args) +// return orig.apply(this, args) +// } +// } + +const FRONTEND_EVENTS = ['benchmark added', 'benchmark start', 'benchmark update', 'benchmark end', 'benchmark error', 'benchmark results', 'benchmark queued', 'benchmark cancel'] + +export default class Manager { + private frontends: socketIO.Socket[] = []; + private logger: AgentLogger; + + constructor(logger: AgentLogger) { + this.logger = logger; + this.attachListeners(); + } + + addFrontend(socket: socketIO.Socket) { + const index = this.frontends.length; + this.frontends.push(socket); + + socket.on('disconnect', () => { + this.frontends.splice(index, 1); + }) + } + + private attachListeners() { + // proxifyWithAfter(client, 'emit', (name: string, packet: any) => { + // this.notifyFrontends(client.id, name, packet); + // }) + + FRONTEND_EVENTS.forEach(e => { + this.logger.on(e, (packet: any) => { + this.notifyFrontends(e, packet); + }) + }) + } + + private notifyFrontends(name: string, packet: any) { + this.frontends.forEach(frontend => { + frontend.emit(name, packet) + }) + } +} diff --git a/packages/@best/agent-frontend/tsconfig.json b/packages/@best/agent-frontend/tsconfig.json new file mode 100644 index 00000000..445852b7 --- /dev/null +++ b/packages/@best/agent-frontend/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "src/server", + "outDir": "build" + }, + "references": [ + { "path": "../agent-logger" } + ] + } + \ No newline at end of file diff --git a/packages/@best/agent-hub/package.json b/packages/@best/agent-hub/package.json index 93d9435f..e90db288 100644 --- a/packages/@best/agent-hub/package.json +++ b/packages/@best/agent-hub/package.json @@ -9,6 +9,7 @@ ], "main": "build/cli/index.js", "dependencies": { + "@best/agent-logger": "4.0.0", "@best/runner-remote": "4.0.0", "@best/utils": "4.0.0", "jsonwebtoken": "^8.5.1", diff --git a/packages/@best/agent-hub/src/Agent.ts b/packages/@best/agent-hub/src/Agent.ts index d9b0b552..9729067c 100644 --- a/packages/@best/agent-hub/src/Agent.ts +++ b/packages/@best/agent-hub/src/Agent.ts @@ -5,6 +5,9 @@ import socketIO from "socket.io-client"; import SocketIOFile from "@best/runner-remote/build/file-uploader"; import { BenchmarkResultsSnapshot, BenchmarkResultsState, BenchmarkRuntimeConfig } from "@best/types"; import { loadBenchmarkJob } from "./benchmark-loader"; +import AgentLogger, { loggedSocket } from '@best/agent-logger'; + +const AGENT_CONNECTION_ERROR = 'Agent running job became offline.'; export interface Spec { browser: string, @@ -29,13 +32,16 @@ export enum AgentStatus { RunningJob, Offline } + export class Agent extends EventEmitter { private _status: AgentStatus = AgentStatus.Idle; private _config: AgentConfig; + private _logger: AgentLogger; - constructor(config: AgentConfig) { + constructor(config: AgentConfig, logger: AgentLogger) { super(); this._config = config; + this._logger = logger.withAgentId(this.host); } get status() { @@ -68,6 +74,7 @@ export class Agent extends EventEmitter { await loadBenchmarkJob(job); } catch (err) { console.log('Error while uploading file to the hub', err); + this._logger.event(job.socketConnection.id, 'benchmark error', err, false); job.socketConnection.emit('benchmark_error', err); this.status = AgentStatus.Idle; return ; @@ -79,9 +86,14 @@ export class Agent extends EventEmitter { // @todo: move to success queue this.status = AgentStatus.Idle; }) - .catch(() => { + .catch((err) => { // @todo: move to failures queue - this.status = AgentStatus.Idle; + if (err.message === AGENT_CONNECTION_ERROR) { + this.status = AgentStatus.Offline; + // TODO: in this case, we need to re-run the job on a different agent (if we have one) + } else { + this.status = AgentStatus.Idle; + } }); } @@ -115,20 +127,28 @@ export class Agent extends EventEmitter { return new Promise(async (resolve, reject) => { const socket = socketIO(self._config.host, self._config.options); + // const socket = loggedSocket(, this._logger); + const jobSocket = loggedSocket(job.socketConnection, this._logger); let resolved: boolean = false; job.socketConnection.on('disconnect', () => { + if (!resolved) { // it is not yet resolved then the job has been cancelled + this._logger.event(job.socketConnection.id, 'benchmark cancel'); + } + resolved = true; socket.disconnect(); reject(new Error('Connection terminated')); }); - socket.on('connect_error', function() { + socket.on('connect_error', () => { console.log("Connection error with hub: ", [self._config.host, self._config.options]); self.status = AgentStatus.Offline; // this is a special case that we need to handle with care, right now the job is scheduled to run in this hub // which is offline, but, is not the job fault, it can run in another agent. Note: can be solved if we add a new queue, and retry in another queue. - reject(new Error('Agent running job became offline.')); + socket.disconnect(); + jobSocket.emit('benchmark_error', 'Error connecting to agent'); + reject(new Error(AGENT_CONNECTION_ERROR)); // @todo: add a retry logic? }); @@ -144,19 +164,20 @@ export class Agent extends EventEmitter { }); }); - socket.on('running_benchmark_start', () => { - job.socketConnection.emit('running_benchmark_start'); + socket.on('running_benchmark_start', ({ entry }: { entry: string }) => { + jobSocket.emit('running_benchmark_start', { entry }); }); socket.on('running_benchmark_update', ({ state, opts }: { state: BenchmarkResultsState, opts: BenchmarkRuntimeConfig }) => { - job.socketConnection.emit('running_benchmark_update', { state, opts }); + jobSocket.emit('running_benchmark_update', { state, opts }); }); - socket.on('running_benchmark_end', () => { - job.socketConnection.emit('running_benchmark_end'); + + socket.on('running_benchmark_end', ({ entry }: { entry: string }) => { + jobSocket.emit('running_benchmark_end', { entry }); }); socket.on('benchmark_enqueued', ({ pending }: { pending: number }) => { - job.socketConnection.emit('benchmark_enqueued', { pending }); + jobSocket.emit('benchmark_enqueued', { pending }); }); // @todo: this should put the runner in a weird state and dont allow any new job until we can confirm the connection is valid. @@ -164,29 +185,30 @@ export class Agent extends EventEmitter { if (!resolved) { resolved = true; const err = new Error(reason); - job.socketConnection.emit('benchmark_error', reason); + jobSocket.emit('benchmark_error', reason); reject(err); } }); socket.on('error', (err: any) => { resolved = true; - job.socketConnection.emit('benchmark_error', err instanceof Error ? err.message : err); + const reason = err instanceof Error ? err.message : err + jobSocket.emit('benchmark_error', reason); socket.disconnect(); reject(err); }); socket.on('benchmark_error', (err: any) => { resolved = true; - console.log(err); - job.socketConnection.emit('benchmark_error', err); + this._logger.error(job.socketConnection.id, 'benchmark_error', err); + jobSocket.emit('benchmark_error', err); socket.disconnect(); reject(new Error('Benchmark couldn\'t finish running. ')); }); socket.on('benchmark_results', (result: BenchmarkResultsSnapshot) => { resolved = true; - job.socketConnection.emit('benchmark_results', result); + jobSocket.emit('benchmark_results', result); socket.disconnect(); resolve(result); }); diff --git a/packages/@best/agent-hub/src/AgentManager.ts b/packages/@best/agent-hub/src/AgentManager.ts index 7a1e5c4c..df381ff1 100644 --- a/packages/@best/agent-hub/src/AgentManager.ts +++ b/packages/@best/agent-hub/src/AgentManager.ts @@ -1,6 +1,7 @@ import { EventEmitter } from "events"; import {Agent, AgentConfig, AgentStatus, Spec} from "./Agent"; import BenchmarkJob from "./BenchmarkJob"; +import AgentLogger from "@best/agent-logger"; export class AgentManager extends EventEmitter { private agents: Agent[] = []; @@ -54,8 +55,8 @@ export class AgentManager extends EventEmitter { } } -export function createAgentManager(agentsConfig: AgentConfig[]): AgentManager { - const agents: Agent[] = agentsConfig.map((agentConfig: AgentConfig) => new Agent(agentConfig)); +export function createAgentManager(agentsConfig: AgentConfig[], logger: AgentLogger): AgentManager { + const agents: Agent[] = agentsConfig.map((agentConfig: AgentConfig) => new Agent(agentConfig, logger)); return new AgentManager(agents); } diff --git a/packages/@best/agent-hub/src/HubApplication.ts b/packages/@best/agent-hub/src/HubApplication.ts index 96ccc1a2..20658b5c 100644 --- a/packages/@best/agent-hub/src/HubApplication.ts +++ b/packages/@best/agent-hub/src/HubApplication.ts @@ -4,19 +4,22 @@ import BenchmarkJob from "./BenchmarkJob"; import { AgentManager } from "./AgentManager"; import {Agent, Spec} from "./Agent"; import {Client} from "./Client"; +import AgentLogger from "@best/agent-logger"; export class HubApplication { private _incomingQueue: ObservableQueue; private _agentManager: AgentManager; + private _logger: AgentLogger; private assignedAgents: WeakMap = new WeakMap(); private clientAgents: WeakMap = new WeakMap(); private pendingClients: Client[] = []; private runningClients: Client[] = []; - constructor(incomingQueue: ObservableQueue, agentManager: AgentManager) { + constructor(incomingQueue: ObservableQueue, agentManager: AgentManager, logger: AgentLogger) { this._incomingQueue = incomingQueue; this._agentManager = agentManager; + this._logger = logger; this.attachEventListeners(); } @@ -25,6 +28,7 @@ export class HubApplication { // @todo: define the types for the data. // @todo: add timeout on waiting the benchmark task? socket.on('benchmark_task', (data: any) => { + this._logger.event(socket.id, 'benchmark added', { benchmarkName: data.benchmarkName }) const job = new BenchmarkJob({ ...data, socket @@ -104,6 +108,7 @@ export class HubApplication { agent.runJob(job); } else { job.socketConnection.emit('benchmark_enqueued', { pending: this._incomingQueue.size }); + this._logger.event(job.socketConnection.id, 'benchmark queued', { pending: this._incomingQueue.size }, false); } }; diff --git a/packages/@best/agent-hub/src/agents-api.ts b/packages/@best/agent-hub/src/agents-api.ts index ba23e9c8..2c018dc3 100644 --- a/packages/@best/agent-hub/src/agents-api.ts +++ b/packages/@best/agent-hub/src/agents-api.ts @@ -2,6 +2,7 @@ import {Application, NextFunction, Request, Response} from "express"; import * as jwt from "jsonwebtoken"; import {AgentManager} from "./AgentManager"; import {Agent, AgentStatus} from "./Agent"; +import AgentLogger from "@best/agent-logger"; function authenticateAgentApi(tokenSecret: string, req: Request, res: Response, next: NextFunction) { if (req.path.startsWith('/api/v1')) { @@ -25,7 +26,7 @@ function authenticateAgentApi(tokenSecret: string, req: Request, res: Response, } } -function addAgentToHub(agentManager: AgentManager, req: Request, res: Response) { +function addAgentToHub(agentManager: AgentManager, logger: AgentLogger, req: Request, res: Response) { // @todo: validate payload. const agentConfig = { host: req.body.host, @@ -35,14 +36,14 @@ function addAgentToHub(agentManager: AgentManager, req: Request, res: Response) remoteRunnerConfig: req.body.remoteRunnerConfig }; - const agent = new Agent(agentConfig); + const agent = new Agent(agentConfig, logger); agent.status = AgentStatus.Offline; agentManager.addAgent(agent); agent.status = AgentStatus.Idle; - console.log('Added agent with host and spec: ', agentConfig.host, JSON.stringify(agentConfig.spec)); + logger.info('', 'agent added', [agentConfig.host, agentConfig.spec]); return res.status(201).send({ success: 'true', @@ -63,9 +64,9 @@ function pingByAgent(agentManager: AgentManager, req: Request, res: Response) { }); } -export function configureAgentsApi(app: Application, agentManager: AgentManager, tokenSecret: string) { +export function configureAgentsApi(app: Application, agentManager: AgentManager, logger: AgentLogger, tokenSecret: string) { app.use(authenticateAgentApi.bind(null, tokenSecret)); - app.post('/api/v1/agents', addAgentToHub.bind(null, agentManager)); + app.post('/api/v1/agents', addAgentToHub.bind(null, agentManager, logger)); app.get('/api/v1/agent-ping', pingByAgent.bind(null, agentManager)); } diff --git a/packages/@best/agent-hub/src/hub-server.ts b/packages/@best/agent-hub/src/hub-server.ts index b58de943..a63c1d41 100644 --- a/packages/@best/agent-hub/src/hub-server.ts +++ b/packages/@best/agent-hub/src/hub-server.ts @@ -7,29 +7,35 @@ import { createAgentManager } from "./AgentManager"; import { HubApplication } from "./HubApplication"; import { AgentConfig } from "./Agent"; import { configureAgentsApi } from "./agents-api"; +import { attachMiddleware } from '@best/agent-frontend'; +import AgentLogger from '@best/agent-logger'; export interface HubConfig { tokenSecret: string, agents: AgentConfig[], } -function createHubApplication(config: HubConfig): HubApplication { +function createHubApplication(config: HubConfig, logger: AgentLogger): HubApplication { const incomingQueue = new ObservableQueue(); - const agentsManager = createAgentManager(config.agents); + const agentsManager = createAgentManager(config.agents, logger); - return new HubApplication(incomingQueue, agentsManager); + return new HubApplication(incomingQueue, agentsManager, logger); } export function runHub(server: any, app: Application, hubConfig: HubConfig) { const socketServer: SocketIO.Server = socketIO(server, { path: '/hub' }); - const hub: HubApplication = createHubApplication(hubConfig); + const logger = new AgentLogger(); + const hub: HubApplication = createHubApplication(hubConfig, logger); - configureAgentsApi(app, hub.agentManager, hubConfig.tokenSecret); + configureAgentsApi(app, hub.agentManager, logger, hubConfig.tokenSecret); // Authentication middleware socketServer.use((socket, next) => { const token = socket.handshake.query.token || ''; + // TODO: add authentication specifically for frontend + if (socket.handshake.query.frontend) return next(); + jwt.verify(token, hubConfig.tokenSecret, (err: Error, payload: any) => { if (err) { return next(new Error('authentication error: ' + err.message)); @@ -42,8 +48,10 @@ export function runHub(server: any, app: Application, hubConfig: HubConfig) { }); socketServer.on('connect', (socket) => { - hub.handleIncomingSocketConnection(socket) + if (!socket.handshake.query.frontend) hub.handleIncomingSocketConnection(socket); }); + + attachMiddleware(socketServer, logger); } export default { runHub }; diff --git a/packages/@best/agent-hub/tsconfig.json b/packages/@best/agent-hub/tsconfig.json index 15133e09..1e28aec3 100644 --- a/packages/@best/agent-hub/tsconfig.json +++ b/packages/@best/agent-hub/tsconfig.json @@ -5,6 +5,7 @@ "outDir": "build" }, "references": [ + { "path": "../agent-logger" }, { "path": "../runner-remote" }, { "path": "../utils" }, { "path": "../types" } diff --git a/packages/@best/agent-logger/jest.config.js b/packages/@best/agent-logger/jest.config.js new file mode 100644 index 00000000..488232d7 --- /dev/null +++ b/packages/@best/agent-logger/jest.config.js @@ -0,0 +1,7 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const COMMON = require('../../../scripts/jest/common.config') + +module.exports = { + ...COMMON, + displayName: 'agent-logger', +} diff --git a/packages/@best/agent-logger/package.json b/packages/@best/agent-logger/package.json new file mode 100644 index 00000000..c09ed094 --- /dev/null +++ b/packages/@best/agent-logger/package.json @@ -0,0 +1,12 @@ +{ + "name": "@best/agent-logger", + "version": "4.0.0", + "description": "Best Agent Logger", + "main": "build/index.js", + "dependencies": { + "chalk": "^2.4.2", + "memoizee": "0.4.14", + "@types/memoizee": "0.4.2" + } + } + \ No newline at end of file diff --git a/packages/@best/agent-logger/src/index.ts b/packages/@best/agent-logger/src/index.ts new file mode 100644 index 00000000..155cc3da --- /dev/null +++ b/packages/@best/agent-logger/src/index.ts @@ -0,0 +1,84 @@ +import { EventEmitter } from 'events'; +import chalk from 'chalk'; +export { loggedSocket, LoggedSocket } from './socket'; + +const THROTTLE_WAIT = 750; + +export default class AgentLogger extends EventEmitter { + private agentId: string; + private outputStream: NodeJS.WriteStream; + private parent?: AgentLogger + + private pendingEvents = new Set(); + + constructor(agentId?: string, outputStream: NodeJS.WriteStream = process.stdout) { + super(); + this.agentId = agentId || 'Hub'; + this.outputStream = outputStream; + } + + withAgentId(agentId: string): AgentLogger { + const child = new AgentLogger(agentId, this.outputStream); + child.parent = this; + return child; + } + + // This function logs the event to the console, but does not emit the event + error(jobId: string, name: string, packet?: any, logPacket: boolean = true) { + this.log(jobId, chalk.bold.red(name), (packet && logPacket) ? JSON.stringify(packet) : null); + } + + // This function logs the event to the console, but does not emit the event + info(jobId: string, name: string, packet?: any, logPacket: boolean = true) { + this.log(jobId, chalk.bold.cyan(name), (packet && logPacket) ? JSON.stringify(packet) : null); + } + + // This function emits the event to all listeners of the logger + // Optionally, if `log` true it will log the event as well. + event(jobId: string, name: string, packet?: any, logPacket: boolean = true) { + this.emitEvent(name, { + agentId: this.agentId, + jobId, + packet + }) + + this.log(jobId, chalk.bold.green(name), (packet && logPacket) ? JSON.stringify(packet) : null) + } + + // This function is similar to `event()` with two exceptions: + // 1. It throttle emitting the event to listeners + // 2. It will always log events, which is also throttled + // NOTE: it only throttles events with the same jobId and name + throttle(jobId: string, name: string, packet?: any, logPacket?: boolean) { + const cacheKey = `${jobId},${name}`; + + if (!this.pendingEvents.has(cacheKey)) { + this.pendingEvents.add(cacheKey); + this.event(jobId, name, packet, logPacket); + + setTimeout(() => { + this.pendingEvents.delete(cacheKey); + }, THROTTLE_WAIT); + } + } + + private emitEvent(event: string, packet: any) { + this.emit(event, packet); + + if (this.parent) this.parent.emitEvent(event, packet); + } + + private log(jobId: string, name: string, packet?: any) { + let message = `${chalk.gray(this.agentId)} Job[${chalk.bold(jobId)}]: ${name}`; + + if (packet) { + message += ` - ${packet}`; + } + + this.write(message); + } + + private write(message: string) { + this.outputStream.write(message + '\n'); + } +} \ No newline at end of file diff --git a/packages/@best/agent-logger/src/socket.ts b/packages/@best/agent-logger/src/socket.ts new file mode 100644 index 00000000..ad5d70fb --- /dev/null +++ b/packages/@best/agent-logger/src/socket.ts @@ -0,0 +1,31 @@ +import AgentLogger from './index'; +import { sanitize } from './utils/sanitize'; + +export interface LoggedSocket { + rawSocket: SocketIO.Socket; + on(event: string, listener: (...args: any[]) => void): void; + once(event: string, listener: (...args: any[]) => void): void; + emit(event: string, ...args: any[]): boolean; + disconnect( close?: boolean ): SocketIO.Socket; +} + +export const loggedSocket = (rawSocket: SocketIO.Socket, logger: AgentLogger): LoggedSocket => { + return { + rawSocket, + emit(name: string, ...args: any[]): boolean { + const event = sanitize(name, args); + logger.throttle(rawSocket.id, event.name, event.packet, false); + + return rawSocket.emit.apply(rawSocket, [name, ...args]); + }, + on(event: string, listener: (...args: any[]) => void): void { + rawSocket.on.apply(rawSocket, [event, listener]); + }, + once(event: string, listener: (...args: any[]) => void): void { + rawSocket.once.apply(rawSocket, [event, listener]); + }, + disconnect(close?: boolean): SocketIO.Socket { + return rawSocket.disconnect.apply(rawSocket); + } + } +} diff --git a/packages/@best/agent-logger/src/utils/sanitize.ts b/packages/@best/agent-logger/src/utils/sanitize.ts new file mode 100644 index 00000000..2d7c9d60 --- /dev/null +++ b/packages/@best/agent-logger/src/utils/sanitize.ts @@ -0,0 +1,30 @@ +export interface SanitizedEvent { + name: string; + packet: any; +} + +const normalizePacket = (name: string, rawPacket: any): any => { + let packet: any; + if (rawPacket instanceof Array && rawPacket.length === 1) { + packet = rawPacket[0]; + } + + switch (name) { + case 'benchmark results': + return { + resultCount: packet.results.length + } + default: + return packet; + } +} + +export const sanitize = (rawEvent: string, packet: any): SanitizedEvent => { + const eventName = rawEvent.replace('running_', '').replace('_', ' '); + const cleaned = normalizePacket(eventName, packet); + + return { + name: eventName, + packet: cleaned + } +} \ No newline at end of file diff --git a/packages/@best/agent-logger/tsconfig.json b/packages/@best/agent-logger/tsconfig.json new file mode 100644 index 00000000..aa688247 --- /dev/null +++ b/packages/@best/agent-logger/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.settings.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "build", + "experimentalDecorators": true + }, + "references": [ + ] + } + \ No newline at end of file diff --git a/packages/@best/agent/package.json b/packages/@best/agent/package.json index ef669ae9..f1cc9a84 100644 --- a/packages/@best/agent/package.json +++ b/packages/@best/agent/package.json @@ -8,6 +8,8 @@ ], "main": "build/cli/index.js", "dependencies": { + "@best/agent-frontend": "4.0.0", + "@best/agent-logger": "4.0.0", "@best/runner": "4.0.0", "@best/utils": "4.0.0", "axios": "~0.19.0", diff --git a/packages/@best/agent/src/AgentApp.ts b/packages/@best/agent/src/AgentApp.ts index 17555dee..be05be88 100644 --- a/packages/@best/agent/src/AgentApp.ts +++ b/packages/@best/agent/src/AgentApp.ts @@ -3,14 +3,17 @@ import ObservableQueue from "./utils/ObservableQueue"; import BenchmarkRunner, { RunnerStatus } from "./BenchmarkRunner"; import BenchmarkTask from "./BenchmarkTask"; import { BuildConfig } from "@best/types"; +import AgentLogger from '@best/agent-logger'; export class AgentApp { private queue: ObservableQueue; private runner: BenchmarkRunner; + private logger: AgentLogger; - constructor(queue: ObservableQueue, runner: BenchmarkRunner) { + constructor(queue: ObservableQueue, runner: BenchmarkRunner, logger: AgentLogger) { this.queue = queue; this.runner = runner; + this.logger = logger; this.initializeHandlers(); } @@ -23,6 +26,7 @@ export class AgentApp { handleIncomingConnection(socket: SocketIO.Socket) { socket.on('benchmark_task', (data: BuildConfig) => { const task = new BenchmarkTask(data, socket); + this.logger.event(socket.id, 'benchmark added', { benchmarkName: data.benchmarkName }, false); socket.on('disconnect', () => { this.queue.remove(task); @@ -39,6 +43,7 @@ export class AgentApp { this.runner.run(task); } else { task.socketConnection.emit('benchmark_enqueued', { pending: this.queue.size }); + this.logger.event(task.socketConnection.id, 'benchmark queued', { pending: this.queue.size }); } } diff --git a/packages/@best/agent/src/BenchmarkRunner.ts b/packages/@best/agent/src/BenchmarkRunner.ts index d7830d52..d5009577 100644 --- a/packages/@best/agent/src/BenchmarkRunner.ts +++ b/packages/@best/agent/src/BenchmarkRunner.ts @@ -4,8 +4,8 @@ import { runBenchmark } from '@best/runner'; import BenchmarkTask from "./BenchmarkTask"; import { loadBenchmarkJob } from "./benchmark-loader"; import { x as extractTar } from 'tar'; -import * as SocketIO from "socket.io"; import { RunnerOutputStream } from "@best/console-stream"; +import AgentLogger, { loggedSocket, LoggedSocket } from '@best/agent-logger'; import { BenchmarkResultsSnapshot, BenchmarkResultsState, @@ -21,30 +21,28 @@ export enum RunnerStatus { } // @todo: make a Runner Stream, and add an interface type instead of the class. -function initializeForwarder(socket: SocketIO.Socket, logger: Function): RunnerOutputStream { +function initializeForwarder(socket: LoggedSocket): RunnerOutputStream { return { init() {}, finish() {}, onBenchmarkStart(benchmarkPath: string) { - if (socket.connected) { - logger(`STATUS: running_benchmark ${benchmarkPath}`); - socket.emit('running_benchmark_start', benchmarkPath); + if (socket.rawSocket.connected) { + socket.emit('running_benchmark_start', { entry: benchmarkPath }); } }, onBenchmarkEnd(benchmarkPath: string) { - if (socket.connected) { - logger(`STATUS: finished_benchmark ${benchmarkPath}`); - socket.emit('running_benchmark_end', benchmarkPath); + if (socket.rawSocket.connected) { + socket.emit('running_benchmark_end', { entry: benchmarkPath }); } }, onBenchmarkError(benchmarkPath: string) { - if (socket.connected) { - socket.emit('running_benchmark_error', benchmarkPath); + if (socket.rawSocket.connected) { + socket.emit('running_benchmark_error', { entry: benchmarkPath }); } }, updateBenchmarkProgress(state: BenchmarkResultsState, opts: BenchmarkRuntimeConfig) { - if (socket.connected) { - socket.emit('running_benchmark_update', {state, opts}); + if (socket.rawSocket.connected) { + socket.emit('running_benchmark_update', { state, opts }); } }, } as RunnerOutputStream; @@ -67,7 +65,13 @@ export default class BenchmarkRunner extends EventEmitter { public runningTask: BenchmarkTask | null = null; public runningWasCancelled = false; private cancelledTimeout: any = null; - private _log: Function = () => {}; + + private logger: AgentLogger; + + constructor(logger: AgentLogger) { + super(); + this.logger = logger; + } get status() { return this._status; @@ -84,7 +88,7 @@ export default class BenchmarkRunner extends EventEmitter { cancelRun(task: BenchmarkTask) { if (this.runningTask === task) { - this._log('Running was cancelled.'); + this.logger.event(task.socketConnection.id, 'benchmark_cancel'); this.runningWasCancelled = true; this.cancelledTimeout = setTimeout(() => { this.status = RunnerStatus.IDLE; @@ -100,14 +104,9 @@ export default class BenchmarkRunner extends EventEmitter { this.status = RunnerStatus.RUNNING; this.runningWasCancelled = false; this.runningTask = task; - this._log = (msg: string) => { - if (!this.runningWasCancelled) { - process.stdout.write(`Task[${task.socketConnection.id}] - ${msg}\n`); - } - }; // @todo: just to be safe, add timeout in cancel so it waits for the runner to finish or dismiss the run assuming something went wrong - loadBenchmarkJob(task.socketConnection) + loadBenchmarkJob(task.socketConnection, this.logger) .then(extractBenchmarkTarFile(task)) .then(() => this.runBenchmark(task)) .then(({ error, results }: {error: any, results: any}) => { @@ -120,20 +119,20 @@ export default class BenchmarkRunner extends EventEmitter { private async runBenchmark(task: BenchmarkTask) { const { benchmarkName } = task; - const messenger = initializeForwarder(task.socketConnection, this._log); + const taskSocket = loggedSocket(task.socketConnection, this.logger); + const messenger = initializeForwarder(taskSocket); let results; let error; try { - this._log(`Running benchmark ${benchmarkName}`); + this.logger.info(task.socketConnection.id, 'benchmark start', benchmarkName); results = await runBenchmark(task.config, messenger); - this._log(`Benchmark ${benchmarkName} completed successfully`); + this.logger.info(task.socketConnection.id, 'benchmark completed', benchmarkName); } catch (err) { - this._log(`Something went wrong while running ${benchmarkName}`); - process.stderr.write(err + '\n'); + this.logger.error(task.socketConnection.id, 'benchmark error', err); error = err; } @@ -142,14 +141,13 @@ export default class BenchmarkRunner extends EventEmitter { private afterRunBenchmark(err: any, results: BenchmarkResultsSnapshot | null) { if (!this.runningWasCancelled) { - this._log(`Sending results to client`); + this.logger.info(this.runningTask!.socketConnection.id, 'sending results'); if (err) { - this._log(`Sending error`); this.runningTask!.socketConnection.emit('benchmark_error', err.toString()); } else { - this._log(`Sending results`); this.runningTask!.socketConnection.emit('benchmark_results', results); + this.logger.event(this.runningTask!.socketConnection.id, 'benchmark results', { resultCount: results!.results.length }, false); } } diff --git a/packages/@best/agent/src/__tests__/AgentApp.spec.ts b/packages/@best/agent/src/__tests__/AgentApp.spec.ts index 8f99b907..f1aed033 100644 --- a/packages/@best/agent/src/__tests__/AgentApp.spec.ts +++ b/packages/@best/agent/src/__tests__/AgentApp.spec.ts @@ -5,6 +5,7 @@ import BenchmarkRunner, {RunnerStatus} from "../BenchmarkRunner"; import * as SocketIO from "socket.io"; import { EventEmitter } from "events"; import { FrozenGlobalConfig, FrozenProjectConfig } from "@best/types"; +import AgentLogger from "@best/agent-logger"; const createTask = (idx: number) => { const SocketMock = jest.fn(); @@ -26,15 +27,17 @@ const createTask = (idx: number) => { ); }; +const logger = new AgentLogger('Test Agent', { write: jest.fn() }); + describe('Agent app', () => { test('subscribes to queue.item-added and runner.idle-runner', async () => { const queue = new ObservableQueue(); const queueOnSpy = jest.spyOn(queue, 'on'); - const runner = new BenchmarkRunner(); + const runner = new BenchmarkRunner(logger); const runnerOnSpy = jest.spyOn(runner, 'on'); - const agentApp = new AgentApp(queue, runner); + const agentApp = new AgentApp(queue, runner, logger); expect(queueOnSpy).toHaveBeenCalled(); expect(queueOnSpy.mock.calls[0][0]).toBe('item-added'); @@ -48,12 +51,12 @@ describe('Agent app', () => { const queue = new ObservableQueue(); const queueRemoveSpy = jest.spyOn(queue, 'remove'); - const runner = new BenchmarkRunner(); + const runner = new BenchmarkRunner(logger); runner.run = jest.fn(); const runnerRunSpy = jest.spyOn(runner, 'run'); - const agentApp = new AgentApp(queue, runner); + const agentApp = new AgentApp(queue, runner, logger); const task = createTask(1); @@ -69,12 +72,12 @@ describe('Agent app', () => { test('if runner is running a task informs client that the job is added to the queue', async () => { const queue = new ObservableQueue(); - const runner = new BenchmarkRunner(); + const runner = new BenchmarkRunner(logger); runner.status = RunnerStatus.RUNNING; runner.run = jest.fn(); const runnerRunSpy = jest.spyOn(runner, 'run'); - const agentApp = new AgentApp(queue, runner); + const agentApp = new AgentApp(queue, runner, logger); const task = createTask(1); task.socketConnection.emit = jest.fn(); @@ -100,12 +103,12 @@ describe('Agent app', () => { queue.push(task1); queue.push(task2); - const runner = new BenchmarkRunner(); + const runner = new BenchmarkRunner(logger); runner.status = RunnerStatus.RUNNING; runner.run = jest.fn(); const runnerRunSpy = jest.spyOn(runner, 'run'); - const agentApp = new AgentApp(queue, runner); + const agentApp = new AgentApp(queue, runner, logger); runner.status = RunnerStatus.IDLE; @@ -119,9 +122,9 @@ describe('Agent app', () => { describe('incoming socket connection', () => { test('listen for benchmark_task event', async () => { const queue = new ObservableQueue(); - const runner = new BenchmarkRunner(); + const runner = new BenchmarkRunner(logger); - const agentApp = new AgentApp(queue, runner); + const agentApp = new AgentApp(queue, runner, logger); const SocketMock = jest.fn(); const socket = new SocketMock(); @@ -140,9 +143,9 @@ describe('Agent app', () => { queue.push = jest.fn(); const queuePushSpy = jest.spyOn(queue, 'push'); - const runner = new BenchmarkRunner(); + const runner = new BenchmarkRunner(logger); - const agentApp = new AgentApp(queue, runner); + const agentApp = new AgentApp(queue, runner, logger); const SocketMock = jest.fn(); const socket = new SocketMock(); @@ -185,11 +188,11 @@ describe('Agent app', () => { queue.remove = jest.fn(); const queueRemoveSpy = jest.spyOn(queue, 'remove'); - const runner = new BenchmarkRunner(); + const runner = new BenchmarkRunner(logger); runner.cancelRun = jest.fn(); const runnerCancelRunSpy = jest.spyOn(runner, 'cancelRun'); - const agentApp = new AgentApp(queue, runner); + const agentApp = new AgentApp(queue, runner, logger); const SocketMock = jest.fn(); const socket = new SocketMock(); diff --git a/packages/@best/agent/src/agent-service.ts b/packages/@best/agent/src/agent-service.ts index fb4f895d..e555e8a7 100644 --- a/packages/@best/agent/src/agent-service.ts +++ b/packages/@best/agent/src/agent-service.ts @@ -1,19 +1,23 @@ import socketIO from 'socket.io'; -import * as SocketIO from "socket.io"; import { AgentApp } from "./AgentApp"; import ObservableQueue from "./utils/ObservableQueue"; import BenchmarkTask from "./BenchmarkTask"; import BenchmarkRunner from "./BenchmarkRunner"; +import AgentLogger from '@best/agent-logger'; +import { attachMiddleware } from '@best/agent-frontend'; import { Server } from "http"; -export async function runAgent(server: Server) { - const socketServer: SocketIO.Server = socketIO(server, { path: '/best' }); +export function runAgent(server: Server) { + const socketServer = socketIO(server, { path: '/best' }); + const logger = new AgentLogger('Agent ' + (process.env.PORT || 5000)); const taskQueue = new ObservableQueue(); - const taskRunner = new BenchmarkRunner(); - const agentApp: AgentApp = new AgentApp(taskQueue, taskRunner); + const taskRunner = new BenchmarkRunner(logger); + const agentApp = new AgentApp(taskQueue, taskRunner, logger); socketServer.on('connect', (socket: SocketIO.Socket) => agentApp.handleIncomingConnection(socket)); + + attachMiddleware(socketServer, logger); } export default { runAgent }; diff --git a/packages/@best/agent/src/benchmark-loader.ts b/packages/@best/agent/src/benchmark-loader.ts index 04033674..31c6257b 100644 --- a/packages/@best/agent/src/benchmark-loader.ts +++ b/packages/@best/agent/src/benchmark-loader.ts @@ -2,6 +2,7 @@ import SocketIOFile from "socket.io-file"; import path from "path"; import { cacheDirectory } from '@best/utils'; import * as SocketIO from "socket.io"; +import AgentLogger from "@best/agent-logger"; // This is all part of the initialization const LOADER_CONFIG = { @@ -15,7 +16,7 @@ const LOADER_CONFIG = { const UPLOAD_START_TIMEOUT = 5000; -export async function loadBenchmarkJob(socketConnection: SocketIO.Socket): Promise { +export async function loadBenchmarkJob(socketConnection: SocketIO.Socket, logger: AgentLogger): Promise { return new Promise(async (resolve, reject) => { const socket = socketConnection; let uploaderTimeout: any = null; @@ -23,7 +24,7 @@ export async function loadBenchmarkJob(socketConnection: SocketIO.Socket): Promi uploader.on('start', () => clearTimeout(uploaderTimeout)); uploader.on('stream', ({ wrote, size }: any) => { - process.stdout.write(`Client[${socketConnection.id}] - downloading ${wrote} / ${size}\n`); + logger.info(socketConnection.id, 'downloading', `${wrote} / ${size}`); }); uploader.on('complete', (info: any) => resolve(info)); uploader.on('error', (err: any) => reject(err)); diff --git a/packages/@best/agent/src/cli/index.ts b/packages/@best/agent/src/cli/index.ts index a19ce788..93bde8b4 100644 --- a/packages/@best/agent/src/cli/index.ts +++ b/packages/@best/agent/src/cli/index.ts @@ -1,7 +1,8 @@ import express from 'express'; import { readFileSync } from 'fs'; import { runAgent } from '../agent-service'; -import { registerWithHub, HubConfig } from "../hub-registration"; +import { registerWithHub, HubConfig } from '../hub-registration'; +import { serveFrontend } from '@best/agent-frontend'; const PORT = process.env.PORT || 5000; const SSL_PFX_FILE = process.env.SSL_PFX_FILE; @@ -10,6 +11,7 @@ const hubRegistrationConfig: HubConfig = process.env.HUB_CONFIG ? JSON.parse(pro export function run() { const app = express(); + serveFrontend(app); const enableHttps = SSL_PFX_FILE && SSL_PFX_PASSPHRASE; const http = require(enableHttps ? 'https' : 'http'); @@ -21,8 +23,8 @@ export function run() { const server = http.createServer(options, app); server.listen(PORT); - app.get('/', (req, res) => res.send('BEST agent running!')); - process.stdout.write(`Best agent listening in port ${PORT}... \n\n`); + // app.get('/', (req, res) => res.send('BEST agent running!')); + process.stdout.write(`Best agent listening in port ${PORT}...\n`); runAgent(server); diff --git a/packages/@best/agent/src/hub-registration.ts b/packages/@best/agent/src/hub-registration.ts index a3bd159d..2e4e8661 100644 --- a/packages/@best/agent/src/hub-registration.ts +++ b/packages/@best/agent/src/hub-registration.ts @@ -53,6 +53,7 @@ async function connectToHub(hubConfig: HubConfig): Promise { } export async function registerWithHub(hubConfig: HubConfig) { + const pingTimeout = hubConfig.hub.pingTimeout || 30000; let keepPing = true; try { const agentStatus = await pingHub(hubConfig.hub.host, hubConfig.hub.authToken, hubConfig.agentConfig.host); @@ -71,11 +72,11 @@ export async function registerWithHub(hubConfig: HubConfig) { keepPing = false; console.log('Invalid auth credentials, suspending registration with hub'); } else { - console.log(`Retrying in ${hubConfig.hub.pingTimeout} ms`); + console.log(`Retrying in ${pingTimeout} ms`); } } if (keepPing) { - setTimeout(registerWithHub.bind(null, hubConfig), hubConfig.hub.pingTimeout); + setTimeout(registerWithHub.bind(null, hubConfig), pingTimeout); } } diff --git a/packages/@best/agent/tsconfig.json b/packages/@best/agent/tsconfig.json index d83c6a1b..deb2f61c 100644 --- a/packages/@best/agent/tsconfig.json +++ b/packages/@best/agent/tsconfig.json @@ -5,6 +5,8 @@ "outDir": "build" }, "references": [ + { "path": "../agent-frontend" }, + { "path": "../agent-logger" }, { "path": "../runner" }, { "path": "../utils" }, { "path": "../types" } diff --git a/packages/@best/runner-headless/src/headless.ts b/packages/@best/runner-headless/src/headless.ts index 1156b195..2fed0ea7 100644 --- a/packages/@best/runner-headless/src/headless.ts +++ b/packages/@best/runner-headless/src/headless.ts @@ -1,4 +1,6 @@ import path from 'path'; +import fs from 'fs'; +import os from 'os'; import puppeteer from 'puppeteer'; import { parseTrace, removeTrace, mergeTracedMetrics } from './trace' import { FrozenProjectConfig } from '@best/types'; @@ -17,6 +19,11 @@ const BROWSER_ARGS = [ const PUPPETEER_OPTIONS = { args: BROWSER_ARGS }; +function tempDir() { + const TEMP_DIR_PREFIX = 'runner-headless-temp'; + return fs.mkdtempSync(path.join(os.tmpdir(), TEMP_DIR_PREFIX)); +} + export default class HeadlessBrowser { pageUrl: string; projectConfig: FrozenProjectConfig; @@ -28,7 +35,7 @@ export default class HeadlessBrowser { constructor(url: string, projectConfig: FrozenProjectConfig) { this.pageUrl = url; this.projectConfig = projectConfig; - this.tracePath = path.resolve(projectConfig.benchmarkOutput, 'trace.json'); + this.tracePath = path.resolve(tempDir(), 'trace.json'); } async initialize() { diff --git a/packages/best-benchmarks/best.config.js b/packages/best-benchmarks/best.config.js index b517d290..2c4ef89a 100644 --- a/packages/best-benchmarks/best.config.js +++ b/packages/best-benchmarks/best.config.js @@ -18,11 +18,11 @@ module.exports = { "runner": "@best/runner-hub", "alias": "hub", "config": { - "host": "http://localhost:6000", + "host": "http://localhost:5555", "options": { path: "/hub", query: { - token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6ImNsaWVudCIsImlhdCI6MTU2MTYwNzI1OCwiZXhwIjoxNTY0MTk5MjU4fQ.BER-PIIlsf6NWNBctWrmS1YWB4QkI2aYiNp0BE6aASU' + token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzY29wZSI6ImNsaWVudCIsImlhdCI6MTU2MzI5NjkyMywiZXhwIjoxNTY1ODg4OTIzfQ.3TN91ySnte8_dhJ1Iabe4fUcOvS7lp9J700YywCMC5Q" } }, "spec": { diff --git a/scripts/jest/root.config.js b/scripts/jest/root.config.js index 27927826..ba119c06 100644 --- a/scripts/jest/root.config.js +++ b/scripts/jest/root.config.js @@ -17,6 +17,8 @@ module.exports = { '/packages/@best/regex-util', '/packages/@best/frontend', '/packages/@best/agent', + '/packages/@best/agent-frontend', + '/packages/@best/agent-logger', '/packages/@best/config', '/packages/@best/builder', '/packages/@best/runner-headless', diff --git a/tsconfig.json b/tsconfig.json index 5e55a33a..5dd3460e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,6 +4,8 @@ { "path": "./packages/@best/types" }, { "path": "./packages/@best/agent" }, { "path": "./packages/@best/agent-hub" }, + { "path": "./packages/@best/agent-frontend" }, + { "path": "./packages/@best/agent-logger" }, { "path": "./packages/@best/analyzer" }, { "path": "./packages/@best/builder" }, { "path": "./packages/@best/cli" }, diff --git a/yarn.lock b/yarn.lock index 6edc4862..dc7661d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -95,6 +95,18 @@ "@babel/traverse" "^7.4.4" "@babel/types" "^7.4.4" +"@babel/helper-create-class-features-plugin@^7.4.4": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.0.tgz#02edb97f512d44ba23b3227f1bf2ed43454edac5" + integrity sha512-EAoMc3hE5vE5LNhMqDOwB1usHvmRjCDAnH8CD4PVkX9/Yr3W/tcz8xE8QvdZxfsFBDICwZnF2UTHIqslRpvxmA== + dependencies: + "@babel/helper-function-name" "^7.1.0" + "@babel/helper-member-expression-to-functions" "^7.0.0" + "@babel/helper-optimise-call-expression" "^7.0.0" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/helper-replace-supers" "^7.4.4" + "@babel/helper-split-export-declaration" "^7.4.4" + "@babel/helper-define-map@^7.1.0": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-define-map/-/helper-define-map-7.4.4.tgz#6969d1f570b46bdc900d1eba8e5d59c48ba2c12a" @@ -191,7 +203,7 @@ "@babel/traverse" "^7.1.0" "@babel/types" "^7.0.0" -"@babel/helper-replace-supers@^7.1.0": +"@babel/helper-replace-supers@^7.1.0", "@babel/helper-replace-supers@^7.4.4": version "7.4.4" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27" integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg== @@ -266,6 +278,15 @@ "@babel/helper-replace-supers" "^7.1.0" "@babel/plugin-syntax-class-properties" "^7.0.0" +"@babel/plugin-proposal-decorators@^7.4.4": + version "7.4.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.4.4.tgz#de9b2a1a8ab0196f378e2a82f10b6e2a36f21cc0" + integrity sha512-z7MpQz3XC/iQJWXH9y+MaWcLPNSMY9RQSthrLzak8R8hCj0fuyNk+Dzi9kfNe/JxxlWQ2g7wkABbgWjW36MTcw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.4.4" + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-decorators" "^7.2.0" + "@babel/plugin-proposal-object-rest-spread@7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.0.0.tgz#9a17b547f64d0676b6c9cecd4edf74a82ab85e7e" @@ -289,6 +310,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.0.0" +"@babel/plugin-syntax-decorators@^7.2.0": + version "7.2.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.2.0.tgz#c50b1b957dcc69e4b1127b65e1c33eef61570c1b" + integrity sha512-38QdqVoXdHUQfTpZo3rQwqQdWtCn5tMv4uV6r2RMfTqNBuv4ZBhz79SfaQWKTVmxHjeFv/DnXVC/+agHCklYWA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-syntax-object-rest-spread@^7.0.0", "@babel/plugin-syntax-object-rest-spread@^7.2.0": version "7.2.0" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.2.0.tgz#3b7a3e733510c57e820b9142a6579ac8b0dfad2e" @@ -2180,6 +2208,11 @@ dependencies: "@types/node" "*" +"@types/memoizee@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@types/memoizee/-/memoizee-0.4.2.tgz#a500158999a8144a9b46cf9a9fb49b15f1853573" + integrity sha512-bhdZXZWKfpkQuuiQjVjnPiNeBHpIAC6rfOFqlJXKD3VC35mCcolfVfXYTnk9Ppee5Mkmmz3Llgec7xCdJAbzWw== + "@types/micromatch@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@types/micromatch/-/micromatch-3.1.0.tgz#514c8a3d24b2680a9b838eeb80e6d7d724545433" @@ -7044,6 +7077,22 @@ husky@2.4.0: run-node "^1.0.0" slash "^3.0.0" +husky@^2.3: + version "2.7.0" + resolved "https://registry.yarnpkg.com/husky/-/husky-2.7.0.tgz#c0a9a6a3b51146224e11bba0b46bba546e461d05" + integrity sha512-LIi8zzT6PyFpcYKdvWRCn/8X+6SuG2TgYYMrM6ckEYhlp44UcEduVymZGIZNLiwOUjrEud+78w/AsAiqJA/kRg== + dependencies: + cosmiconfig "^5.2.0" + execa "^1.0.0" + find-up "^3.0.0" + get-stdin "^7.0.0" + is-ci "^2.0.0" + pkg-dir "^4.1.0" + please-upgrade-node "^3.1.1" + read-pkg "^5.1.1" + run-node "^1.0.0" + slash "^3.0.0" + hyperlinker@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hyperlinker/-/hyperlinker-1.0.0.tgz#23dc9e38a206b208ee49bc2d6c8ef47027df0c0e" @@ -8518,6 +8567,36 @@ linkify-it@^2.0.0: dependencies: uc.micro "^1.0.1" +lint-staged@^8.1.5: + version "8.2.1" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.2.1.tgz#752fcf222d9d28f323a3b80f1e668f3654ff221f" + integrity sha512-n0tDGR/rTCgQNwXnUf/eWIpPNddGWxC32ANTNYsj2k02iZb7Cz5ox2tytwBu+2r0zDXMEMKw7Y9OD/qsav561A== + dependencies: + chalk "^2.3.1" + commander "^2.14.1" + cosmiconfig "^5.2.0" + debug "^3.1.0" + dedent "^0.7.0" + del "^3.0.0" + execa "^1.0.0" + g-status "^2.0.2" + is-glob "^4.0.0" + is-windows "^1.0.2" + listr "^0.14.2" + listr-update-renderer "^0.5.0" + lodash "^4.17.11" + log-symbols "^2.2.0" + micromatch "^3.1.8" + npm-which "^3.0.1" + p-map "^1.1.1" + path-is-inside "^1.0.2" + pify "^3.0.0" + please-upgrade-node "^3.0.2" + staged-git-files "1.1.2" + string-argv "^0.0.2" + stringify-object "^3.2.2" + yup "^0.27.0" + lint-staged@~8.1.0: version "8.1.7" resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.1.7.tgz#a8988bc83bdffa97d04adb09dbc0b1f3a58fa6fc" @@ -9135,7 +9214,7 @@ mem@^4.0.0: mimic-fn "^2.0.0" p-is-promise "^2.0.0" -memoizee@^0.4.14: +memoizee@0.4.14, memoizee@^0.4.14: version "0.4.14" resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.14.tgz#07a00f204699f9a95c2d9e77218271c7cd610d57" integrity sha512-/SWFvWegAIYAO4NQMpcX+gcra0yEZu4OntmUdrBaWrJncxOqAziGFlHxc7yjKVK2uu3lpPW27P27wkR82wA8mg==