├── .editorconfig ├── .gitignore ├── .travis.yml ├── README.md ├── app ├── io │ └── gatling │ │ ├── app │ │ ├── Gatling.scala │ │ ├── PeaGatlingRunner.scala │ │ ├── PeaRunResultProcessor.scala │ │ ├── RunResult.scala │ │ ├── RunResultProcessor.scala │ │ ├── Runner.scala │ │ ├── Selection.scala │ │ ├── classloader │ │ │ ├── FileSystemBackedClassLoader.scala │ │ │ └── SimulationClassLoader.scala │ │ ├── cli │ │ │ ├── ArgsParser.scala │ │ │ ├── CommandLineConstants.scala │ │ │ └── StatusCode.scala │ │ └── package.scala │ │ └── charts │ │ └── report │ │ ├── PeaReportsGenerator.scala │ │ └── PeaStatsReportGenerator.scala └── pea │ └── app │ ├── ErrorMessages.scala │ ├── PeaConfig.scala │ ├── actor │ ├── CompilerActor.scala │ ├── CompilerMonitorActor.scala │ ├── GatlingRunnerActor.scala │ ├── ProgramRunnerActor.scala │ ├── ReporterActor.scala │ ├── ReporterWorkersActor.scala │ ├── ResponseMonitorActor.scala │ ├── WebCompilerMonitorActor.scala │ ├── WebResponseMonitorActor.scala │ ├── WebWorkerMonitorActor.scala │ ├── WorkerActor.scala │ └── WorkerMonitorActor.scala │ ├── api │ ├── BaseApi.scala │ ├── CommonChecks.scala │ ├── CommonFunctions.scala │ ├── GatlingApi.scala │ ├── HomeApi.scala │ ├── ResourceApi.scala │ ├── filters │ │ └── SecurityFilters.scala │ └── util │ │ └── ResultUtils.scala │ ├── compiler │ ├── CompileResponse.scala │ ├── CompilerConfiguration.scala │ ├── ReloadableClassLoader.scala │ ├── ScalaCompiler.scala │ └── ZincCompilerInstance.scala │ ├── gatling │ ├── PeaConfigKeys.scala │ ├── PeaDataWriter.scala │ ├── PeaDataWriterTypes.scala │ ├── PeaDataWritersStatsEngine.scala │ ├── PeaRequestStatistics.scala │ └── PeaSimulation.scala │ ├── hook │ ├── ApplicationStart.scala │ └── ErrorHandler.scala │ ├── http │ ├── HostHttpSource.scala │ └── HttpClient.scala │ ├── model │ ├── DownloadResourceRequest.scala │ ├── FinishedCallbackRequest.scala │ ├── FinishedCallbackResponse.scala │ ├── Injection.scala │ ├── LoadJob.scala │ ├── LoadMessage.scala │ ├── LoadTypes.scala │ ├── MemberStatus.scala │ ├── OshiInfo.scala │ ├── PeaMember.scala │ ├── ReporterJobStatus.scala │ ├── ResourceModels.scala │ ├── Role.scala │ ├── SimulationModel.scala │ ├── SingleJob.scala │ ├── SingleRequest.scala │ ├── WorkersCompileRequest.scala │ ├── WorkersRequest.scala │ ├── job │ │ ├── RunProgramMessage.scala │ │ ├── RunProgramSingleJob.scala │ │ ├── RunScriptMessage.scala │ │ ├── RunScriptSingleJob.scala │ │ ├── SingleHttpScenarioMessage.scala │ │ └── SingleHttpScenarioSingleJob.scala │ └── params │ │ ├── AssertionItem.scala │ │ ├── AssertionsParam.scala │ │ ├── DurationParam.scala │ │ ├── FeederParam.scala │ │ ├── HttpAssertionParam.scala │ │ ├── LoopParam.scala │ │ ├── ThrottleParam.scala │ │ └── ThrottleStep.scala │ ├── modules │ ├── ApplicationStartModule.scala │ └── BasicSecurityModule.scala │ ├── package.scala │ ├── service │ ├── NotifyService.scala │ ├── PeaService.scala │ └── ResourceService.scala │ ├── simulation │ └── SingleHttpSimulation.scala │ └── util │ ├── FileUtils.scala │ └── SimulationLogUtils.scala ├── build.sbt ├── conf ├── application.conf ├── framework.conf ├── gatling.conf ├── logback.xml ├── messages ├── messages.en └── routes ├── dist ├── bin │ └── pea.sh └── ext │ └── .gitkeep ├── docker ├── Dockerfile └── build.sh ├── images ├── banner.jpeg ├── report-01.png ├── report-02.png ├── shoot-01.png └── shoot-job.png ├── license └── gatling.txt ├── pea-common └── src │ └── main │ └── scala │ └── pea │ └── common │ ├── actor │ ├── ActorEvent.scala │ ├── BaseActor.scala │ └── SenderMessage.scala │ ├── exceptions │ └── ErrorMessages.scala │ ├── model │ └── package.scala │ └── util │ ├── DateUtils.scala │ ├── FutureUtils.scala │ ├── JsonUtils.scala │ ├── LogUtils.scala │ ├── NetworkUtils.scala │ ├── ProcessUtils.scala │ ├── StringUtils.scala │ └── XtermUtils.scala ├── pea-dubbo └── src │ └── main │ └── scala │ └── pea │ └── dubbo │ ├── DubboDsl.scala │ ├── Predef.scala │ ├── action │ ├── DubboAction.scala │ └── DubboActionBuilder.scala │ ├── check │ ├── DubboCheckModel.scala │ ├── DubboCheckSupport.scala │ ├── DubboJsonPathCheckMaterializer.scala │ └── DubboSimpleCheck.scala │ ├── dubbo.scala │ ├── protocol │ ├── DubboComponents.scala │ ├── DubboProtocol.scala │ ├── DubboProtocolBuilder.scala │ └── ProtocolModifier.scala │ └── request │ ├── CustomReferenceConfig.java │ ├── DubboDslBuilder.scala │ └── ReferenceConfigCache.scala ├── pea-grpc ├── README.md └── src │ └── main │ └── scala │ └── pea │ └── grpc │ ├── GrpcDsl.scala │ ├── Predef.scala │ ├── action │ ├── GrpcAction.scala │ └── GrpcActionBuilder.scala │ ├── check │ ├── GrpcCheck.scala │ ├── GrpcCheckSupport.scala │ ├── ResponseExtract.scala │ └── StatusExtract.scala │ ├── grpc.scala │ ├── protocol │ ├── GrpcComponents.scala │ └── GrpcProtocol.scala │ └── request │ └── HeaderPair.scala ├── project ├── Dependencies.scala ├── FrontendCommands.scala ├── FrontendRunHook.scala ├── build.properties ├── plugins.sbt └── scalapb.sbt ├── test-generated └── pea │ └── grpc │ └── hello │ ├── HelloProto.scala │ ├── HelloRequest.scala │ ├── HelloResponse.scala │ └── HelloServiceGrpc.scala ├── test ├── logback-test.xml ├── pea │ └── app │ │ ├── IDEPathHelper.scala │ │ ├── compiler │ │ └── ZincCompilerSpec.scala │ │ ├── dubbo │ │ ├── GreetingConsumerApp.scala │ │ ├── GreetingProviderApp.scala │ │ ├── RegistryAddressConfig.scala │ │ ├── api │ │ │ └── GreetingService.java │ │ └── provider │ │ │ └── GreetingsServiceImpl.java │ │ ├── gatling │ │ ├── CompilerSpec.scala │ │ └── RunnerSpec.scala │ │ ├── grpc │ │ ├── HelloServiceClient.scala │ │ └── HelloServiceServer.scala │ │ └── simulations │ │ ├── BasicSimulation.scala │ │ ├── DubboGreetingSimulation.scala │ │ └── GrpcHelloSimulation.scala └── protobuf │ └── hello.proto ├── ui-build.sbt └── ui ├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── browserslist ├── build.sh ├── dev.sh ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.json ├── karma.conf.js ├── package.json ├── src ├── app │ ├── api │ │ ├── api-code.interceptor.ts │ │ ├── base.service.ts │ │ ├── gatling.service.ts │ │ ├── home.service.ts │ │ ├── resource.service.ts │ │ └── xterm.service.ts │ ├── app-routing.module.ts │ ├── app.component.css │ ├── app.component.html │ ├── app.component.ts │ ├── app.module.ts │ ├── icons-provider.module.ts │ ├── model │ │ ├── api.model.ts │ │ └── pea.model.ts │ ├── pages │ │ ├── lets-shoot │ │ │ ├── lets-shoot.component.css │ │ │ ├── lets-shoot.component.html │ │ │ ├── lets-shoot.component.ts │ │ │ ├── lets-shoot.module.ts │ │ │ └── lets-shoot.routing.module.ts │ │ ├── local-reports │ │ │ ├── local-reports.component.css │ │ │ ├── local-reports.component.html │ │ │ ├── local-reports.component.ts │ │ │ ├── local-reports.module.ts │ │ │ └── local-reports.routing.module.ts │ │ ├── local-resources │ │ │ ├── local-resources.component.css │ │ │ ├── local-resources.component.html │ │ │ ├── local-resources.component.ts │ │ │ ├── local-resources.module.ts │ │ │ └── local-resources.routing.module.ts │ │ ├── running-jobs │ │ │ ├── job-summary │ │ │ │ ├── job-summary.component.css │ │ │ │ ├── job-summary.component.html │ │ │ │ └── job-summary.component.ts │ │ │ ├── running-job │ │ │ │ ├── running-job.component.css │ │ │ │ ├── running-job.component.html │ │ │ │ └── running-job.component.ts │ │ │ ├── running-jobs.component.css │ │ │ ├── running-jobs.component.html │ │ │ ├── running-jobs.component.ts │ │ │ ├── running-jobs.module.ts │ │ │ ├── running-jobs.routing.module.ts │ │ │ └── worker-status │ │ │ │ ├── worker-status.component.css │ │ │ │ ├── worker-status.component.html │ │ │ │ └── worker-status.component.ts │ │ ├── shared │ │ │ ├── assertions │ │ │ │ ├── assertions.component.css │ │ │ │ ├── assertions.component.html │ │ │ │ └── assertions.component.ts │ │ │ ├── feeder │ │ │ │ ├── feeder.component.css │ │ │ │ ├── feeder.component.html │ │ │ │ └── feeder.component.ts │ │ │ ├── injections-builder │ │ │ │ ├── injections-builder.component.css │ │ │ │ ├── injections-builder.component.html │ │ │ │ └── injections-builder.component.ts │ │ │ ├── member-selector │ │ │ │ ├── member-selector.component.css │ │ │ │ ├── member-selector.component.html │ │ │ │ └── member-selector.component.ts │ │ │ ├── oshi-info │ │ │ │ ├── oshi-info.component.css │ │ │ │ ├── oshi-info.component.html │ │ │ │ └── oshi-info.component.ts │ │ │ ├── pea-member │ │ │ │ ├── pea-member.component.css │ │ │ │ ├── pea-member.component.html │ │ │ │ └── pea-member.component.ts │ │ │ ├── response-monitor │ │ │ │ ├── response-monitor.component.css │ │ │ │ ├── response-monitor.component.html │ │ │ │ └── response-monitor.component.ts │ │ │ ├── shared.module.ts │ │ │ └── throttle │ │ │ │ ├── throttle.component.css │ │ │ │ ├── throttle.component.html │ │ │ │ └── throttle.component.ts │ │ ├── simulations │ │ │ ├── compiler-output │ │ │ │ ├── compiler-output.component.css │ │ │ │ ├── compiler-output.component.html │ │ │ │ └── compiler-output.component.ts │ │ │ ├── simulations.component.css │ │ │ ├── simulations.component.html │ │ │ ├── simulations.component.ts │ │ │ ├── simulations.module.ts │ │ │ └── simulations.routing.module.ts │ │ └── worker-peas │ │ │ ├── worker-peas.component.css │ │ │ ├── worker-peas.component.html │ │ │ ├── worker-peas.component.ts │ │ │ ├── worker-peas.module.ts │ │ │ └── worker-peas.routing.module.ts │ └── util │ │ ├── drawer.ts │ │ ├── file.ts │ │ ├── injection.ts │ │ └── ws.ts ├── assets │ ├── .gitkeep │ ├── i18n │ │ ├── cn.json │ │ └── en.json │ └── img │ │ ├── logo.gif │ │ ├── logo.png │ │ └── shoot.jpg ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── polyfills.ts ├── proxy.conf.js ├── styles.css ├── test.ts └── theme.less ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.spec.json ├── tslint.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/ 3 | logs/ 4 | /.idea_modules 5 | /.classpath 6 | /.project 7 | /.settings 8 | /RUNNING_PID 9 | node_modules/ 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | _book/ 14 | target/ 15 | docs/_build/ 16 | .vscode/ 17 | /public 18 | *.zip 19 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | jdk: 3 | - openjdk8 4 | sbt_args: -Xmx2048M 5 | env: 6 | - NODE_VERSION="12.8.1" 7 | before_install: 8 | - nvm install $NODE_VERSION 9 | before_script: 10 | - travis_wait 30 sbt clean 11 | script: 12 | - sbt coverage test coverageReport 13 | - find $HOME/.sbt -name "*.lock" | xargs rm 14 | - find $HOME/.ivy2 -name "ivydata-*.properties" | xargs rm 15 | after_success: 16 | - bash <(curl -s https://codecov.io/bash) 17 | sudo: false 18 | # cache settings 19 | cache: 20 | directories: 21 | - $HOME/.ivy2/cache 22 | - $HOME/.sbt/launchers 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gatling Pea 2 | 3 | ![](./images/banner.jpeg) 4 | 5 | [![Build Status](https://travis-ci.org/asura-pro/pea.svg?branch=master)](https://travis-ci.org/asura-pro/pea) 6 | ![GitHub release](https://img.shields.io/github/release/asura-pro/pea.svg) 7 | ![Maven Central](https://img.shields.io/maven-metadata/v/http/central.maven.org/maven2/cc/akkaha/pea_2.12/maven-metadata.xml.svg) 8 | 9 | --- 10 | 11 | ### 关于 Gatling 12 | 13 | [Gatling](http://gatling.io/) 是基于 [Netty](https://netty.io/) 和 [Akka](http://akka.io/) 技术实现的高性能压测工具. 14 | 15 | ### 关于 Pea 16 | 17 | 由于单独一台机器硬件资源和网络协议的限制存在, 在高负载测试中需要多台机器共同提供负载. `Pea` 是在以 `Galting` 为引擎, 在多节点场景下的压测工具. 包含以下特性: 18 | 19 | - 管理和监控多个工作节点. 依赖 Zookeeper 20 | - 运行过程中可以实时查看每个节点的具体执行状态 21 | - 多个节点执行完后会自动收集日志, 生成统一的报告 22 | - 支持原生的 Gatling 脚本, 原生的 `HTTP` 协议 23 | - 扩展支持了 `Dubbo`和 `Grpc` 协议 24 | - 以 Git 仓库管理脚本和资源文件 25 | - 内置了 Scala 增量编译器, 脚本可在线快速编译 26 | - 不同于其他实现, 所有这些功能都在同一进程内实现. 感谢 Gatling 作者高质量的代码 27 | - 可以在实体机, 虚拟机, Docker 容器中运行 28 | 29 | --- 30 | 31 | ### 脚本示例 32 | 33 | ```scala 34 | import io.gatling.core.Predef._ 35 | import pea.dubbo.Predef._ 36 | import pea.dubbo.api.GreetingService 37 | import pea.gatling.PeaSimulation 38 | 39 | class DubboGreetingSimulation extends PeaSimulation { 40 | override val description: String = 41 | """ 42 | |Dubbo simulation example 43 | |""".stripMargin 44 | val dubboProtocol = dubbo 45 | .application("gatling-pea") 46 | .endpoint("127.0.0.1", 20880) 47 | .threads(10) 48 | val scn = scenario("dubbo") 49 | .exec( 50 | invoke(classOf[GreetingService]) { (service, _) => 51 | service.sayHello("pea") 52 | }.check(simple { response => 53 | response.value == "hi, pea" 54 | }).check( 55 | jsonPath("$").is("hi, pea") 56 | ) 57 | ) 58 | setUp( 59 | scn.inject(atOnceUsers(10000)) 60 | ).protocols(dubboProtocol) 61 | } 62 | ``` 63 | 64 | --- 65 | 66 | ### 版本 67 | 68 | | pea | gatling| dubbo | grpc | 69 | | ------ | ------ | ------- | ------ | 70 | | 0.6.0~ | 3.3.1 | 2.7.4.1 | 1.22.2 | 71 | 72 | --- 73 | 74 | ### 视频演示 75 | 76 | > https://www.bilibili.com/video/av73339161/ 77 | 78 | ### 截图示例 79 | 80 | `创建任务` 81 | ![](./images/shoot-01.png) 82 | 83 | `任务执行中的节点状态` 84 | ![](./images/shoot-job.png) 85 | 86 | `整体报告` 87 | ![](./images/report-01.png) 88 | 89 | `单个请求细节报告` 90 | ![](./images/report-02.png) 91 | -------------------------------------------------------------------------------- /app/io/gatling/app/RunResult.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011-2019 GatlingCorp (https://gatling.io) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.gatling.app 18 | 19 | case class RunResult(runId: String, hasAssertions: Boolean) 20 | -------------------------------------------------------------------------------- /app/io/gatling/app/cli/CommandLineConstants.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011-2019 GatlingCorp (https://gatling.io) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.gatling.app.cli 18 | 19 | import io.gatling.core.cli.CommandLineConstant 20 | 21 | private[cli] object CommandLineConstants { 22 | val Help = CommandLineConstant("help", "h") 23 | val NoReports = CommandLineConstant("no-reports", "nr") 24 | val ReportsOnly = CommandLineConstant("reports-only", "ro") 25 | val ResultsFolder = CommandLineConstant("results-folder", "rf") 26 | val ResourcesFolder = CommandLineConstant("resources-folder", "rsf") 27 | val SimulationsFolder = CommandLineConstant("simulations-folder", "sf") 28 | val BinariesFolder = CommandLineConstant("binaries-folder", "bf") 29 | val Simulation = CommandLineConstant("simulation", "s") 30 | val RunDescription = CommandLineConstant("run-description", "rd") 31 | } 32 | -------------------------------------------------------------------------------- /app/io/gatling/app/cli/StatusCode.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011-2019 GatlingCorp (https://gatling.io) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.gatling.app.cli 18 | 19 | object StatusCode { 20 | case object Success extends StatusCode(0) 21 | case object InvalidArguments extends StatusCode(1) 22 | case object AssertionsFailed extends StatusCode(2) 23 | } 24 | 25 | private[gatling] sealed abstract class StatusCode(val code: Int) 26 | -------------------------------------------------------------------------------- /app/io/gatling/app/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011-2019 GatlingCorp (https://gatling.io) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package io.gatling 18 | 19 | import io.gatling.core.scenario.Simulation 20 | 21 | import scala.collection.mutable 22 | 23 | package object app { 24 | 25 | type ConfigOverrides = mutable.Map[String, _] 26 | 27 | type SelectedSimulationClass = Option[Class[Simulation]] 28 | type SimulationClasses = List[Class[Simulation]] 29 | } 30 | -------------------------------------------------------------------------------- /app/pea/app/ErrorMessages.scala: -------------------------------------------------------------------------------- 1 | package pea.app 2 | 3 | import pea.common.exceptions.ErrorMessages.ErrorMessage 4 | import pea.common.exceptions.{ErrorMessages => CommonErrorMessages} 5 | 6 | object ErrorMessages extends CommonErrorMessages { 7 | 8 | val error_BusyStatus = ErrorMessage("Node is busy")("error_BusyStatus") 9 | } 10 | -------------------------------------------------------------------------------- /app/pea/app/PeaConfig.scala: -------------------------------------------------------------------------------- 1 | package pea.app 2 | 3 | import akka.actor.{ActorRef, ActorSystem} 4 | import akka.stream.ActorMaterializer 5 | import akka.util.Timeout 6 | import org.apache.curator.framework.CuratorFramework 7 | import pea.common.util.StringUtils 8 | 9 | import scala.concurrent.ExecutionContext 10 | import scala.concurrent.duration._ 11 | 12 | object PeaConfig { 13 | 14 | val DEFAULT_SCHEME = "pea" 15 | val DEFAULT_WS_ACTOR_BUFFER_SIZE = 10000 16 | val KEEP_ALIVE_INTERVAL = 2 17 | val PATH_WORKERS = "workers" 18 | val PATH_REPORTERS = "reporters" 19 | val PATH_JOBS = "jobs" 20 | val SIMULATION_LOG_FILE = "simulation.log" 21 | 22 | implicit val DEFAULT_ACTOR_ASK_TIMEOUT: Timeout = 10 minutes 23 | implicit var system: ActorSystem = _ 24 | implicit var dispatcher: ExecutionContext = _ 25 | implicit var materializer: ActorMaterializer = _ 26 | 27 | // system actor 28 | var workerActor: ActorRef = null 29 | var reporterActor: ActorRef = null 30 | var workerMonitorActor: ActorRef = null 31 | var compilerMonitorActor: ActorRef = null 32 | var responseMonitorActor: ActorRef = null 33 | 34 | // node 35 | var address = StringUtils.EMPTY 36 | var port = 0 37 | var hostname = StringUtils.EMPTY 38 | var label = StringUtils.EMPTY 39 | 40 | // roles 41 | var enableReporter = false 42 | var enableWorker = false 43 | 44 | // zk 45 | var zkClient: CuratorFramework = null 46 | var zkRootPath: String = null 47 | var zkCurrNode: String = null 48 | var zkCurrWorkerPath: String = null 49 | var zkCurrReporterPath: String = null 50 | 51 | // gatling report 52 | var reportLogoHref: String = null 53 | var reportDescHref: String = null 54 | var reportDescContent: String = null 55 | var resultsFolder: String = null 56 | var resourcesFolder: String = null 57 | 58 | // worker 59 | var workerProtocol: String = "http" 60 | var defaultSimulationSourceFolder: String = null 61 | var defaultSimulationOutputFolder: String = null 62 | var webSimulationEditorBaseUrl: String = null 63 | var compilerExtraClasspath: String = null 64 | } 65 | -------------------------------------------------------------------------------- /app/pea/app/actor/CompilerMonitorActor.scala: -------------------------------------------------------------------------------- 1 | package pea.app.actor 2 | 3 | import akka.actor.{ActorRef, ActorSystem, Props} 4 | import akka.event.{ActorClassifier, ActorEventBus, ManagedActorClassification} 5 | import pea.app.actor.CompilerMonitorActor.{CompilerMonitorBus, MonitorMessage, MonitorSubscriberMessage} 6 | import pea.common.actor.BaseActor 7 | 8 | class CompilerMonitorActor extends BaseActor { 9 | 10 | val monitorBus = new CompilerMonitorBus(context.system) 11 | 12 | override def receive: Receive = { 13 | case MonitorSubscriberMessage(ref) => 14 | monitorBus.subscribe(ref, self) 15 | case data: String => 16 | monitorBus.publish(MonitorMessage(self, data)) 17 | case message: Any => 18 | log.warning(s"Unknown message type ${message}") 19 | } 20 | } 21 | 22 | object CompilerMonitorActor { 23 | 24 | def props() = Props(new CompilerMonitorActor()) 25 | 26 | case class MonitorSubscriberMessage(ref: ActorRef) 27 | 28 | case class MonitorMessage(ref: ActorRef, data: String) 29 | 30 | class CompilerMonitorBus(val system: ActorSystem) extends ActorEventBus with ActorClassifier with ManagedActorClassification { 31 | 32 | override type Event = MonitorMessage 33 | 34 | override protected def classify(event: MonitorMessage): ActorRef = event.ref 35 | 36 | override protected def mapSize: Int = 1 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /app/pea/app/actor/ResponseMonitorActor.scala: -------------------------------------------------------------------------------- 1 | package pea.app.actor 2 | 3 | import akka.actor.{ActorRef, ActorSystem, Props} 4 | import akka.event.{ActorClassifier, ActorEventBus, ManagedActorClassification} 5 | import pea.app.actor.ResponseMonitorActor.{ResponseMessage, ResponseMonitorBus, ResponseSubscriberMessage} 6 | import pea.common.actor.BaseActor 7 | import pea.common.util.XtermUtils 8 | 9 | class ResponseMonitorActor extends BaseActor { 10 | 11 | val monitorBus = new ResponseMonitorBus(context.system) 12 | 13 | override def receive: Receive = { 14 | case ResponseSubscriberMessage(ref) => 15 | monitorBus.subscribe(ref, self) 16 | case data: String => 17 | monitorBus.publish(ResponseMessage(self, data)) 18 | case message: Any => 19 | log.warning(s"Unknown message type ${message}") 20 | } 21 | } 22 | 23 | object ResponseMonitorActor { 24 | 25 | def props() = Props(new ResponseMonitorActor()) 26 | 27 | case class ResponseSubscriberMessage(ref: ActorRef) 28 | 29 | case class ResponseMessage(ref: ActorRef, data: String) 30 | 31 | class ResponseMonitorBus(val system: ActorSystem) extends ActorEventBus with ActorClassifier with ManagedActorClassification { 32 | 33 | override type Event = ResponseMessage 34 | 35 | override protected def classify(event: ResponseMessage): ActorRef = event.ref 36 | 37 | override protected def mapSize: Int = 1 38 | } 39 | 40 | def formatResponse(status: Int, response: String): String = { 41 | s""" 42 | |${if (status != 200) XtermUtils.redWrap(status.toString) else XtermUtils.greenWrap(status.toString)} 43 | |${XtermUtils.blueWrap(response)} 44 | |""".stripMargin 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/pea/app/actor/WebCompilerMonitorActor.scala: -------------------------------------------------------------------------------- 1 | package pea.app.actor 2 | 3 | import akka.actor.{ActorRef, Props} 4 | import pea.app.PeaConfig 5 | import pea.app.actor.CompilerMonitorActor.{MonitorMessage, MonitorSubscriberMessage} 6 | import pea.common.actor.{BaseActor, NotifyActorEvent, SenderMessage} 7 | 8 | class WebCompilerMonitorActor() extends BaseActor { 9 | 10 | PeaConfig.compilerMonitorActor ! MonitorSubscriberMessage(self) 11 | var webActor: ActorRef = null 12 | 13 | override def receive: Receive = { 14 | case SenderMessage(sender) => webActor = sender 15 | case MonitorMessage(_, data) => 16 | if (null != webActor) webActor ! NotifyActorEvent(data) 17 | case _ => 18 | } 19 | } 20 | 21 | object WebCompilerMonitorActor { 22 | 23 | def props() = Props(new WebCompilerMonitorActor()) 24 | 25 | case class WebCompilerMonitorOptions() 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/pea/app/actor/WebResponseMonitorActor.scala: -------------------------------------------------------------------------------- 1 | package pea.app.actor 2 | 3 | import akka.actor.{ActorRef, Props} 4 | import pea.app.PeaConfig 5 | import pea.app.actor.ResponseMonitorActor.{ResponseMessage, ResponseSubscriberMessage} 6 | import pea.common.actor.{BaseActor, NotifyActorEvent, SenderMessage} 7 | 8 | class WebResponseMonitorActor() extends BaseActor { 9 | 10 | PeaConfig.responseMonitorActor ! ResponseSubscriberMessage(self) 11 | var webActor: ActorRef = null 12 | 13 | override def receive: Receive = { 14 | case SenderMessage(sender) => webActor = sender 15 | case ResponseMessage(_, data) => 16 | if (null != webActor) webActor ! NotifyActorEvent(data) 17 | case _ => 18 | } 19 | } 20 | 21 | object WebResponseMonitorActor { 22 | 23 | def props() = Props(new WebResponseMonitorActor()) 24 | 25 | case class WebResponseMonitorOptions() 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/pea/app/actor/WebWorkerMonitorActor.scala: -------------------------------------------------------------------------------- 1 | package pea.app.actor 2 | 3 | import akka.actor.{ActorRef, Props} 4 | import pea.app.PeaConfig 5 | import pea.app.actor.WorkerMonitorActor.{MonitorMessage, MonitorSubscriberMessage} 6 | import pea.common.actor.{BaseActor, ItemActorEvent, SenderMessage} 7 | 8 | /** 9 | * subscribe to monitor event bus 10 | */ 11 | class WebWorkerMonitorActor() extends BaseActor { 12 | 13 | PeaConfig.workerMonitorActor ! MonitorSubscriberMessage(self) 14 | var webActor: ActorRef = null 15 | 16 | override def receive: Receive = { 17 | case SenderMessage(sender) => webActor = sender 18 | case MonitorMessage(_, data) => 19 | if (null != webActor) webActor ! ItemActorEvent(data) 20 | case _ => 21 | } 22 | } 23 | 24 | object WebWorkerMonitorActor { 25 | 26 | def props() = Props(new WebWorkerMonitorActor()) 27 | 28 | case class WebWorkerMonitorOptions() 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/pea/app/actor/WorkerMonitorActor.scala: -------------------------------------------------------------------------------- 1 | package pea.app.actor 2 | 3 | import akka.actor.{ActorRef, ActorSystem, Props} 4 | import akka.event.{ActorClassifier, ActorEventBus, ManagedActorClassification} 5 | import pea.app.actor.WorkerMonitorActor.{MonitorMessage, MonitorSubscriberMessage, WorkerMonitorBus} 6 | import pea.app.gatling.PeaDataWriter.MonitorData 7 | import pea.common.actor.BaseActor 8 | 9 | /** 10 | * monitor user and request counts 11 | */ 12 | class WorkerMonitorActor extends BaseActor { 13 | 14 | val monitorBus = new WorkerMonitorBus(context.system) 15 | 16 | override def receive: Receive = { 17 | case MonitorSubscriberMessage(ref) => 18 | monitorBus.subscribe(ref, self) 19 | case data: MonitorData => 20 | monitorBus.publish(MonitorMessage(self, data)) 21 | case message: Any => 22 | log.warning(s"Unknown message type ${message}") 23 | } 24 | } 25 | 26 | object WorkerMonitorActor { 27 | 28 | def props() = Props(new WorkerMonitorActor()) 29 | 30 | case class MonitorSubscriberMessage(ref: ActorRef) 31 | 32 | case class MonitorMessage(ref: ActorRef, data: MonitorData) 33 | 34 | class WorkerMonitorBus(val system: ActorSystem) extends ActorEventBus with ActorClassifier with ManagedActorClassification { 35 | 36 | override type Event = MonitorMessage 37 | 38 | override protected def classify(event: MonitorMessage): ActorRef = event.ref 39 | 40 | override protected def mapSize: Int = 1 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /app/pea/app/api/CommonChecks.scala: -------------------------------------------------------------------------------- 1 | package pea.app.api 2 | 3 | import pea.app.PeaConfig 4 | import pea.common.util.StringUtils 5 | import play.api.mvc.Result 6 | 7 | import scala.concurrent.Future 8 | 9 | trait CommonChecks extends CommonFunctions { 10 | 11 | def checkWorkerEnable(func: => Future[Result]): Future[Result] = { 12 | if (PeaConfig.enableWorker) { 13 | func 14 | } else { 15 | FutureErrorResult("Role worker is disabled") 16 | } 17 | } 18 | 19 | def checkReporterEnable(func: => Future[Result]): Future[Result] = { 20 | if (PeaConfig.enableReporter) { 21 | func 22 | } else { 23 | FutureErrorResult("Role reporter is disabled") 24 | } 25 | } 26 | 27 | def checkUserDataFolder(func: => Result): Result = { 28 | if (StringUtils.isNotEmpty(PeaConfig.resourcesFolder)) { 29 | func 30 | } else { 31 | ErrorResult("Config 'resources' is not set") 32 | } 33 | } 34 | 35 | def checkJarFolder(func: => Result): Result = { 36 | if (StringUtils.isNotEmpty(PeaConfig.compilerExtraClasspath)) { 37 | func 38 | } else { 39 | ErrorResult("Config 'classpath' is not set") 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/pea/app/api/CommonFunctions.scala: -------------------------------------------------------------------------------- 1 | package pea.app.api 2 | 3 | import pea.app.api.BaseApi.OkApiRes 4 | import pea.common.model.ApiResError 5 | import play.api.mvc.Result 6 | 7 | import scala.concurrent.Future 8 | 9 | trait CommonFunctions { 10 | 11 | object ErrorResult { 12 | def apply(msg: String): Result = { 13 | OkApiRes(ApiResError(msg)) 14 | } 15 | } 16 | 17 | object FutureErrorResult { 18 | def apply(msg: String): Future[Result] = { 19 | Future.successful(ErrorResult(msg)) 20 | } 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/pea/app/api/filters/SecurityFilters.scala: -------------------------------------------------------------------------------- 1 | package pea.app.api.filters 2 | 3 | import javax.inject.Inject 4 | import org.pac4j.play.filters.SecurityFilter 5 | import play.api.http.HttpFilters 6 | 7 | class SecurityFilters @Inject()(securityFilter: SecurityFilter) extends HttpFilters { 8 | 9 | def filters = Seq(securityFilter) 10 | 11 | } 12 | -------------------------------------------------------------------------------- /app/pea/app/compiler/CompileResponse.scala: -------------------------------------------------------------------------------- 1 | package pea.app.compiler 2 | 3 | case class CompileResponse(success: Boolean, errMsg: String = null, hasModified: Boolean = true) 4 | -------------------------------------------------------------------------------- /app/pea/app/compiler/CompilerConfiguration.scala: -------------------------------------------------------------------------------- 1 | package pea.app.compiler 2 | 3 | import java.nio.charset.StandardCharsets 4 | import java.nio.file.{Path, Paths} 5 | 6 | import pea.app.actor.CompilerActor.SyncCompileMessage 7 | 8 | case class CompilerConfiguration( 9 | simulationsDirectory: Path, 10 | binariesDirectory: Path, 11 | encoding: String = StandardCharsets.UTF_8.name(), 12 | extraScalacOptions: Seq[String] = Nil 13 | ) 14 | 15 | object CompilerConfiguration { 16 | 17 | def fromCompileMessage(message: SyncCompileMessage): CompilerConfiguration = { 18 | CompilerConfiguration( 19 | Paths.get(message.srcFolder).toAbsolutePath(), 20 | Paths.get(message.outputFolder).toAbsolutePath(), 21 | ) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/pea/app/gatling/PeaConfigKeys.scala: -------------------------------------------------------------------------------- 1 | package pea.app.gatling 2 | 3 | import io.gatling.core.ConfigKeys 4 | 5 | object PeaConfigKeys { 6 | 7 | val Writers = ConfigKeys.data.Writers 8 | val WritePeriod = "gatling.data.pea.writePeriod" 9 | } 10 | -------------------------------------------------------------------------------- /app/pea/app/gatling/PeaDataWriterTypes.scala: -------------------------------------------------------------------------------- 1 | package pea.app.gatling 2 | 3 | object PeaDataWriterTypes { 4 | 5 | private val AllTypes = Seq( 6 | PeaDataWriterType, 7 | ConsoleDataWriterType, 8 | FileDataWriterType, 9 | GraphiteDataWriterType, 10 | LeakReporterDataWriterType 11 | ).map(t => t.name -> t).toMap 12 | 13 | def findByName(name: String): Option[DataWriterType] = AllTypes.get(name) 14 | 15 | sealed abstract class DataWriterType(val name: String, val className: String) 16 | 17 | object PeaDataWriterType extends DataWriterType("pea", "pea.app.gatling.PeaDataWriter") 18 | 19 | object ConsoleDataWriterType extends DataWriterType("console", "io.gatling.core.stats.writer.ConsoleDataWriter") 20 | 21 | object FileDataWriterType extends DataWriterType("file", "io.gatling.core.stats.writer.LogFileDataWriter") 22 | 23 | object GraphiteDataWriterType extends DataWriterType("graphite", "io.gatling.graphite.GraphiteDataWriter") 24 | 25 | object LeakReporterDataWriterType extends DataWriterType("leak", "io.gatling.core.stats.writer.LeakReporterDataWriter") 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/pea/app/gatling/PeaRequestStatistics.scala: -------------------------------------------------------------------------------- 1 | package pea.app.gatling 2 | 3 | import pea.app.gatling.PeaRequestStatistics.{PeaGroupedCount, PeaStatistics} 4 | 5 | case class PeaRequestStatistics( 6 | name: String, 7 | path: String, 8 | numberOfRequestsStatistics: PeaStatistics[Long], 9 | minResponseTimeStatistics: PeaStatistics[Int], 10 | maxResponseTimeStatistics: PeaStatistics[Int], 11 | meanStatistics: PeaStatistics[Int], 12 | stdDeviationStatistics: PeaStatistics[Int], 13 | percentiles1: PeaStatistics[Int], 14 | percentiles2: PeaStatistics[Int], 15 | percentiles3: PeaStatistics[Int], 16 | percentiles4: PeaStatistics[Int], 17 | groupedCounts: Seq[PeaGroupedCount], 18 | meanNumberOfRequestsPerSecondStatistics: PeaStatistics[Double] 19 | ) 20 | 21 | object PeaRequestStatistics { 22 | 23 | final case class PeaGroupedCount(name: String, count: Long, total: Long) { 24 | val percentage: Int = if (total == 0) 0 else (count.toDouble / total * 100).round.toInt 25 | } 26 | 27 | final case class PeaStatistics[T: Numeric](name: String, total: T, success: T, failure: T) { 28 | def all = List(total, success, failure) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /app/pea/app/gatling/PeaSimulation.scala: -------------------------------------------------------------------------------- 1 | package pea.app.gatling 2 | 3 | import io.gatling.core.Predef.Simulation 4 | 5 | abstract class PeaSimulation extends Simulation { 6 | 7 | val description: String 8 | } 9 | -------------------------------------------------------------------------------- /app/pea/app/hook/ErrorHandler.scala: -------------------------------------------------------------------------------- 1 | package pea.app.hook 2 | 3 | import javax.inject.{Inject, Singleton} 4 | import org.slf4j.LoggerFactory 5 | import pea.app.api.BaseApi.OkApiRes 6 | import pea.common.exceptions.ErrorMessages 7 | import pea.common.exceptions.ErrorMessages.ErrorMessageException 8 | import pea.common.model.{ApiCode, ApiRes, ApiResError} 9 | import pea.common.util.{LogUtils, StringUtils} 10 | import play.api.http.HttpErrorHandler 11 | import play.api.i18n.{Langs, MessagesApi} 12 | import play.api.mvc.{RequestHeader, Result} 13 | 14 | import scala.concurrent.Future 15 | 16 | @Singleton 17 | class ErrorHandler @Inject()(messagesApi: MessagesApi, langs: Langs) extends HttpErrorHandler with ErrorMessages { 18 | 19 | lazy val logger = LoggerFactory.getLogger(classOf[ErrorHandler]) 20 | 21 | override def onClientError(request: RequestHeader, statusCode: Int, message: String): Future[Result] = { 22 | val msg = s""""${request.method} ${request.uri}" ${statusCode} ${if (StringUtils.isNotEmpty(message)) message else ""}""" 23 | Future.successful(OkApiRes(ApiResError(msg))) 24 | } 25 | 26 | override def onServerError(request: RequestHeader, exception: Throwable): Future[Result] = { 27 | val logStack = LogUtils.stackTraceToString(exception) 28 | logger.warn(logStack) 29 | val requestLocal = request.headers.get("Local") 30 | implicit val lang = if (requestLocal.nonEmpty) { 31 | langs.availables.find(_.code == requestLocal.get).getOrElse(langs.availables.head) 32 | } else { 33 | langs.availables.head 34 | } 35 | exception match { 36 | case errMsgException: ErrorMessageException => 37 | val errMsg = messagesApi(errMsgException.error.name, errMsgException.error.errMsg) 38 | Future.successful(OkApiRes(ApiRes(code = ApiCode.ERROR, msg = errMsg, data = logStack))) 39 | case _ => 40 | val message = if (StringUtils.isNotEmpty(exception.getMessage)) { 41 | exception.getMessage 42 | } else { 43 | messagesApi(error_ServerError.name) 44 | } 45 | Future.successful(OkApiRes(ApiRes(code = ApiCode.ERROR, msg = message, data = logStack))) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/pea/app/http/HostHttpSource.scala: -------------------------------------------------------------------------------- 1 | package pea.app.http 2 | 3 | import akka.http.scaladsl.Http 4 | import akka.http.scaladsl.model.{HttpRequest, HttpResponse} 5 | import akka.stream.scaladsl.{Flow, Sink, Source} 6 | import pea.app.PeaConfig._ 7 | 8 | import scala.concurrent.Future 9 | 10 | // connection level http cline, no pool and cache 11 | // https://doc.akka.io/docs/akka-http/current/client-side/connection-level.html 12 | class HostHttpSource(host: String, port: Int) { 13 | 14 | private val connFlow: Flow[HttpRequest, HttpResponse, Future[Http.OutgoingConnection]] = 15 | Http().outgoingConnection(host = host, port = port) 16 | 17 | def execute(request: HttpRequest): Future[HttpResponse] = { 18 | val responseFuture: Future[HttpResponse] = 19 | Source.single(request) 20 | .via(connFlow) 21 | .runWith(Sink.head) 22 | responseFuture 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/pea/app/http/HttpClient.scala: -------------------------------------------------------------------------------- 1 | package pea.app.http 2 | 3 | import pea.app.PeaConfig._ 4 | import play.api.libs.ws.ahc.StandaloneAhcWSClient 5 | 6 | object HttpClient { 7 | 8 | lazy val wsClient = StandaloneAhcWSClient() 9 | 10 | def close(): Unit = { 11 | if (null != wsClient) { 12 | wsClient.close() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/pea/app/model/DownloadResourceRequest.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | case class DownloadResourceRequest( 4 | url: String, 5 | file: String, 6 | ) 7 | -------------------------------------------------------------------------------- /app/pea/app/model/FinishedCallbackRequest.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.params 2 | 3 | case class FinishedCallbackRequest( 4 | url: String, 5 | ext: Any, 6 | ) 7 | -------------------------------------------------------------------------------- /app/pea/app/model/FinishedCallbackResponse.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | import pea.app.gatling.PeaRequestStatistics 4 | 5 | case class FinishedCallbackResponse( 6 | runId: String, 7 | start: Long, 8 | end: Long, 9 | code: Int, 10 | errMsg: String = null, 11 | statistics: PeaRequestStatistics = null, 12 | ext: Any = null, 13 | ) 14 | -------------------------------------------------------------------------------- /app/pea/app/model/Injection.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | import pea.app.model.params.DurationParam 4 | 5 | // https://gatling.io/docs/current/general/simulation_setup 6 | case class Injection( 7 | var `type`: String, 8 | var users: Int, 9 | var from: Int = 0, 10 | var to: Int = 0, 11 | var duration: DurationParam = null, 12 | var times: Int = 0, 13 | var eachLevelLasting: DurationParam = null, 14 | var separatedByRampsLasting: DurationParam = null, 15 | ) 16 | 17 | object Injection { 18 | 19 | // Open Model 20 | val TYPE_NOTHING_FOR = "nothingFor" 21 | val TYPE_AT_ONCE_USERS = "atOnceUsers" 22 | val TYPE_RAMP_USERS = "rampUsers" 23 | val TYPE_CONSTANT_USERS_PER_SEC = "constantUsersPerSec" 24 | val TYPE_RAMP_USERS_PER_SEC = "rampUsersPerSec" 25 | val TYPE_HEAVISIDE_USERS = "heavisideUsers" 26 | val TYPE_INCREMENT_USERS_PER_SEC = "incrementUsersPerSec" // meta 27 | 28 | // Close Model 29 | val TYPE_CONSTANT_CONCURRENT_USERS = "constantConcurrentUsers" 30 | val TYPE_RAMP_CONCURRENT_USERS = "rampConcurrentUsers" 31 | val TYPE_INCREMENT_CONCURRENT_USERS = "incrementConcurrentUsers" // meta 32 | } 33 | -------------------------------------------------------------------------------- /app/pea/app/model/LoadJob.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | import pea.app.model.params.FinishedCallbackRequest 4 | 5 | trait LoadJob { 6 | 7 | val `type`: String 8 | val workers: Seq[PeaMember] = null // for each worker has the same job 9 | val load: LoadMessage = null // for each worker has the same job 10 | val jobs: Seq[SingleJob] = null // each worker has itself job 11 | 12 | var report: Boolean = true 13 | var simulationId: String = null 14 | var start: Long = 0L 15 | var callback: FinishedCallbackRequest = null 16 | var ext: Any = null // any additional information 17 | } 18 | -------------------------------------------------------------------------------- /app/pea/app/model/LoadMessage.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | trait LoadMessage { 4 | 5 | var simulationId: String 6 | var start: Long 7 | var report: Boolean 8 | // should print request and response detail 9 | var verbose: Boolean = false 10 | // load type 11 | val `type`: String 12 | 13 | def isValid(): Exception 14 | } 15 | -------------------------------------------------------------------------------- /app/pea/app/model/LoadTypes.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | object LoadTypes { 4 | 5 | val SINGLE = "single" 6 | val SCRIPT = "script" 7 | val PROGRAM = "program" 8 | } 9 | -------------------------------------------------------------------------------- /app/pea/app/model/MemberStatus.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | import pea.app.PeaConfig 4 | import pea.common.util.StringUtils 5 | 6 | /** node data 7 | * 8 | * @param status node status 9 | * @param runId report id of last job 10 | * @param start start time of last job 11 | * @param end end time of last job 12 | * @param code code of last job 13 | * @param errMsg error message of last job 14 | * @param label label 15 | * @param oshi operating system and hardware information 16 | */ 17 | case class MemberStatus( 18 | var status: String = MemberStatus.WORKER_IDLE, 19 | var runId: String = StringUtils.EMPTY, 20 | var start: Long = 0L, 21 | var end: Long = 0L, 22 | var code: Int = 0, 23 | var errMsg: String = null, 24 | var label: String = PeaConfig.label, 25 | var oshi: OshiInfo = OshiInfo.getOshiInfo(), 26 | ) 27 | 28 | object MemberStatus { 29 | 30 | val WORKER_IDLE = "idle" 31 | val WORKER_RUNNING = "running" 32 | 33 | val REPORTER_RUNNING = WORKER_RUNNING 34 | val REPORTER_REPORTING = "reporting" 35 | val REPORTER_FINISHED = "finished" 36 | 37 | // extra worker status in reporter 38 | val REPORTER_WORKER_IIL = "ill" 39 | val REPORTER_WORKER_GATHERING = "gathering" 40 | val REPORTER_WORKER_FINISHED = REPORTER_FINISHED 41 | 42 | def isWorkerOver(status: String): Boolean = { 43 | REPORTER_WORKER_IIL.equals(status) || REPORTER_WORKER_FINISHED.equals(status) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/pea/app/model/OshiInfo.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty 4 | import oshi.SystemInfo 5 | 6 | case class OshiInfo( 7 | os: String, 8 | @JsonProperty("memory.total") 9 | memoryTotal: Long, 10 | @JsonProperty("memory.available") 11 | memoryAvailable: Long, 12 | @JsonProperty("cpu.name") 13 | cpuName: String, 14 | @JsonProperty("cpu.physical.processor.count") 15 | cpuPhysicalProcessorCount: Int, 16 | @JsonProperty("cpu.logical.processor.count") 17 | cpuLogicalProcessorCount: Int, 18 | ) 19 | 20 | object OshiInfo { 21 | 22 | def getOshiInfo(): OshiInfo = { 23 | val si = new SystemInfo() 24 | val os = si.getOperatingSystem 25 | val hardware = si.getHardware 26 | val memory = hardware.getMemory 27 | val cpu = hardware.getProcessor 28 | OshiInfo( 29 | os = os.toString, 30 | memoryTotal = memory.getTotal, 31 | memoryAvailable = memory.getAvailable, 32 | cpuName = cpu.getName, 33 | cpuPhysicalProcessorCount = cpu.getPhysicalProcessorCount, 34 | cpuLogicalProcessorCount = cpu.getLogicalProcessorCount, 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/pea/app/model/PeaMember.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | import java.net.URI 4 | 5 | import com.typesafe.scalalogging.Logger 6 | import pea.app.PeaConfig 7 | import pea.common.util.{LogUtils, StringUtils} 8 | 9 | import scala.collection.mutable 10 | 11 | case class PeaMember( 12 | address: String, 13 | port: Int, 14 | hostname: String, 15 | ) { 16 | 17 | def toNodeName: String = PeaMember.toNodeName(address, port, hostname) 18 | 19 | def toAddress: String = PeaMember.toAddress(address, port) 20 | } 21 | 22 | object PeaMember { 23 | 24 | val logger = Logger("PeaMember") 25 | 26 | def apply(uriWithoutScheme: String): PeaMember = { 27 | try { 28 | val uri = URI.create(s"${PeaConfig.DEFAULT_SCHEME}://${uriWithoutScheme}") 29 | val queryMap = mutable.Map[String, String]() 30 | uri.getQuery.split("&").foreach(paramStr => { 31 | val param = paramStr.split("=") 32 | if (param.length == 2) { 33 | queryMap += (param(0) -> param(1)) 34 | } 35 | }) 36 | PeaMember(uri.getHost, uri.getPort, queryMap.getOrElse("hostname", StringUtils.EMPTY)) 37 | } catch { 38 | case t: Throwable => 39 | logger.warn(LogUtils.stackTraceToString(t)) 40 | null 41 | } 42 | } 43 | 44 | def toNodeName(address: String, port: Int, hostname: String): String = { 45 | s"${address}:${port}?hostname=${hostname}" 46 | } 47 | 48 | def toAddress(address: String, port: Int) = s"${address}:${port}" 49 | } 50 | -------------------------------------------------------------------------------- /app/pea/app/model/ReporterJobStatus.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | import pea.app.model.ReporterJobStatus.JobWorkerStatus 4 | import pea.common.util.StringUtils 5 | 6 | import scala.collection.mutable 7 | 8 | case class ReporterJobStatus( 9 | var status: String = MemberStatus.REPORTER_FINISHED, 10 | var runId: String = StringUtils.EMPTY, 11 | var start: Long = 0L, 12 | var end: Long = 0L, 13 | var workers: mutable.Map[String, JobWorkerStatus] = mutable.Map.empty, 14 | var load: Any = null, // any for jackson 15 | ) 16 | 17 | object ReporterJobStatus { 18 | 19 | case class JobWorkerStatus( 20 | status: String = MemberStatus.WORKER_IDLE, 21 | errMsg: String = null, 22 | ) 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/pea/app/model/ResourceModels.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | object ResourceModels { 4 | 5 | case class ResourceCheckRequest( 6 | file: String, // relative path to `user-data` files 7 | ) 8 | 9 | case class ResourceInfo( 10 | exists: Boolean, 11 | isDirectory: Boolean, 12 | size: Long = 0L, 13 | modified: Long = 0L, 14 | md5: String = null, 15 | filename: String = null, 16 | ) 17 | 18 | case class NewFolder( 19 | path: String, 20 | name: String, 21 | ) 22 | 23 | } 24 | -------------------------------------------------------------------------------- /app/pea/app/model/Role.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | object Role { 4 | 5 | val WORKER = "worker" 6 | val REPORTER = "reporter" 7 | } 8 | -------------------------------------------------------------------------------- /app/pea/app/model/SimulationModel.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | import pea.common.util.StringUtils 4 | 5 | case class SimulationModel( 6 | name: String, 7 | protocols: Seq[String], 8 | description: String = StringUtils.EMPTY 9 | ) 10 | -------------------------------------------------------------------------------- /app/pea/app/model/SingleJob.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | trait SingleJob { 4 | val worker: PeaMember 5 | val load: LoadMessage 6 | } 7 | -------------------------------------------------------------------------------- /app/pea/app/model/SingleRequest.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | import pea.common.util.StringUtils 4 | 5 | case class SingleRequest( 6 | var name: String, 7 | var url: String, 8 | var method: String, 9 | var headers: Map[String, String], 10 | var body: String, 11 | ) { 12 | 13 | def getHeaders(): Map[String, String] = { 14 | if (null == headers) Map.empty else headers 15 | } 16 | 17 | def getBody(): String = { 18 | StringUtils.notEmptyElse(body, StringUtils.EMPTY) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/pea/app/model/WorkersCompileRequest.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | case class WorkersCompileRequest( 4 | workers: Seq[PeaMember], 5 | pull: Boolean, 6 | ) 7 | -------------------------------------------------------------------------------- /app/pea/app/model/WorkersRequest.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model 2 | 3 | case class WorkersRequest(workers: Seq[PeaMember]) 4 | -------------------------------------------------------------------------------- /app/pea/app/model/job/RunProgramMessage.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.job 2 | 3 | import pea.app.model.{LoadMessage, LoadTypes} 4 | import pea.common.util.StringUtils 5 | 6 | case class RunProgramMessage( 7 | var program: String, 8 | var simulationId: String = null, 9 | var start: Long = 0L, 10 | ) extends LoadMessage { 11 | 12 | val `type`: String = LoadTypes.PROGRAM 13 | var report: Boolean = true 14 | var reportStdout: Boolean = false 15 | var reportStderr: Boolean = true 16 | 17 | def isValid(): Exception = { 18 | if (StringUtils.isEmpty(program)) { 19 | new RuntimeException("Empty program") 20 | } else { 21 | null 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/pea/app/model/job/RunProgramSingleJob.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.job 2 | 3 | import pea.app.model.{PeaMember, SingleJob} 4 | 5 | case class RunProgramSingleJob( 6 | worker: PeaMember, 7 | load: RunProgramMessage 8 | ) extends SingleJob 9 | -------------------------------------------------------------------------------- /app/pea/app/model/job/RunScriptMessage.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.job 2 | 3 | import pea.app.model.{LoadMessage, LoadTypes} 4 | import pea.common.util.StringUtils 5 | 6 | case class RunScriptMessage( 7 | var simulation: String, 8 | var report: Boolean = true, 9 | var simulationId: String = null, 10 | var start: Long = 0L 11 | ) extends LoadMessage { 12 | 13 | val `type`: String = LoadTypes.SCRIPT 14 | 15 | def isValid(): Exception = { 16 | if (StringUtils.isEmpty(simulation)) { 17 | new RuntimeException("Empty simulation class") 18 | } else { 19 | null 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/pea/app/model/job/RunScriptSingleJob.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.job 2 | 3 | import pea.app.model.{PeaMember, SingleJob} 4 | 5 | case class RunScriptSingleJob( 6 | worker: PeaMember, 7 | load: RunScriptMessage 8 | ) extends SingleJob 9 | -------------------------------------------------------------------------------- /app/pea/app/model/job/SingleHttpScenarioMessage.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.job 2 | 3 | import pea.app.model.params._ 4 | import pea.app.model.{Injection, LoadMessage, LoadTypes, SingleRequest} 5 | import pea.common.util.StringUtils 6 | 7 | case class SingleHttpScenarioMessage( 8 | var name: String, 9 | var request: SingleRequest, 10 | var injections: Seq[Injection], 11 | var report: Boolean = true, 12 | var simulationId: String = null, 13 | var start: Long = 0L, 14 | var feeder: FeederParam = null, 15 | var loop: LoopParam = null, 16 | var maxDuration: DurationParam = null, 17 | var assertions: HttpAssertionParam = null, 18 | var throttle: ThrottleParam = null, 19 | ) extends LoadMessage { 20 | 21 | val `type`: String = LoadTypes.SINGLE 22 | 23 | def isValid(): Exception = { 24 | if (null == request || StringUtils.isEmpty(request.url)) { 25 | new RuntimeException("Empty request") 26 | } else if (null == injections || injections.isEmpty) { 27 | new RuntimeException("Empty injections") 28 | } else { 29 | null 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/pea/app/model/job/SingleHttpScenarioSingleJob.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.job 2 | 3 | import pea.app.model.{PeaMember, SingleJob} 4 | 5 | case class SingleHttpScenarioSingleJob( 6 | worker: PeaMember, 7 | load: SingleHttpScenarioMessage 8 | ) extends SingleJob 9 | -------------------------------------------------------------------------------- /app/pea/app/model/params/AssertionItem.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.params 2 | 3 | /** 4 | * @param op operation type 5 | * @param path selection expression to get expect value of response, eg. jsonpath, header key 6 | * @param expect response expect value 7 | */ 8 | case class AssertionItem( 9 | op: String, 10 | path: String, 11 | expect: Any, 12 | ) 13 | 14 | object AssertionItem { 15 | val TYPE_EQ = "eq" 16 | val TYPE_JSONPATH = "jsonpath" 17 | } 18 | -------------------------------------------------------------------------------- /app/pea/app/model/params/AssertionsParam.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.params 2 | 3 | case class AssertionsParam( 4 | list: Seq[AssertionItem], 5 | ) 6 | -------------------------------------------------------------------------------- /app/pea/app/model/params/DurationParam.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.params 2 | 3 | case class DurationParam( 4 | value: Int, 5 | unit: String, 6 | ) 7 | 8 | object DurationParam { 9 | 10 | val TIME_UNIT_MILLI = "milli" 11 | val TIME_UNIT_SECOND = "second" 12 | val TIME_UNIT_MINUTE = "minute" 13 | val TIME_UNIT_HOUR = "hour" 14 | } 15 | -------------------------------------------------------------------------------- /app/pea/app/model/params/FeederParam.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.params 2 | 3 | case class FeederParam( 4 | `type`: String, 5 | path: String, 6 | ) 7 | 8 | object FeederParam { 9 | 10 | val TYPE_CSV = "csv" 11 | val TYPE_JSON = "json" 12 | } 13 | -------------------------------------------------------------------------------- /app/pea/app/model/params/HttpAssertionParam.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.params 2 | 3 | case class HttpAssertionParam( 4 | status: AssertionsParam, 5 | header: AssertionsParam, 6 | body: AssertionsParam, 7 | ) 8 | -------------------------------------------------------------------------------- /app/pea/app/model/params/LoopParam.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.params 2 | 3 | case class LoopParam( 4 | forever: Boolean = false, 5 | repeat: Int = 0, 6 | ) 7 | -------------------------------------------------------------------------------- /app/pea/app/model/params/ThrottleParam.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.params 2 | 3 | case class ThrottleParam( 4 | steps: Seq[ThrottleStep] 5 | ) 6 | -------------------------------------------------------------------------------- /app/pea/app/model/params/ThrottleStep.scala: -------------------------------------------------------------------------------- 1 | package pea.app.model.params 2 | 3 | case class ThrottleStep( 4 | `type`: String, 5 | rps: Int, 6 | duration: DurationParam = null, 7 | ) 8 | 9 | object ThrottleStep { 10 | 11 | val TYPE_REACH = "reach" 12 | val TYPE_HOLD = "hold" 13 | val TYPE_JUMP = "jump" 14 | } 15 | -------------------------------------------------------------------------------- /app/pea/app/modules/ApplicationStartModule.scala: -------------------------------------------------------------------------------- 1 | package pea.app.modules 2 | 3 | import com.google.inject.AbstractModule 4 | import pea.app.hook.ApplicationStart 5 | 6 | class ApplicationStartModule extends AbstractModule { 7 | 8 | override def configure(): Unit = { 9 | bind(classOf[ApplicationStart]).asEagerSingleton() 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/pea/app/modules/BasicSecurityModule.scala: -------------------------------------------------------------------------------- 1 | package pea.app.modules 2 | 3 | import com.google.inject.AbstractModule 4 | import org.pac4j.play.LogoutController 5 | import org.pac4j.play.scala.{DefaultSecurityComponents, SecurityComponents} 6 | import org.pac4j.play.store.{PlayCacheSessionStore, PlaySessionStore} 7 | import play.api.{Configuration, Environment} 8 | 9 | class BasicSecurityModule(environment: Environment, configuration: Configuration) extends AbstractModule { 10 | override def configure(): Unit = { 11 | bind(classOf[PlaySessionStore]).to(classOf[PlayCacheSessionStore]) 12 | // logout 13 | val logoutController = new LogoutController() 14 | logoutController.setDestroySession(true) 15 | logoutController.setLocalLogout(true) 16 | logoutController.setDefaultUrl("/") 17 | bind(classOf[LogoutController]).toInstance(logoutController) 18 | // security components used in controllers 19 | bind(classOf[SecurityComponents]).to(classOf[DefaultSecurityComponents]) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/pea/app/package.scala: -------------------------------------------------------------------------------- 1 | package pea 2 | 3 | import pea.app.model.job.SingleHttpScenarioMessage 4 | 5 | package object app { 6 | var singleHttpScenario: SingleHttpScenarioMessage = null 7 | } 8 | -------------------------------------------------------------------------------- /app/pea/app/service/NotifyService.scala: -------------------------------------------------------------------------------- 1 | package pea.app.service 2 | 3 | import com.typesafe.scalalogging.StrictLogging 4 | import pea.app.PeaConfig.dispatcher 5 | import pea.app.http.HttpClient 6 | import pea.app.model.FinishedCallbackResponse 7 | import pea.app.model.params.FinishedCallbackRequest 8 | import pea.common.util.{JsonUtils, LogUtils} 9 | 10 | object NotifyService extends StrictLogging { 11 | 12 | // ignore response 13 | def gatlingResultCallback(request: FinishedCallbackRequest, response: FinishedCallbackResponse) = { 14 | val strBody = JsonUtils.stringify(response) 15 | logger.debug(s"Notify ${request.url} with response: ${strBody}") 16 | HttpClient.wsClient 17 | .url(request.url) 18 | .post(strBody) 19 | .recover { 20 | case t: Throwable => 21 | logger.warn(s"Send callback(${request.url}) error: ${LogUtils.stackTraceToString(t)}") 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/pea/app/service/ResourceService.scala: -------------------------------------------------------------------------------- 1 | package pea.app.service 2 | 3 | import java.io.File 4 | import java.nio.file.{Files, Paths, StandardOpenOption} 5 | 6 | import akka.stream.scaladsl.Sink 7 | import akka.util.ByteString 8 | import com.typesafe.scalalogging.Logger 9 | import pea.app.PeaConfig 10 | import pea.app.PeaConfig._ 11 | import pea.app.http.HttpClient 12 | import pea.app.model.DownloadResourceRequest 13 | 14 | import scala.concurrent.Future 15 | 16 | object ResourceService { 17 | 18 | val logger = Logger(getClass) 19 | 20 | def downloadResource(request: DownloadResourceRequest): Future[File] = { 21 | HttpClient.wsClient 22 | .url(request.url) 23 | .withMethod("GET") 24 | .stream() 25 | .flatMap(res => { 26 | val file = new File(s"${PeaConfig.resourcesFolder}${File.separator}${request.file}") 27 | if (file.getCanonicalPath.startsWith(PeaConfig.resourcesFolder)) { 28 | Files.createDirectories(Paths.get(file.getParent)) 29 | val os = Files.newOutputStream(file.toPath, StandardOpenOption.CREATE, StandardOpenOption.WRITE) 30 | val sink = Sink.foreach[ByteString] { bytes => 31 | os.write(bytes.toArray) 32 | } 33 | res.bodyAsSource 34 | .runWith(sink) 35 | .andThen { case result => 36 | os.close() 37 | result.get 38 | } 39 | .map(_ => file) 40 | } else { 41 | Future.failed(new RuntimeException(s"Resource file must be in ${PeaConfig.resourcesFolder}")) 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/pea/app/util/FileUtils.scala: -------------------------------------------------------------------------------- 1 | package pea.app.util 2 | 3 | import java.io.{File, RandomAccessFile} 4 | 5 | object FileUtils { 6 | 7 | def readHead1K(file: File): String = { 8 | val bytes = Array.fill[Byte](1024)(0) 9 | val access = new RandomAccessFile(file, "r") 10 | try { 11 | if (file.length() <= 1024) { 12 | access.readFully(bytes, 0, file.length().toInt) 13 | new String(bytes, 0, file.length().toInt) 14 | } else { 15 | access.readFully(bytes, 0, 1024) 16 | new String(bytes) 17 | } 18 | } finally { 19 | access.close() 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/pea/app/util/SimulationLogUtils.scala: -------------------------------------------------------------------------------- 1 | package pea.app.util 2 | 3 | import java.io.File 4 | 5 | import pea.app.PeaConfig 6 | 7 | object SimulationLogUtils { 8 | 9 | def simulationLogFile(runId: String): String = { 10 | s"${PeaConfig.resultsFolder}${File.separator}${runId}${File.separator}${PeaConfig.SIMULATION_LOG_FILE}" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import Dependencies._ 2 | 3 | lazy val pea = Project("pea", file(".")) 4 | .enablePlugins(PlayScala) 5 | .settings(commonSettings: _*) 6 | .settings(publishSettings: _*) 7 | .dependsOn( 8 | peaCommon % "compile->compile;test->test", 9 | peaDubbo % "compile->compile;test->test", 10 | peaGrpc % "compile->compile;test->test", 11 | ).aggregate(peaCommon, peaDubbo, peaGrpc) 12 | 13 | // pea-app dependencies 14 | val gatlingVersion = "3.3.1" 15 | val gatling = "io.gatling.highcharts" % "gatling-charts-highcharts" % gatlingVersion exclude("io.gatling", "gatling-app") 16 | val gatlingCompiler = "io.gatling" % "gatling-compiler" % gatlingVersion 17 | val curator = "org.apache.curator" % "curator-recipes" % "2.12.0" 18 | val oshiCore = "com.github.oshi" % "oshi-core" % "4.0.0" 19 | 20 | libraryDependencies ++= Seq(akkaStream, gatling, gatlingCompiler, curator, oshiCore) ++ appPlayDeps 21 | libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "4.0.3" % Test 22 | 23 | // pea-common 24 | lazy val peaCommon = subProject("pea-common") 25 | .settings(libraryDependencies ++= commonDependencies) 26 | 27 | // pea-dubbo 28 | val dubbo = "org.apache.dubbo" % "dubbo" % "2.7.4.1" 29 | lazy val peaDubbo = subProject("pea-dubbo") 30 | .settings(libraryDependencies ++= Seq( 31 | gatling, dubbo, 32 | )) 33 | 34 | // pea-grpc 35 | val grpcVersion = "1.22.2" // override 1.8, com.trueaccord.scalapb.compiler.Version.grpcJavaVersion 36 | val grpcNetty = "io.grpc" % "grpc-netty" % grpcVersion exclude("io.netty", "netty-codec-http2") // be compatible with gatling(4.1.42.Final) 37 | val scalapbRuntime = "com.trueaccord.scalapb" %% "scalapb-runtime-grpc" % com.trueaccord.scalapb.compiler.Version.scalapbVersion 38 | // Override the version that scalapb depends on. This adds an explicit dependency on 39 | // protobuf-java. This will cause sbt to evict the older version that is used by 40 | // scalapb-runtime. 41 | val protobuf = "com.google.protobuf" % "protobuf-java" % "3.7.0" 42 | lazy val peaGrpc = subProject("pea-grpc") 43 | .settings(libraryDependencies ++= Seq( 44 | gatling, grpcNetty, scalapbRuntime, protobuf 45 | )) 46 | 47 | // options: https://github.com/thesamet/sbt-protoc 48 | PB.protoSources in Compile := Seq( 49 | baseDirectory.value / "test/protobuf" 50 | ) 51 | PB.targets in Compile := Seq( 52 | scalapb.gen(grpc = true) -> baseDirectory.value / "test-generated" 53 | ) 54 | unmanagedSourceDirectories in Compile += baseDirectory.value / "test-generated" 55 | sourceGenerators in Compile -= (PB.generate in Compile).taskValue 56 | 57 | coverageEnabled := false 58 | -------------------------------------------------------------------------------- /conf/application.conf: -------------------------------------------------------------------------------- 1 | # https://www.playframework.com/documentation/latest/Configuration 2 | include "framework.conf" 3 | 4 | play.http.secret.key = "wuae_/xG6QUxPLWvXneCm8TH:b]Ki`?Hm0mOom`uahFh3xgTg8[9R_dfjCdpkVPG" 5 | play.http.secret.key = ${?APPLICATION_SECRET} 6 | 7 | pea { 8 | address = ${?ADDRESS} 9 | port = ${?PORT} 10 | label = "Pea" 11 | label = ${?LABEL} 12 | simulations { 13 | compileAtStartup = true 14 | webEditorBaseUrl = "https://github.com/asura-pro/pea-simulations/blob/master/src/main/scala/" 15 | webEditorBaseUrl = ${?WEB_EDITOR_BASE} 16 | } 17 | results { 18 | folder = "./logs" 19 | folder = ${?RESULTS_FOLDER} 20 | report { 21 | logo.href = "https://github.com/asura-pro/pea" 22 | desc.href = "https://github.com/asura-pro/pea" 23 | desc.content = "https://github.com/asura-pro/pea" 24 | } 25 | } 26 | zk { 27 | enabled = true 28 | role.worker = true 29 | role.worker = ${?ROLE_WORKER} 30 | role.reporter = true 31 | role.reporter = ${?ROLE_REPORTER} 32 | path = "/pea" 33 | connectString = "localhost:2181" 34 | username = "" 35 | password = "" 36 | } 37 | worker { 38 | protocol = "http" 39 | // simulation source folder 40 | source = "./test/simulations" 41 | source = ${?SIMULATIONS_SOURCE} 42 | // simulation compile output folder 43 | output = "./logs/output" 44 | output = ${?SIMULATIONS_OUTPUT} 45 | resources = "." 46 | resources = ${?RESOURCES_FOLDER} 47 | // external classpath for compiler and user libs 48 | classpath = "" 49 | classpath = ${?EXTRA_CLASSPATH} 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /conf/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | ${application.home:-.}/logs/application.log 9 | 10 | %date [%level] from %logger in %thread - %message%n%xException 11 | 12 | 13 | 14 | 15 | 16 | %coloredLevel %logger{15} - %message%n%xException{10} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /conf/messages: -------------------------------------------------------------------------------- 1 | # https://www.playframework.com/documentation/latest/ScalaI18N 2 | error_BusyStatus=节点工作中, 稍后重试 3 | -------------------------------------------------------------------------------- /conf/messages.en: -------------------------------------------------------------------------------- 1 | # https://www.playframework.com/documentation/latest/ScalaI18N 2 | error_BusyStatus=Node is busy, try later 3 | -------------------------------------------------------------------------------- /dist/bin/pea.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | START_SCRIPT=./bin/pea 4 | PID_FILE=./pea.pid 5 | 6 | export ROLE_WORKER=true 7 | export ROLE_REPORTER=true 8 | 9 | # *********************************************** 10 | # *********************************************** 11 | ARGS="-Dhttp.port=9000 -Dconfig.resource=application.conf" 12 | DAEMON=$START_SCRIPT 13 | 14 | # colors 15 | red='\e[0;31m' 16 | green='\e[0;32m' 17 | yellow='\e[0;33m' 18 | reset='\e[0m' 19 | 20 | echoRed() { echo -e "${red}$1${reset}"; } 21 | echoGreen() { echo -e "${green}$1${reset}"; } 22 | echoYellow() { echo -e "${yellow}$1${reset}"; } 23 | 24 | start() { 25 | PID=`$DAEMON $ARGS > /dev/null 2>&1 & echo $!` 26 | } 27 | 28 | case "$1" in 29 | start) 30 | if [ -f $PID_FILE ]; then 31 | PID=`cat $PID_FILE` 32 | if [ -z "`ps axf | grep -w ${PID} | grep -v grep`" ]; then 33 | start 34 | else 35 | echoYellow "Already running [$PID]" 36 | exit 0 37 | fi 38 | else 39 | start 40 | fi 41 | 42 | if [ -z $PID ]; then 43 | echoRed "Failed starting" 44 | exit 3 45 | else 46 | echo $PID > $PID_FILE 47 | echoGreen "Started [$PID]" 48 | exit 0 49 | fi 50 | ;; 51 | 52 | status) 53 | if [ -f $PID_FILE ]; then 54 | PID=`cat $PID_FILE` 55 | if [ -z "`ps axf | grep -w ${PID} | grep -v grep`" ]; then 56 | echoRed "Not running (process dead but pidfile exists)" 57 | exit 1 58 | else 59 | echoGreen "Running [$PID]" 60 | exit 0 61 | fi 62 | else 63 | echoRed "Not running" 64 | exit 3 65 | fi 66 | ;; 67 | 68 | stop) 69 | if [ -f $PID_FILE ]; then 70 | PID=`cat $PID_FILE` 71 | if [ -z "`ps axf | grep -w ${PID} | grep -v grep`" ]; then 72 | echoRed "Not running (process dead but pidfile exists)" 73 | exit 1 74 | else 75 | PID=`cat $PID_FILE` 76 | kill -HUP $PID 77 | echoGreen "Stopped [$PID]" 78 | rm -f $PID_FILE 79 | exit 0 80 | fi 81 | else 82 | echoRed "Not running (pid not found)" 83 | exit 3 84 | fi 85 | ;; 86 | 87 | restart) 88 | $0 stop 89 | $0 start 90 | ;; 91 | 92 | *) 93 | echo "Usage: $0 {status|start|stop|restart}" 94 | exit 1 95 | esac 96 | -------------------------------------------------------------------------------- /dist/ext/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/dist/ext/.gitkeep -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8 2 | COPY ./target/universal/pea /opt/pea 3 | EXPOSE 9000 4 | CMD /opt/pea/bin/pea \ 5 | -J-Xms128M -J-Xmx1048m \ 6 | -Dconfig.resource=application.conf \ 7 | -Dpidfile.path=/dev/null 8 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | cd .. 3 | sbt clean dist 4 | cd target/universal 5 | unzip pea-*.zip 6 | rm pea-*.zip 7 | mv pea-* pea 8 | cd ../../docker 9 | _tag=$1 10 | if [ -z "${_tag}" ]; then 11 | _tag=latest 12 | fi 13 | docker build --file ./Dockerfile -t "asurapro/pea:${_tag}" ../ 14 | docker push asurapro/pea 15 | -------------------------------------------------------------------------------- /images/banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/images/banner.jpeg -------------------------------------------------------------------------------- /images/report-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/images/report-01.png -------------------------------------------------------------------------------- /images/report-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/images/report-02.png -------------------------------------------------------------------------------- /images/shoot-01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/images/shoot-01.png -------------------------------------------------------------------------------- /images/shoot-job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/images/shoot-job.png -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/actor/ActorEvent.scala: -------------------------------------------------------------------------------- 1 | package pea.common.actor 2 | 3 | import pea.common.model.{ApiCode, ApiMsg} 4 | import pea.common.util.StringUtils 5 | 6 | case class ActorEvent( 7 | `type`: String = StringUtils.EMPTY, 8 | code: String = ApiCode.OK, 9 | msg: String = ApiMsg.SUCCESS, 10 | data: Any = null) 11 | 12 | object ActorEvent { 13 | val TYPE_INIT = "init" 14 | val TYPE_LIST = "list" 15 | val TYPE_ITEM = "item" 16 | val TYPE_OVER = "over" 17 | val TYPE_NOTIFY = "notify" 18 | val TYPE_ERROR = "error" 19 | } 20 | 21 | object InitActorEvent { 22 | def apply(): ActorEvent = new ActorEvent(`type` = ActorEvent.TYPE_INIT) 23 | } 24 | 25 | object OverActorEvent { 26 | def apply(data: Any): ActorEvent = new ActorEvent(`type` = ActorEvent.TYPE_OVER, data = data) 27 | } 28 | 29 | object ListActorEvent { 30 | def apply(data: Any): ActorEvent = new ActorEvent(`type` = ActorEvent.TYPE_LIST, data = data) 31 | } 32 | 33 | object ItemActorEvent { 34 | def apply(data: Any): ActorEvent = new ActorEvent(`type` = ActorEvent.TYPE_ITEM, data = data) 35 | } 36 | 37 | object NotifyActorEvent { 38 | def apply(msg: String): ActorEvent = new ActorEvent(`type` = ActorEvent.TYPE_NOTIFY, msg = msg) 39 | } 40 | 41 | object ErrorActorEvent { 42 | def apply(msg: String): ActorEvent = new ActorEvent(`type` = ActorEvent.TYPE_ERROR, code = ApiCode.ERROR, msg = msg) 43 | } 44 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/actor/BaseActor.scala: -------------------------------------------------------------------------------- 1 | package pea.common.actor 2 | 3 | import akka.actor.{Actor, ActorLogging} 4 | 5 | abstract class BaseActor extends Actor with ActorLogging { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/actor/SenderMessage.scala: -------------------------------------------------------------------------------- 1 | package pea.common.actor 2 | 3 | import akka.actor.ActorRef 4 | 5 | // message which wrap the sender actor 6 | case class SenderMessage(sender: ActorRef) 7 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/exceptions/ErrorMessages.scala: -------------------------------------------------------------------------------- 1 | package pea.common.exceptions 2 | 3 | import pea.common.exceptions.ErrorMessages.ErrorMessage 4 | 5 | import scala.concurrent.Future 6 | 7 | trait ErrorMessages { 8 | 9 | val error_ServerError = ErrorMessage("Server Error")("error_ServerError") 10 | val error_InvalidRequestParameters = ErrorMessage("Invalid request parameters")("error_InvalidRequestParameters") 11 | 12 | def error_Throwable(t: Throwable) = ErrorMessage(t.getMessage, t)("error_Throwable") 13 | 14 | def error_Msgs(msgs: Seq[String]) = ErrorMessage(msgs.mkString(","))("error_Msgs") 15 | 16 | def error_IllegalCharacter(msg: String) = ErrorMessage(msg)("error_IllegalCharacter") 17 | } 18 | 19 | object ErrorMessages { 20 | 21 | case class ErrorMessage(val errMsg: String, val t: Throwable = null)(_name: String) { 22 | 23 | def toException: ErrorMessageException = { 24 | ErrorMessageException(this) 25 | } 26 | 27 | def toFutureFail: Future[Nothing] = { 28 | Future.failed(ErrorMessageException(this)) 29 | } 30 | 31 | val name = _name 32 | } 33 | 34 | case class ErrorMessageException(error: ErrorMessages.ErrorMessage) extends RuntimeException(error.errMsg) 35 | 36 | } 37 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/model/package.scala: -------------------------------------------------------------------------------- 1 | package pea.common 2 | 3 | package object model { 4 | 5 | case class ApiReq[T](data: T) 6 | 7 | case class ApiRes(code: String = ApiCode.OK, msg: String = ApiMsg.SUCCESS, data: Any = null) 8 | 9 | object ApiResError { 10 | def apply(msg: String = "Error"): ApiRes = ApiRes(code = ApiCode.ERROR, msg = msg) 11 | } 12 | 13 | object ApiResInvalid { 14 | def apply(msg: String = "Invalid"): ApiRes = ApiRes(code = ApiCode.INVALID, msg = msg) 15 | } 16 | 17 | object ApiCode { 18 | val DEFAULT = "00000" 19 | val OK = "10000" 20 | val INVALID = "20000" 21 | val ERROR = "90000" 22 | val NOT_LOGIN = "90001" 23 | val PERMISSION_DENIED = "90002" 24 | } 25 | 26 | object ApiMsg { 27 | val SUCCESS = "SUCCESS" 28 | val FAIL = "FAIL" 29 | val ABORTED = "ABORTED" 30 | val NEED_LOGIN = "NEED LOGIN" 31 | val NOT_FOUND = "NOT FOUND" 32 | val INVALID_REQUEST_BODY = "INVALID REQUEST BODY" 33 | val ILLEGAL_CHARACTER = "ILLEGAL CHARACTER" 34 | val EMPTY_DATA = "EMPTY DATA" 35 | } 36 | 37 | object ApiType { 38 | val REST = "rest" 39 | val GRPC = "grpc" 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/util/DateUtils.scala: -------------------------------------------------------------------------------- 1 | package pea.common.util 2 | 3 | import java.sql.Timestamp 4 | import java.text.SimpleDateFormat 5 | import java.time.LocalDateTime 6 | import java.time.format.DateTimeFormatter 7 | import java.util.Date 8 | 9 | object DateUtils { 10 | 11 | val DEFAULT_DATE_TIME_PATTERN = "yyyy-MM-dd'T'HH:mm:ss" 12 | 13 | def nowTimestamp(): Timestamp = Timestamp.valueOf(LocalDateTime.now()) 14 | 15 | def nowDateTime: String = LocalDateTime.now().format(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_PATTERN)) 16 | 17 | def parse(time: Long, pattern: String = DEFAULT_DATE_TIME_PATTERN): String = { 18 | val sdf = new SimpleDateFormat(pattern) 19 | sdf.format(new Date(time)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/util/FutureUtils.scala: -------------------------------------------------------------------------------- 1 | package pea.common.util 2 | 3 | import scala.concurrent.duration._ 4 | import scala.concurrent.{Await, Future} 5 | 6 | object FutureUtils { 7 | 8 | implicit class RichFuture[T](future: Future[T]) { 9 | def await(implicit duration: Duration = 600 seconds): T = Await.result(future, duration) 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/util/JsonUtils.scala: -------------------------------------------------------------------------------- 1 | package pea.common.util 2 | 3 | import java.io.InputStream 4 | import java.text.SimpleDateFormat 5 | 6 | import com.fasterxml.jackson.annotation.JsonInclude 7 | import com.fasterxml.jackson.core.JsonParser 8 | import com.fasterxml.jackson.core.`type`.TypeReference 9 | import com.fasterxml.jackson.databind.{DeserializationFeature, ObjectMapper} 10 | import com.fasterxml.jackson.module.scala.DefaultScalaModule 11 | import com.fasterxml.jackson.module.scala.experimental.ScalaObjectMapper 12 | 13 | object JsonUtils extends JsonUtils { 14 | 15 | val mapper: ObjectMapper with ScalaObjectMapper = new ObjectMapper() with ScalaObjectMapper 16 | mapper.registerModule(DefaultScalaModule) 17 | mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")) 18 | mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL) 19 | mapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true) 20 | mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) 21 | mapper.configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false) 22 | mapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true) 23 | mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true) 24 | mapper.configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true) 25 | 26 | } 27 | 28 | trait JsonUtils { 29 | val mapper: ObjectMapper 30 | 31 | def stringify(obj: AnyRef): String = { 32 | mapper.writeValueAsString(obj) 33 | } 34 | 35 | def parse[T <: AnyRef](content: String, c: Class[T]): T = { 36 | mapper.readValue(content, c) 37 | } 38 | 39 | def parse[T <: AnyRef](input: InputStream, c: Class[T]): T = { 40 | mapper.readValue(input, c) 41 | } 42 | 43 | def parse[T <: AnyRef](content: String, typeReference: TypeReference[T]): T = { 44 | mapper.readValue(content, typeReference) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/util/LogUtils.scala: -------------------------------------------------------------------------------- 1 | package pea.common.util 2 | 3 | import java.io.{PrintWriter, StringWriter} 4 | 5 | object LogUtils { 6 | 7 | def stackTraceToString(t: Throwable): String = { 8 | val sw = new StringWriter() 9 | t.printStackTrace(new PrintWriter(sw)) 10 | sw.toString 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/util/NetworkUtils.scala: -------------------------------------------------------------------------------- 1 | package pea.common.util 2 | 3 | import java.net.{InetAddress, NetworkInterface} 4 | 5 | import scala.collection.JavaConverters._ 6 | 7 | object NetworkUtils { 8 | 9 | def getLocalIpAddress(): String = { 10 | val interfaces = NetworkInterface.getNetworkInterfaces.asScala.toSeq 11 | val ipAddresses = interfaces.flatMap(_.getInetAddresses.asScala.toSeq) 12 | val address = ipAddresses.find(address => { 13 | val host = address.getHostAddress 14 | host.contains(".") && !address.isLoopbackAddress 15 | }).getOrElse(InetAddress.getLocalHost) 16 | address.getHostAddress 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/util/StringUtils.scala: -------------------------------------------------------------------------------- 1 | package pea.common.util 2 | 3 | object StringUtils { 4 | 5 | val EMPTY = "" 6 | 7 | def isEmpty(value: String): Boolean = null == value || value.length == 0 8 | 9 | def hasEmpty(fist: String, rest: String*): Boolean = { 10 | var hasEmpty = false 11 | if (isEmpty(fist)) hasEmpty = true 12 | for (v <- rest if !hasEmpty) { 13 | if (isEmpty(v)) hasEmpty = true 14 | } 15 | hasEmpty 16 | } 17 | 18 | def isEmpty(value: Option[String]): Boolean = { 19 | if (value.nonEmpty) { 20 | isEmpty(value.get) 21 | } else { 22 | false 23 | } 24 | } 25 | 26 | def isNotEmpty(value: String): Boolean = !isEmpty(value) 27 | 28 | def isNotEmpty(value: Option[String]): Boolean = !isEmpty(value) 29 | 30 | def notEmptyElse(value: String, default: String): String = if (isNotEmpty(value)) value else default 31 | 32 | def toOption(value: String): Option[String] = if (isEmpty(value)) None else Some(value) 33 | } 34 | -------------------------------------------------------------------------------- /pea-common/src/main/scala/pea/common/util/XtermUtils.scala: -------------------------------------------------------------------------------- 1 | package pea.common.util 2 | 3 | // https://en.wikipedia.org/wiki/ANSI_escape_code 4 | object XtermUtils { 5 | 6 | def redWrap(msg: String): String = { 7 | s"\033[1;31m$msg\033[0m" 8 | } 9 | 10 | def greenWrap(msg: String): String = { 11 | s"\033[1;32m$msg\033[0m" 12 | } 13 | 14 | def yellowWrap(msg: String): String = { 15 | s"\033[1;33m$msg\033[0m" 16 | } 17 | 18 | def blueWrap(msg: String): String = { 19 | s"\033[1;34m$msg\033[0m" 20 | } 21 | 22 | def magentaWrap(msg: String): String = { 23 | s"\033[1;35m$msg\033[0m" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/DubboDsl.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo 2 | 3 | import io.gatling.core.action.builder.ActionBuilder 4 | import io.gatling.core.config.GatlingConfiguration 5 | import io.gatling.core.session.Session 6 | import pea.dubbo.protocol.{DubboProtocol, DubboProtocolBuilder} 7 | import pea.dubbo.request.DubboDslBuilder 8 | 9 | trait DubboDsl { 10 | 11 | def dubbo(implicit configuration: GatlingConfiguration) = DubboProtocolBuilder(configuration) 12 | 13 | implicit def protocolBuilder2Protocol(builder: DubboProtocolBuilder): DubboProtocol = builder.build 14 | 15 | def invoke[T, R](clazz: Class[T])(func: (T, Session) => R): DubboDslBuilder[T, R] = { 16 | DubboDslBuilder(clazz, func) 17 | } 18 | 19 | implicit def dubboDslBuilder2ActionBuilder[T, R](builder: DubboDslBuilder[T, R]): ActionBuilder = builder.build() 20 | } 21 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/Predef.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo 2 | 3 | import pea.dubbo.check.DubboCheckSupport 4 | 5 | object Predef extends DubboDsl with DubboCheckSupport 6 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/action/DubboActionBuilder.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.action 2 | 3 | import io.gatling.core.action.Action 4 | import io.gatling.core.action.builder.ActionBuilder 5 | import io.gatling.core.protocol.ProtocolComponentsRegistry 6 | import io.gatling.core.session.Session 7 | import io.gatling.core.structure.ScenarioContext 8 | import pea.dubbo.DubboCheck 9 | import pea.dubbo.protocol.{DubboComponents, DubboProtocol} 10 | 11 | case class DubboActionBuilder[T, V]( 12 | clazz: Class[T], 13 | func: (T, Session) => V, 14 | checks: List[DubboCheck[V]], 15 | protocol: Option[DubboProtocol] = None, 16 | ) extends ActionBuilder { 17 | 18 | private def components(protocolComponentsRegistry: ProtocolComponentsRegistry): DubboComponents = 19 | protocolComponentsRegistry.components(DubboProtocol.DubboProtocolKey) 20 | 21 | override def build(ctx: ScenarioContext, next: Action): Action = { 22 | import ctx._ 23 | val dubboComponents = components(protocolComponentsRegistry) 24 | new DubboAction[T, V](clazz, func, checks, dubboComponents, coreComponents, throttled, next, protocol) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/check/DubboCheckModel.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.check 2 | 3 | import java.util 4 | 5 | import io.gatling.commons.validation.Validation 6 | import io.gatling.core.check.CheckResult 7 | import io.gatling.core.session.Session 8 | import pea.dubbo.{DubboCheck, DubboResponse} 9 | 10 | case class DubboCheckModel[V](wrapped: DubboCheck[V]) extends DubboCheck[V] { 11 | override def check(response: DubboResponse[V], session: Session, preparedCache: util.Map[Any, Any]): Validation[CheckResult] = { 12 | wrapped.check(response, session, preparedCache) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/check/DubboCheckSupport.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.check 2 | 3 | import com.fasterxml.jackson.databind.JsonNode 4 | import io.gatling.core.check.jsonpath.JsonPathCheckType 5 | import io.gatling.core.check.{CheckBuilder, CheckMaterializer, FindCheckBuilder, ValidatorCheckBuilder} 6 | import io.gatling.core.json.JsonParsers 7 | import pea.dubbo.{DubboCheck, DubboResponse} 8 | 9 | import scala.annotation.implicitNotFound 10 | 11 | trait DubboCheckSupport { 12 | 13 | def simple: DubboSimpleCheck.type = DubboSimpleCheck 14 | 15 | @implicitNotFound("Could not find a CheckMaterializer. This check might not be valid for Dubbo.") 16 | implicit def checkBuilder2DubboCheck[A, P, X, V](checkBuilder: CheckBuilder[A, P, X])(implicit materializer: CheckMaterializer[A, DubboCheck[V], DubboResponse[V], P]): DubboCheck[V] = 17 | checkBuilder.build(materializer) 18 | 19 | @implicitNotFound("Could not find a CheckMaterializer. This check might not be valid for Dubbo.") 20 | implicit def validatorCheckBuilder2DubboCheck[A, P, X, V](validatorCheckBuilder: ValidatorCheckBuilder[A, P, X])(implicit materializer: CheckMaterializer[A, DubboCheck[V], DubboResponse[V], P]): DubboCheck[V] = 21 | validatorCheckBuilder.exists 22 | 23 | @implicitNotFound("Could not find a CheckMaterializer. This check might not be valid for Dubbo.") 24 | implicit def findCheckBuilder2DubboCheck[A, P, X, V](findCheckBuilder: FindCheckBuilder[A, P, X])(implicit materializer: CheckMaterializer[A, DubboCheck[V], DubboResponse[V], P]): DubboCheck[V] = 25 | findCheckBuilder.find.exists 26 | 27 | implicit def dubboJsonPathCheckMaterializer[V](implicit jsonParsers: JsonParsers): CheckMaterializer[JsonPathCheckType, DubboCheck[V], DubboResponse[V], JsonNode] = new DubboJsonPathCheckMaterializer[V](jsonParsers) 28 | } 29 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/check/DubboJsonPathCheckMaterializer.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.check 2 | 3 | import com.fasterxml.jackson.databind.{JsonNode, ObjectMapper} 4 | import io.gatling.core.check.jsonpath.JsonPathCheckType 5 | import io.gatling.core.check.{CheckMaterializer, Preparer} 6 | import io.gatling.core.json.JsonParsers 7 | import pea.dubbo.{DubboCheck, DubboResponse} 8 | 9 | object DubboJsonPathCheckMaterializer { 10 | 11 | private val objectMapper = new ObjectMapper() 12 | 13 | private def jsonPathPreparer(jsonParsers: JsonParsers): Preparer[DubboResponse[_], JsonNode] = 14 | response => { 15 | val valueString = if (null != response.value) objectMapper.writeValueAsString(response.value) else "null" 16 | jsonParsers.safeParse(valueString) 17 | } 18 | } 19 | 20 | class DubboJsonPathCheckMaterializer[V](jsonParsers: JsonParsers) 21 | extends CheckMaterializer[JsonPathCheckType, DubboCheck[V], DubboResponse[V], JsonNode]( 22 | (wrapped: DubboCheck[V]) => DubboCheckModel(wrapped) 23 | ) { 24 | 25 | import DubboJsonPathCheckMaterializer._ 26 | 27 | override val preparer: Preparer[DubboResponse[V], JsonNode] = jsonPathPreparer(jsonParsers) 28 | } 29 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/check/DubboSimpleCheck.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.check 2 | 3 | import java.util.{Map => JMap} 4 | 5 | import io.gatling.commons.validation._ 6 | import io.gatling.core.check.CheckResult 7 | import io.gatling.core.session.Session 8 | import pea.dubbo.{DubboCheck, DubboResponse} 9 | 10 | final case class DubboSimpleCheck[R](func: DubboResponse[R] => Boolean) extends DubboCheck[R] { 11 | 12 | override def check(response: DubboResponse[R], session: Session, preparedCache: JMap[Any, Any]): Validation[CheckResult] = { 13 | if (func(response)) { 14 | CheckResult.NoopCheckResultSuccess 15 | } else { 16 | DubboSimpleCheck.DubboSimpleCheckFailure 17 | } 18 | } 19 | } 20 | 21 | object DubboSimpleCheck { 22 | 23 | private val DubboSimpleCheckFailure = "Dubbo check failed".failure 24 | } 25 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/dubbo.scala: -------------------------------------------------------------------------------- 1 | package pea 2 | 3 | import io.gatling.core.check.Check 4 | 5 | package object dubbo { 6 | 7 | case class DubboResponse[V](value: V) 8 | 9 | type DubboCheck[V] = Check[DubboResponse[V]] 10 | } 11 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/protocol/DubboComponents.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.protocol 2 | 3 | import io.gatling.core.protocol.ProtocolComponents 4 | import io.gatling.core.session.Session 5 | 6 | import scala.concurrent.ExecutionContext 7 | 8 | case class DubboComponents( 9 | dubboProtocol: DubboProtocol, 10 | executionContext: ExecutionContext, 11 | ) extends ProtocolComponents { 12 | 13 | override def onStart: Session => Session = ProtocolComponents.NoopOnStart 14 | 15 | override def onExit: Session => Unit = ProtocolComponents.NoopOnExit 16 | } 17 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/protocol/DubboProtocol.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.protocol 2 | 3 | import java.util.concurrent.Executors 4 | 5 | import io.gatling.core.CoreComponents 6 | import io.gatling.core.config.GatlingConfiguration 7 | import io.gatling.core.protocol.{Protocol, ProtocolKey} 8 | import pea.dubbo.request.ReferenceConfigCache 9 | 10 | import scala.concurrent.ExecutionContext 11 | 12 | case class DubboProtocol( 13 | application: Option[String] = Some("pea-dubbo-consumer"), 14 | group: Option[String] = None, 15 | version: Option[String] = None, 16 | endpointUrl: Option[String] = None, 17 | registryUrl: Option[String] = None, 18 | threads: Int = 200, 19 | timeout: Option[Int] = None, 20 | ) extends Protocol 21 | 22 | object DubboProtocol { 23 | 24 | def apply(configuration: GatlingConfiguration): DubboProtocol = DubboProtocol() 25 | 26 | val DubboProtocolKey: ProtocolKey[DubboProtocol, DubboComponents] = new ProtocolKey[DubboProtocol, DubboComponents] { 27 | 28 | def protocolClass: Class[io.gatling.core.protocol.Protocol] = classOf[DubboProtocol].asInstanceOf[Class[io.gatling.core.protocol.Protocol]] 29 | 30 | def defaultProtocolValue(configuration: GatlingConfiguration): DubboProtocol = throw new IllegalStateException("Can't provide a default value for DubboProtocol") 31 | 32 | def newComponents(coreComponents: CoreComponents): DubboProtocol => DubboComponents = { 33 | dubboProtocol => { 34 | val executor = Executors.newFixedThreadPool(dubboProtocol.threads) 35 | coreComponents.actorSystem.registerOnTermination { 36 | ReferenceConfigCache.clear() 37 | executor.shutdown() 38 | } 39 | val executionContext = ExecutionContext.fromExecutor(executor) 40 | DubboComponents(dubboProtocol, executionContext) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/protocol/DubboProtocolBuilder.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.protocol 2 | 3 | import io.gatling.core.config.GatlingConfiguration 4 | 5 | case class DubboProtocolBuilder(var protocol: DubboProtocol) extends ProtocolModifier { 6 | 7 | def build = protocol 8 | } 9 | 10 | object DubboProtocolBuilder { 11 | 12 | def apply(implicit configuration: GatlingConfiguration): DubboProtocolBuilder = { 13 | DubboProtocolBuilder(DubboProtocol(configuration)) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/protocol/ProtocolModifier.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.protocol 2 | 3 | import com.softwaremill.quicklens._ 4 | 5 | trait ProtocolModifier { 6 | 7 | var protocol: DubboProtocol 8 | 9 | def application(name: String): this.type = { 10 | initProtocol() 11 | protocol = protocol.modify(_.application).setTo(Option(name)) 12 | this 13 | } 14 | 15 | def group(name: String): this.type = { 16 | initProtocol() 17 | protocol = protocol.modify(_.group).setTo(Option(name)) 18 | this 19 | } 20 | 21 | def version(name: String): this.type = { 22 | initProtocol() 23 | protocol = protocol.modify(_.version).setTo(Option(name)) 24 | this 25 | } 26 | 27 | def endpoint(url: String): this.type = { 28 | initProtocol() 29 | protocol = protocol.modify(_.endpointUrl).setTo(Option(url)) 30 | this 31 | } 32 | 33 | def endpoint(address: String, port: Int): this.type = { 34 | initProtocol() 35 | protocol = protocol.modify(_.endpointUrl).setTo(Some(s"dubbo://${address}:${port}/")) 36 | this 37 | } 38 | 39 | def zookeeper(url: String): this.type = { 40 | initProtocol() 41 | protocol = protocol.modify(_.registryUrl).setTo(Option(url)) 42 | this 43 | } 44 | 45 | def zookeeper(address: String, port: Int): this.type = { 46 | initProtocol() 47 | protocol = protocol.modify(_.registryUrl).setTo(Some(s"zookeeper://${address}:${port}")) 48 | this 49 | } 50 | 51 | def threads(count: Int): this.type = { 52 | initProtocol() 53 | protocol = protocol.modify(_.threads).setTo(count) 54 | this 55 | } 56 | 57 | def timeout(count: Int): this.type = { 58 | initProtocol() 59 | protocol = protocol.modify(_.timeout).setTo(Some(count)) 60 | this 61 | } 62 | 63 | @inline def initProtocol(): Unit = { 64 | if (null == protocol) { 65 | protocol = DubboProtocol() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/request/CustomReferenceConfig.java: -------------------------------------------------------------------------------- 1 | package pea.dubbo.request; 2 | 3 | import org.apache.dubbo.config.ReferenceConfig; 4 | 5 | public class CustomReferenceConfig extends ReferenceConfig { 6 | 7 | @Override 8 | public void checkAndUpdateSubConfigs() { 9 | // https://github.com/asura-pro/pea/issues/6 10 | ClassLoader reloadClassLoader = getInterfaceClass().getClassLoader(); 11 | Thread.currentThread().setContextClassLoader(reloadClassLoader); 12 | super.checkAndUpdateSubConfigs(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /pea-dubbo/src/main/scala/pea/dubbo/request/DubboDslBuilder.scala: -------------------------------------------------------------------------------- 1 | package pea.dubbo.request 2 | 3 | import io.gatling.core.action.builder.ActionBuilder 4 | import io.gatling.core.session.Session 5 | import pea.dubbo.DubboCheck 6 | import pea.dubbo.action.DubboActionBuilder 7 | import pea.dubbo.check.DubboCheckSupport 8 | import pea.dubbo.protocol.{DubboProtocol, ProtocolModifier} 9 | 10 | case class DubboDslBuilder[T, V]( 11 | clazz: Class[T], 12 | func: (T, Session) => V, 13 | checks: List[DubboCheck[V]] = Nil, 14 | var protocol: DubboProtocol = null 15 | ) extends DubboCheckSupport with ProtocolModifier { 16 | 17 | def check(dubboChecks: DubboCheck[V]*): DubboDslBuilder[T, V] = copy[T, V](checks = checks ::: dubboChecks.toList) 18 | 19 | def build(): ActionBuilder = DubboActionBuilder[T, V](clazz, func, checks, Option(protocol)) 20 | } 21 | -------------------------------------------------------------------------------- /pea-grpc/README.md: -------------------------------------------------------------------------------- 1 | Fork from https://github.com/phiSgr/gatling-grpc 2 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/GrpcDsl.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc 2 | 3 | import com.google.common.util.concurrent.{FutureCallback, Futures, ListenableFuture, MoreExecutors} 4 | import io.gatling.commons.NotNothing 5 | import io.gatling.commons.validation.{Failure, Success} 6 | import io.gatling.core.session.Expression 7 | import io.gatling.core.session.el.ElMessages 8 | import io.grpc.stub.{AbstractStub, ClientCalls} 9 | import io.grpc.{CallOptions, Channel, ManagedChannelBuilder, MethodDescriptor} 10 | import pea.grpc.action.GrpcActionBuilder 11 | import pea.grpc.protocol.GrpcProtocol 12 | 13 | import scala.concurrent.{Future, Promise} 14 | import scala.reflect.ClassTag 15 | 16 | trait GrpcDsl { 17 | 18 | def grpc(channelBuilder: ManagedChannelBuilder[_]) = GrpcProtocol(channelBuilder) 19 | 20 | def grpc(requestName: Expression[String]) = new Call(requestName) 21 | 22 | class Call(requestName: Expression[String]) { 23 | def service[Service <: AbstractStub[Service]](stub: Channel => Service) = new CallWithService[Service](requestName, stub) 24 | 25 | def rpc[Req, Res](method: MethodDescriptor[Req, Res]) = { 26 | assert(method.getType == MethodDescriptor.MethodType.UNARY) 27 | new CallWithMethod[Req, Res](requestName, method) 28 | } 29 | } 30 | 31 | class CallWithMethod[Req, Res](requestName: Expression[String], method: MethodDescriptor[Req, Res]) { 32 | val f = (channel: Channel) => { 33 | request: Req => guavaFuture2ScalaFuture(ClientCalls.futureUnaryCall(channel.newCall(method, CallOptions.DEFAULT), request)) 34 | } 35 | 36 | def payload(req: Expression[Req]) = GrpcActionBuilder(requestName, f, req) 37 | } 38 | 39 | class CallWithService[Service <: AbstractStub[Service]](requestName: Expression[String], stub: Channel => Service) { 40 | def rpc[Req, Res](func: Service => Req => Future[Res])(request: Expression[Req]) = 41 | GrpcActionBuilder(requestName, stub andThen (func), request) 42 | } 43 | 44 | def $[T: ClassTag : NotNothing](name: String): Expression[T] = s => s.attributes.get(name) match { 45 | case Some(t: T) => Success(t) 46 | case None => ElMessages.undefinedSessionAttribute(name) 47 | case Some(t) => Failure(s"Value $t is of type ${t.getClass.getName}, expected ${implicitly[ClassTag[T]].runtimeClass.getName}") 48 | } 49 | 50 | def guavaFuture2ScalaFuture[Res](guavaFuture: ListenableFuture[Res]): Future[Res] = { 51 | val p = Promise[Res]() 52 | Futures.addCallback( 53 | guavaFuture, 54 | new FutureCallback[Res] { 55 | override def onFailure(t: Throwable): Unit = p.failure(t) 56 | 57 | override def onSuccess(a: Res): Unit = p.success(a) 58 | }, 59 | MoreExecutors.directExecutor() 60 | ) 61 | p.future 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/Predef.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc 2 | 3 | import com.trueaccord.lenses.{Lens, Mutation, Updatable} 4 | import io.gatling.core.Predef.value2Expression 5 | import io.gatling.core.session.Expression 6 | import pea.grpc.check.GrpcCheckSupport 7 | 8 | object Predef extends GrpcDsl with GrpcCheckSupport { 9 | 10 | implicit class ExprLens[A, B](val l: Lens[A, B]) extends AnyVal { 11 | def :~(e: Expression[B]): Expression[Mutation[A]] = e.map(l := _) 12 | } 13 | 14 | implicit class ExprUpdatable[A <: Updatable[A]](val e: Expression[A]) extends AnyVal { 15 | def updateExpr(mEs: (Lens[A, A] => Expression[Mutation[A]])*): Expression[A] = { 16 | val mutationExprs = mEs.map(_.apply(Lens.unit)) 17 | s => 18 | mutationExprs.foldLeft(e(s)) { (aVal, mExpr) => 19 | for { 20 | a <- aVal 21 | m <- mExpr(s) 22 | } yield m(a) 23 | } 24 | } 25 | } 26 | 27 | implicit def value2ExprUpdatable[A <: Updatable[A]](e: A): ExprUpdatable[A] = new ExprUpdatable(value2Expression(e)) 28 | 29 | implicit class ExpressionZipping[A](val expression: Expression[A]) extends AnyVal { 30 | def zipWith[B, C](that: Expression[B])(f: (A, B) => C): Expression[C] = { session => 31 | expression(session).flatMap(r1 => that(session).map(r2 => f(r1, r2))) 32 | } 33 | } 34 | 35 | implicit class SomeWrapper[T](val value: T) extends AnyVal { 36 | def some: Some[T] = Some(value) 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/action/GrpcActionBuilder.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc.action 2 | 3 | import io.gatling.core.action.Action 4 | import io.gatling.core.action.builder.ActionBuilder 5 | import io.gatling.core.check.{MultipleFindCheckBuilder, ValidatorCheckBuilder} 6 | import io.gatling.core.session.Expression 7 | import io.gatling.core.structure.ScenarioContext 8 | import io.grpc.{Channel, Metadata} 9 | import pea.grpc.check.{GrpcCheck, ResponseExtract} 10 | import pea.grpc.request.HeaderPair 11 | 12 | import scala.collection.breakOut 13 | import scala.concurrent.Future 14 | 15 | case class GrpcActionBuilder[Req, Res]( 16 | requestName: Expression[String], 17 | method: Channel => Req => Future[Res], 18 | payload: Expression[Req], 19 | headers: List[HeaderPair[_]] = Nil, 20 | checks: List[GrpcCheck[Res]] = Nil, 21 | ) extends ActionBuilder { 22 | 23 | override def build(ctx: ScenarioContext, next: Action): Action = GrpcAction(this, ctx, next) 24 | 25 | def header[T](key: Metadata.Key[T])(value: Expression[T]) = 26 | copy(headers = HeaderPair(key, value) :: headers) 27 | 28 | def check(checks: GrpcCheck[Res]*) = 29 | copy(checks = this.checks ::: checks.toList) 30 | 31 | private def mapToList[T, U](s: Seq[T])(f: T => U) = s.map[U, List[U]](f)(breakOut) 32 | 33 | // In fact they can be added to checks using .check, but the type Res cannot be inferred there 34 | def extract[X](f: Res => Option[X])(ts: (ValidatorCheckBuilder[ResponseExtract, Res, X] => GrpcCheck[Res])*) = { 35 | val e = ResponseExtract.extract(f) 36 | copy(checks = checks ::: mapToList(ts)(_.apply(e))) 37 | } 38 | 39 | def exists[X](f: Res => Option[X]) = extract(f)(_.exists.build(ResponseExtract.materializer)) 40 | 41 | def extractMultiple[X](f: Res => Option[Seq[X]])(ts: (MultipleFindCheckBuilder[ResponseExtract, Res, X] => GrpcCheck[Res])*) = { 42 | val e = ResponseExtract.extractMultiple[Res, X](f) 43 | copy(checks = checks ::: mapToList(ts)(_.apply(e))) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/check/GrpcCheck.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc.check 2 | 3 | import java.util.{Map => JMap} 4 | 5 | import io.gatling.core.check.Check 6 | import io.gatling.core.session.Session 7 | import pea.grpc.check.GrpcCheck.{Scope, Status} 8 | 9 | import scala.annotation.unchecked.uncheckedVariance 10 | import scala.util.Try 11 | 12 | case class GrpcCheck[-T](wrapped: Check[Try[T]@uncheckedVariance], scope: Scope) extends Check[Try[T]@uncheckedVariance] { 13 | override def check(response: Try[T], session: Session, cache: JMap[Any, Any]) = 14 | wrapped.check(response, session, cache) 15 | 16 | def checksStatus = scope == Status 17 | } 18 | 19 | object GrpcCheck { 20 | 21 | sealed trait Scope 22 | 23 | case object Status extends Scope 24 | 25 | case object Value extends Scope 26 | 27 | } 28 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/check/GrpcCheckSupport.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc.check 2 | 3 | import io.gatling.core.check.{CheckBuilder, CheckMaterializer, FindCheckBuilder, ValidatorCheckBuilder} 4 | 5 | import scala.util.Try 6 | 7 | trait GrpcCheckSupport { 8 | 9 | val statusCode = StatusExtract.StatusCode 10 | val statusDescription = StatusExtract.StatusDescription 11 | 12 | def extract[T, X](f: T => Option[X]) = ResponseExtract.extract(f) 13 | 14 | def extractMultiple[T, X](f: T => Option[Seq[X]]) = ResponseExtract.extractMultiple(f) 15 | 16 | implicit def responseMat[Res]: CheckMaterializer[ResponseExtract, GrpcCheck[Res], Try[Res], Res] = 17 | ResponseExtract.materializer 18 | 19 | implicit val statusMat: CheckMaterializer[StatusExtract, GrpcCheck[Any], Try[Any], Try[Any]] = 20 | StatusExtract.Materializer 21 | 22 | // The contravarianceHelper is needed because without it, the implicit conversion does not turn 23 | // CheckBuilder[StatusExtract, Try[Any], X] into a GrpcCheck[Res] 24 | // Despite GrpcCheck[Any] is a subtype of GrpcCheck[Res]. 25 | implicit def checkBuilder2GrpcCheck[A, P, X, ResOrAny, Res](checkBuilder: CheckBuilder[A, P, X])( 26 | implicit materializer: CheckMaterializer[A, GrpcCheck[ResOrAny], Try[ResOrAny], P], 27 | contravarianceHelper: GrpcCheck[ResOrAny] => GrpcCheck[Res] 28 | ): GrpcCheck[Res] = 29 | contravarianceHelper(checkBuilder.build(materializer)) 30 | 31 | implicit def validatorCheckBuilder2GrpcCheck[A, P, X, ResOrAny, Res](vCheckBuilder: ValidatorCheckBuilder[A, P, X])( 32 | implicit materializer: CheckMaterializer[A, GrpcCheck[ResOrAny], Try[ResOrAny], P], 33 | contravarianceHelper: GrpcCheck[ResOrAny] => GrpcCheck[Res] 34 | ): GrpcCheck[Res] = vCheckBuilder.exists 35 | 36 | implicit def findCheckBuilder2GrpcCheck[A, P, X, ResOrAny, Res](findCheckBuilder: FindCheckBuilder[A, P, X])( 37 | implicit materializer: CheckMaterializer[A, GrpcCheck[ResOrAny], Try[ResOrAny], P], 38 | contravarianceHelper: GrpcCheck[ResOrAny] => GrpcCheck[Res] 39 | ): GrpcCheck[Res] = findCheckBuilder.find.exists 40 | 41 | } 42 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/check/StatusExtract.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc.check 2 | 3 | import io.gatling.commons.validation.{FailureWrapper, SuccessWrapper, Validation} 4 | import io.gatling.core.Predef.value2Expression 5 | import io.gatling.core.check._ 6 | import io.grpc.{Status, StatusException, StatusRuntimeException} 7 | 8 | import scala.util.{Failure, Success, Try} 9 | 10 | object StatusExtract { 11 | 12 | def extractStatus(t: Try[_]): Validation[Status] = t match { 13 | case Success(_) => Status.OK.success 14 | case Failure(e: StatusException) => e.getStatus.success 15 | case Failure(e: StatusRuntimeException) => e.getStatus.success 16 | case Failure(_) => "Response wasn't received".failure 17 | } 18 | 19 | val StatusDescription: ValidatorCheckBuilder[StatusExtract, Try[Any], String] = ValidatorCheckBuilder( 20 | extractor = new FindExtractor[Try[Any], String]( 21 | name = "grpcStatusDescription", 22 | extractor = extractStatus(_).map(s => Option(s.getDescription))), 23 | displayActualValue = true 24 | ) 25 | 26 | val StatusCode: ValidatorCheckBuilder[StatusExtract, Try[Any], Status.Code] = ValidatorCheckBuilder( 27 | extractor = new FindExtractor[Try[Any], Status.Code]( 28 | name = "grpcStatusCode", 29 | extractor = extractStatus(_).map(s => Some(s.getCode)) 30 | ), 31 | displayActualValue = true 32 | ) 33 | 34 | object Materializer extends CheckMaterializer[StatusExtract, GrpcCheck[Any], Try[Any], Try[Any]](GrpcCheck(_, GrpcCheck.Status)) { 35 | override protected def preparer: Preparer[Try[Any], Try[Any]] = _.success 36 | } 37 | 38 | val DefaultCheck = StatusCode.is(value2Expression(Status.Code.OK)).build(Materializer) 39 | } 40 | 41 | trait StatusExtract 42 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/grpc.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc 2 | 3 | package object grpc { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/protocol/GrpcComponents.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc.protocol 2 | 3 | import io.gatling.core.protocol.ProtocolComponents 4 | import io.gatling.core.session.Session 5 | import io.grpc.ManagedChannel 6 | 7 | case class GrpcComponents( 8 | channel: ManagedChannel, 9 | ) extends ProtocolComponents { 10 | 11 | 12 | override def onStart: Session => Session = ProtocolComponents.NoopOnStart 13 | 14 | override def onExit: Session => Unit = ProtocolComponents.NoopOnExit 15 | } 16 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/protocol/GrpcProtocol.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc.protocol 2 | 3 | import io.gatling.core.CoreComponents 4 | import io.gatling.core.config.GatlingConfiguration 5 | import io.gatling.core.protocol.{Protocol, ProtocolKey} 6 | import io.grpc.ManagedChannelBuilder 7 | 8 | case class GrpcProtocol( 9 | channelBuilder: ManagedChannelBuilder[_], 10 | ) extends Protocol 11 | 12 | object GrpcProtocol { 13 | 14 | val GrpcProtocolKey = new ProtocolKey[GrpcProtocol, GrpcComponents] { 15 | override def protocolClass: Class[Protocol] = classOf[GrpcProtocol].asInstanceOf[Class[Protocol]] 16 | 17 | override def defaultProtocolValue(configuration: GatlingConfiguration) = 18 | throw new IllegalStateException("Can't provide a default value for GrpcProtocol") 19 | 20 | override def newComponents(coreComponents: CoreComponents): GrpcProtocol => GrpcComponents = { protocol => 21 | val channel = protocol.channelBuilder.build() 22 | coreComponents.actorSystem.registerOnTermination { 23 | channel.shutdownNow() 24 | } 25 | GrpcComponents(channel) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /pea-grpc/src/main/scala/pea/grpc/request/HeaderPair.scala: -------------------------------------------------------------------------------- 1 | package pea.grpc.request 2 | 3 | import io.gatling.core.session.Expression 4 | import io.grpc.Metadata 5 | 6 | case class HeaderPair[T](key: Metadata.Key[T], value: Expression[T]) 7 | -------------------------------------------------------------------------------- /project/FrontendCommands.scala: -------------------------------------------------------------------------------- 1 | object FrontendCommands { 2 | 3 | val dependencyInstall: String = "yarn install" 4 | val test: String = "yarn run test:none" 5 | val serve: String = "yarn run start" 6 | val build: String = "yarn run build:prod" 7 | } 8 | -------------------------------------------------------------------------------- /project/FrontendRunHook.scala: -------------------------------------------------------------------------------- 1 | import play.sbt.PlayRunHook 2 | import sbt._ 3 | 4 | import scala.sys.process.Process 5 | 6 | /** 7 | * Frontend build play run hook. 8 | * https://www.playframework.com/documentation/2.7.x/SBTCookbook 9 | */ 10 | object FrontendRunHook { 11 | 12 | def apply(base: File): PlayRunHook = { 13 | 14 | object UIBuildHook extends PlayRunHook { 15 | 16 | var process: Option[Process] = None 17 | 18 | /** 19 | * Change the commands in `FrontendCommands.scala` if you want to use Yarn. 20 | */ 21 | var install: String = FrontendCommands.dependencyInstall 22 | var run: String = FrontendCommands.serve 23 | 24 | // Windows requires npm commands prefixed with cmd /c 25 | if (System.getProperty("os.name").toLowerCase().contains("win")) { 26 | install = "cmd /c" + install 27 | run = "cmd /c" + run 28 | } 29 | 30 | /** 31 | * Executed before play run start. 32 | * Run npm install if node modules are not installed. 33 | */ 34 | override def beforeStarted(): Unit = { 35 | if (!(base / "ui" / "node_modules").exists()) Process(install, base / "ui").! 36 | } 37 | 38 | /** 39 | * Executed after play run start. 40 | * Run npm start 41 | */ 42 | override def afterStarted(): Unit = { 43 | process = Option( 44 | Process(run, base / "ui").run 45 | ) 46 | } 47 | 48 | /** 49 | * Executed after play run stop. 50 | * Cleanup frontend execution processes. 51 | */ 52 | override def afterStopped(): Unit = { 53 | process.foreach(_.destroy()) 54 | process = None 55 | } 56 | 57 | } 58 | 59 | UIBuildHook 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.2.8 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // Pure Scala Artifact Fetching https://github.com/coursier/coursier 2 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "2.0.0-RC5-2") 3 | // addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.3") 4 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.7.4") 5 | addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "1.0.0") 6 | addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.1") 7 | 8 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.2") 9 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.8") 10 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.3") 11 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.5.1") 12 | -------------------------------------------------------------------------------- /project/scalapb.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.thesamet" % "sbt-protoc" % "0.99.13") 2 | 3 | // Be compatible with zinc(https://github.com/sbt/zinc/blob/develop/project/plugins.sbt) 4 | libraryDependencies += "com.trueaccord.scalapb" %% "compilerplugin" % "0.6.7" 5 | -------------------------------------------------------------------------------- /test-generated/pea/grpc/hello/HelloProto.scala: -------------------------------------------------------------------------------- 1 | // Generated by the Scala Plugin for the Protocol Buffer Compiler. 2 | // Do not edit! 3 | // 4 | // Protofile syntax: PROTO3 5 | 6 | package pea.grpc.hello 7 | 8 | 9 | 10 | object HelloProto extends _root_.com.trueaccord.scalapb.GeneratedFileObject { 11 | lazy val dependencies: Seq[_root_.com.trueaccord.scalapb.GeneratedFileObject] = Seq( 12 | ) 13 | lazy val messagesCompanions: Seq[_root_.com.trueaccord.scalapb.GeneratedMessageCompanion[_]] = Seq( 14 | pea.grpc.hello.HelloRequest, 15 | pea.grpc.hello.HelloResponse 16 | ) 17 | private lazy val ProtoBytes: Array[Byte] = 18 | com.trueaccord.scalapb.Encoding.fromBase64(scala.collection.Seq( 19 | """CgtoZWxsby5wcm90bxIIcGVhLmdycGMiKgoMSGVsbG9SZXF1ZXN0EhoKCGdyZWV0aW5nGAEgASgJUghncmVldGluZyIlCg1IZ 20 | Wxsb1Jlc3BvbnNlEhQKBXJlcGx5GAEgASgJUgVyZXBseTJLCgxIZWxsb1NlcnZpY2USOwoIU2F5SGVsbG8SFi5wZWEuZ3JwYy5IZ 21 | Wxsb1JlcXVlc3QaFy5wZWEuZ3JwYy5IZWxsb1Jlc3BvbnNlYgZwcm90bzM=""" 22 | ).mkString) 23 | lazy val scalaDescriptor: _root_.scalapb.descriptors.FileDescriptor = { 24 | val scalaProto = com.google.protobuf.descriptor.FileDescriptorProto.parseFrom(ProtoBytes) 25 | _root_.scalapb.descriptors.FileDescriptor.buildFrom(scalaProto, dependencies.map(_.scalaDescriptor)) 26 | } 27 | lazy val javaDescriptor: com.google.protobuf.Descriptors.FileDescriptor = { 28 | val javaProto = com.google.protobuf.DescriptorProtos.FileDescriptorProto.parseFrom(ProtoBytes) 29 | com.google.protobuf.Descriptors.FileDescriptor.buildFrom(javaProto, Array( 30 | )) 31 | } 32 | @deprecated("Use javaDescriptor instead. In a future version this will refer to scalaDescriptor.", "ScalaPB 0.5.47") 33 | def descriptor: com.google.protobuf.Descriptors.FileDescriptor = javaDescriptor 34 | } -------------------------------------------------------------------------------- /test/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | ${application.home:-.}/logs/application.log 9 | 10 | %date [%level] from %logger in %thread - %message%n%xException 11 | 12 | 13 | 14 | 15 | 16 | %coloredLevel %logger{15} - %message%n%xException{10} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /test/pea/app/IDEPathHelper.scala: -------------------------------------------------------------------------------- 1 | package pea.app 2 | 3 | import java.nio.file.{Path, Paths} 4 | 5 | import io.gatling.commons.util.PathHelper._ 6 | 7 | object IDEPathHelper { 8 | 9 | val projectRootDir: Path = Paths.get(".") 10 | val binariesFolder = projectRootDir / "target" / "scala-2.12" / "test-classes" 11 | val resultsFolder = projectRootDir / "target" / "results" 12 | } 13 | -------------------------------------------------------------------------------- /test/pea/app/compiler/ZincCompilerSpec.scala: -------------------------------------------------------------------------------- 1 | package pea.app.compiler 2 | 3 | import pea.app.IDEPathHelper 4 | import pea.app.actor.CompilerActor.SyncCompileMessage 5 | import pea.common.util.FutureUtils.RichFuture 6 | 7 | object ZincCompilerSpec { 8 | 9 | def main(args: Array[String]): Unit = { 10 | val config = CompilerConfiguration.fromCompileMessage(SyncCompileMessage( 11 | s"${IDEPathHelper.projectRootDir}/test/simulations", 12 | s"${IDEPathHelper.projectRootDir}/logs/output", 13 | )) 14 | val result = ScalaCompiler.doCompile(config).await 15 | println(result) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/pea/app/dubbo/GreetingConsumerApp.scala: -------------------------------------------------------------------------------- 1 | package pea.app.dubbo 2 | 3 | import org.apache.dubbo.config.ApplicationConfig 4 | import pea.app.dubbo.api.GreetingService 5 | import pea.dubbo.request.CustomReferenceConfig 6 | 7 | object GreetingConsumerApp extends RegistryAddressConfig { 8 | 9 | def main(args: Array[String]): Unit = { 10 | val reference = new CustomReferenceConfig[GreetingService]() 11 | reference.setApplication(new ApplicationConfig("pea-dubbo-consumer")) 12 | // reference.setVersion("1.0.0") 13 | reference.setTimeout(3000) 14 | reference.setInterface(classOf[GreetingService]) 15 | // reference.setRegistry(new RegistryConfig(RegistryAddressZK)) 16 | reference.setUrl(s"dubbo://127.0.0.1:20880/${classOf[GreetingService].getName}") 17 | val service = reference.get() 18 | val response = service.sayHello("pea") 19 | println(s"Got: ${response}") 20 | reference.destroy() 21 | System.exit(0) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/pea/app/dubbo/GreetingProviderApp.scala: -------------------------------------------------------------------------------- 1 | package pea.app.dubbo 2 | 3 | import java.util.concurrent.CountDownLatch 4 | 5 | import org.apache.dubbo.config.{ApplicationConfig, RegistryConfig, ServiceConfig} 6 | import pea.app.dubbo.api.GreetingService 7 | import pea.app.dubbo.provider.GreetingsServiceImpl 8 | 9 | object GreetingProviderApp extends RegistryAddressConfig { 10 | 11 | def main(args: Array[String]): Unit = { 12 | val service = new ServiceConfig[GreetingService]() 13 | service.setApplication(new ApplicationConfig("pea-dubbo-provider")) 14 | service.setRegistry(new RegistryConfig(RegistryAddressNA)) 15 | service.setInterface(classOf[GreetingService]) 16 | service.setRef(new GreetingsServiceImpl()) 17 | service.export() 18 | println(s"${service.getInterface}: ${service.getExportedUrls}") 19 | new CountDownLatch(1).await() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/pea/app/dubbo/RegistryAddressConfig.scala: -------------------------------------------------------------------------------- 1 | package pea.app.dubbo 2 | 3 | trait RegistryAddressConfig { 4 | 5 | val RegistryAddressNA = "N/A" 6 | val RegistryAddressZK = "zookeeper://127.0.0.1:2181" 7 | } 8 | -------------------------------------------------------------------------------- /test/pea/app/dubbo/api/GreetingService.java: -------------------------------------------------------------------------------- 1 | package pea.app.dubbo.api; 2 | 3 | public interface GreetingService { 4 | String sayHello(String name); 5 | } 6 | -------------------------------------------------------------------------------- /test/pea/app/dubbo/provider/GreetingsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package pea.app.dubbo.provider; 2 | 3 | import pea.app.dubbo.api.GreetingService; 4 | 5 | public class GreetingsServiceImpl implements GreetingService { 6 | 7 | @Override 8 | public String sayHello(String name) { 9 | return "hi, " + name; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/pea/app/gatling/CompilerSpec.scala: -------------------------------------------------------------------------------- 1 | package pea.app.gatling 2 | 3 | import com.typesafe.scalalogging.StrictLogging 4 | import pea.app.IDEPathHelper 5 | import pea.app.actor.CompilerActor.SyncCompileMessage 6 | import pea.app.compiler.ScalaCompiler 7 | import pea.common.util.FutureUtils.RichFuture 8 | 9 | object CompilerSpec extends StrictLogging { 10 | 11 | def main(args: Array[String]): Unit = { 12 | 13 | val compileMessage = SyncCompileMessage( 14 | srcFolder = s"${IDEPathHelper.projectRootDir}/test/simulations", 15 | outputFolder = s"${IDEPathHelper.projectRootDir}/logs/output", 16 | verbose = false, 17 | ) 18 | val code = ScalaCompiler.doGatlingCompile( 19 | compileMessage, 20 | stdout => { 21 | logger.info(s"stdout: ${stdout}") 22 | }, 23 | stderr => { 24 | logger.error(s"stderr: ${stderr}") 25 | } 26 | ).await 27 | logger.info(s"exit: ${code}") 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /test/pea/app/gatling/RunnerSpec.scala: -------------------------------------------------------------------------------- 1 | package pea.app.gatling 2 | 3 | import com.typesafe.scalalogging.StrictLogging 4 | import pea.app.actor.GatlingRunnerActor 5 | import pea.app.actor.GatlingRunnerActor.StartMessage 6 | import pea.app.simulations.GrpcHelloSimulation 7 | import pea.app.{IDEPathHelper, PeaConfig} 8 | import pea.common.util.FutureUtils.RichFuture 9 | import pea.common.util.StringUtils 10 | 11 | import scala.concurrent.ExecutionContext.global 12 | 13 | object RunnerSpec extends StrictLogging { 14 | 15 | PeaConfig.defaultSimulationOutputFolder = IDEPathHelper.binariesFolder.toAbsolutePath.toString 16 | 17 | def main(args: Array[String]): Unit = { 18 | run() 19 | } 20 | 21 | def run(): Unit = { 22 | val message = StartMessage( 23 | IDEPathHelper.binariesFolder.toAbsolutePath.toString, 24 | classOf[GrpcHelloSimulation].getName, 25 | true, 26 | IDEPathHelper.resultsFolder.toAbsolutePath.toString, 27 | StringUtils.EMPTY, 28 | ) 29 | val result = GatlingRunnerActor.start(message)(global).result.await 30 | logger.info(s"Exit: ${result}") 31 | } 32 | 33 | def report(): Unit = { 34 | val result = GatlingRunnerActor.generateReport( 35 | "runId", 36 | IDEPathHelper.resultsFolder.toAbsolutePath.toString, 37 | ).await 38 | logger.info(s"Exit: ${result}") 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/pea/app/grpc/HelloServiceClient.scala: -------------------------------------------------------------------------------- 1 | package pea.app.grpc 2 | 3 | import java.util.concurrent.TimeUnit 4 | 5 | import io.grpc.ManagedChannel 6 | import io.grpc.netty.NettyChannelBuilder 7 | import pea.common.util.LogUtils 8 | import pea.grpc.hello.HelloServiceGrpc.HelloServiceBlockingStub 9 | import pea.grpc.hello.{HelloRequest, HelloServiceGrpc} 10 | 11 | object HelloServiceClient { 12 | 13 | def main(args: Array[String]): Unit = { 14 | val channel = NettyChannelBuilder 15 | .forAddress("localhost", 50051) 16 | .usePlaintext() 17 | .build() 18 | val blockingStub = HelloServiceGrpc.blockingStub(channel) 19 | val client = new HelloServiceClient(channel, blockingStub) 20 | client.greet("pea") 21 | client.shutdown() 22 | } 23 | } 24 | 25 | class HelloServiceClient( 26 | channel: ManagedChannel, 27 | blockingStub: HelloServiceBlockingStub, 28 | ) { 29 | 30 | def shutdown(): Unit = { 31 | channel.shutdown().awaitTermination(5, TimeUnit.SECONDS) 32 | } 33 | 34 | def greet(name: String): Unit = { 35 | val request = HelloRequest("pea") 36 | try { 37 | val response = blockingStub.sayHello(request) 38 | println(s"GOT: ${response.reply}") 39 | } catch { 40 | case t: Throwable => println(LogUtils.stackTraceToString(t)) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/pea/app/grpc/HelloServiceServer.scala: -------------------------------------------------------------------------------- 1 | package pea.app.grpc 2 | 3 | import io.grpc.Server 4 | import io.grpc.netty.NettyServerBuilder 5 | import pea.grpc.hello.{HelloRequest, HelloResponse, HelloServiceGrpc} 6 | 7 | import scala.concurrent.{ExecutionContext, Future} 8 | 9 | object HelloServiceServer { 10 | 11 | def main(args: Array[String]): Unit = { 12 | val server = new HelloServiceServer(ExecutionContext.global) 13 | server.start(50051) 14 | server.blockUntilShutdown() 15 | } 16 | } 17 | 18 | class HelloServiceServer(executionContext: ExecutionContext) { 19 | 20 | private var server: Server = null 21 | 22 | private def start(port: Int): Unit = { 23 | server = NettyServerBuilder.forPort(port).addService(HelloServiceGrpc.bindService(new HelloServiceImpl, executionContext)).build().start() 24 | println(s"Server start at: ${server.getPort}") 25 | sys.addShutdownHook { 26 | stop() 27 | } 28 | } 29 | 30 | private def stop(): Unit = { 31 | if (server != null) { 32 | server.shutdown() 33 | } 34 | } 35 | 36 | private def blockUntilShutdown(): Unit = { 37 | if (server != null) { 38 | server.awaitTermination() 39 | } 40 | } 41 | 42 | private class HelloServiceImpl extends HelloServiceGrpc.HelloService { 43 | override def sayHello(request: HelloRequest): Future[HelloResponse] = { 44 | val response = HelloResponse(s"hi, ${request.greeting}") 45 | Future.successful(response) 46 | } 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /test/pea/app/simulations/BasicSimulation.scala: -------------------------------------------------------------------------------- 1 | package pea.app.simulations 2 | 3 | import io.gatling.core.Predef._ 4 | import io.gatling.http.Predef._ 5 | import pea.app.gatling.PeaSimulation 6 | 7 | import scala.concurrent.duration._ 8 | 9 | class BasicSimulation extends PeaSimulation { 10 | 11 | override val description: String = "BasicSimulation for test." 12 | 13 | val httpProtocol = http 14 | .baseUrl("http://computer-database.gatling.io") // Here is the root for all relative URLs 15 | .acceptHeader("text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") // Here are the common headers 16 | .acceptEncodingHeader("gzip, deflate") 17 | .acceptLanguageHeader("en-US,en;q=0.5") 18 | .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0") 19 | 20 | val scn = scenario("Scenario Name") // A scenario is a chain of requests and pauses 21 | .exec(http("request_1") 22 | .get("/")) 23 | .pause(7) // Note that Gatling has recorder real time pauses 24 | .exec(http("request_2") 25 | .get("/computers?f=macbook")) 26 | .pause(2) 27 | .exec(http("request_3") 28 | .get("/computers/6")) 29 | .pause(3) 30 | .exec(http("request_4") 31 | .get("/")) 32 | .pause(2) 33 | .exec(http("request_5") 34 | .get("/computers?p=1")) 35 | .pause(670 milliseconds) 36 | .exec(http("request_6") 37 | .get("/computers?p=2")) 38 | .pause(629 milliseconds) 39 | .exec(http("request_7") 40 | .get("/computers?p=3")) 41 | .pause(734 milliseconds) 42 | .exec(http("request_8") 43 | .get("/computers?p=4")) 44 | .pause(5) 45 | .exec(http("request_9") 46 | .get("/computers/new")) 47 | .pause(1) 48 | .exec(http("request_10") // Here's an example of a POST request 49 | .post("/computers") 50 | .formParam("""name""", """Beautiful Computer""") // Note the triple double quotes: used in Scala for protecting a whole chain of characters (no need for backslash) 51 | .formParam("""introduced""", """2012-05-30""") 52 | .formParam("""discontinued""", """""") 53 | .formParam("""company""", """37""")) 54 | 55 | setUp(scn.inject(atOnceUsers(1)).protocols(httpProtocol)) 56 | } 57 | -------------------------------------------------------------------------------- /test/pea/app/simulations/DubboGreetingSimulation.scala: -------------------------------------------------------------------------------- 1 | package pea.app.simulations 2 | 3 | import io.gatling.core.Predef._ 4 | import pea.app.dubbo.api.GreetingService 5 | import pea.app.gatling.PeaSimulation 6 | import pea.dubbo.Predef._ 7 | 8 | class DubboGreetingSimulation extends PeaSimulation { 9 | 10 | override val description: String = 11 | """ 12 | |Dubbo simulation example 13 | |""".stripMargin 14 | 15 | val dubboProtocol = dubbo 16 | .application("gatling-pea") 17 | .endpoint("127.0.0.1", 20880) 18 | .threads(10) 19 | 20 | val scn = scenario("dubbo") 21 | .exec( 22 | invoke(classOf[GreetingService]) { (service, _) => 23 | service.sayHello("pea") 24 | }.check(simple { response => 25 | response.value == "hi, pea" 26 | }).check( 27 | jsonPath("$").is("hi, pea") 28 | ) 29 | ) 30 | 31 | setUp( 32 | scn.inject(atOnceUsers(10000)) 33 | ).protocols(dubboProtocol) 34 | } 35 | -------------------------------------------------------------------------------- /test/pea/app/simulations/GrpcHelloSimulation.scala: -------------------------------------------------------------------------------- 1 | package pea.app.simulations 2 | 3 | import io.gatling.core.Predef._ 4 | import io.grpc.netty.NettyChannelBuilder 5 | import io.grpc.{Context, Metadata, Status} 6 | import pea.app.gatling.PeaSimulation 7 | import pea.grpc.Predef._ 8 | import pea.grpc.hello.{HelloRequest, HelloServiceGrpc} 9 | 10 | class GrpcHelloSimulation extends PeaSimulation { 11 | 12 | override val description: String = 13 | """ 14 | |Grpc simulation example 15 | |""".stripMargin 16 | 17 | val grpcProtocol = grpc( 18 | NettyChannelBuilder.forAddress("localhost", 50051).usePlaintext() 19 | ) 20 | 21 | val TokenHeaderKey = Metadata.Key.of("token", Metadata.ASCII_STRING_MARSHALLER) 22 | val TokenContextKey = Context.key[String]("token") 23 | 24 | val scn = scenario("grpc") 25 | .exec( 26 | grpc("Hello Pea") 27 | .rpc(HelloServiceGrpc.METHOD_SAY_HELLO) 28 | .payload(HelloRequest.defaultInstance.updateExpr( 29 | _.greeting :~ "pea" 30 | )) 31 | .header(TokenHeaderKey)("token") 32 | .check( 33 | statusCode is Status.Code.OK, 34 | ) 35 | .extract(_.reply.some)( 36 | _.is("hi, pea") 37 | ) 38 | .extractMultiple(_.reply.split(" ").toSeq.some)( 39 | _.count is 2, 40 | _.find(10).notExists, 41 | _.findAll is List("hi,", "pea") 42 | ) 43 | ) 44 | 45 | setUp( 46 | scn.inject(atOnceUsers(10000)) 47 | ).protocols(grpcProtocol) 48 | } 49 | -------------------------------------------------------------------------------- /test/protobuf/hello.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package pea.grpc; 4 | 5 | service HelloService { 6 | rpc SayHello (HelloRequest) returns (HelloResponse); 7 | } 8 | 9 | message HelloRequest { 10 | string greeting = 1; 11 | } 12 | 13 | message HelloResponse { 14 | string reply = 1; 15 | } 16 | -------------------------------------------------------------------------------- /ui-build.sbt: -------------------------------------------------------------------------------- 1 | import scala.sys.process.Process 2 | 3 | /* 4 | * UI Build hook Scripts 5 | */ 6 | 7 | // Execution status success. 8 | val Success = 0 9 | 10 | // Execution status failure. 11 | val Error = 1 12 | 13 | // Run angular serve task when Play runs in dev mode, that is, when using 'sbt run' 14 | // https://www.playframework.com/documentation/2.7.x/SBTCookbook 15 | PlayKeys.playRunHooks += baseDirectory.map(FrontendRunHook.apply).value 16 | 17 | // True if build running operating system is windows. 18 | val isWindows = System.getProperty("os.name").toLowerCase().contains("win") 19 | 20 | // Execute on commandline, depending on the operating system. Used to execute npm commands. 21 | def runOnCommandline(script: String)(implicit dir: File): Int = { 22 | if (isWindows) { 23 | Process("cmd /c " + script, dir) 24 | } else { 25 | Process(script, dir) 26 | } 27 | } ! 28 | 29 | // Execute `npm install` command to install all node module dependencies. Return Success if already installed. 30 | def runNpmInstall(implicit dir: File): Int = 31 | runOnCommandline(FrontendCommands.dependencyInstall) 32 | 33 | // Execute task if node modules are installed, else return Error status. 34 | def ifNodeModulesInstalled(task: => Int)(implicit dir: File): Int = 35 | if (runNpmInstall == Success) task 36 | else Error 37 | 38 | // Execute frontend test task. Update to change the frontend test task. 39 | def executeUiTests(implicit dir: File): Int = ifNodeModulesInstalled(runOnCommandline(FrontendCommands.test)) 40 | 41 | // Execute frontend prod build task. Update to change the frontend prod build task. 42 | def executeProdBuild(implicit dir: File): Int = ifNodeModulesInstalled(runOnCommandline(FrontendCommands.build)) 43 | 44 | 45 | // Create frontend build tasks for prod, dev and test execution. 46 | 47 | lazy val `ui-test` = taskKey[Unit]("Run UI tests when testing application.") 48 | 49 | `ui-test` := { 50 | implicit val userInterfaceRoot = baseDirectory.value / "ui" 51 | if (executeUiTests != Success) throw new Exception("UI tests failed!") 52 | } 53 | 54 | lazy val `ui-prod-build` = taskKey[Unit]("Run UI build when packaging the application.") 55 | 56 | `ui-prod-build` := { 57 | implicit val userInterfaceRoot = baseDirectory.value / "ui" 58 | if (executeProdBuild != Success) throw new Exception("Oops! UI Build crashed.") 59 | } 60 | 61 | // Execute frontend prod build task prior to play dist execution. 62 | dist := (dist dependsOn `ui-prod-build`).value 63 | 64 | // Execute frontend prod build task prior to play stage execution. 65 | stage := (stage dependsOn `ui-prod-build`).value 66 | 67 | // Execute frontend test task prior to play test execution. 68 | test := ((test in Test) dependsOn `ui-test`).value 69 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # Pea 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 8.2.2. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /ui/browserslist: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # You can see what browsers were selected by your queries by running: 6 | # npx browserslist 7 | 8 | > 0.5% 9 | last 2 versions 10 | Firefox ESR 11 | not dead 12 | not IE 9-11 # For IE 9-11 support, remove 'not'. -------------------------------------------------------------------------------- /ui/build.sh: -------------------------------------------------------------------------------- 1 | yarn run build:prod 2 | -------------------------------------------------------------------------------- /ui/dev.sh: -------------------------------------------------------------------------------- 1 | yarn run start 2 | -------------------------------------------------------------------------------- /ui/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | 'browserName': 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 31 | } 32 | }; -------------------------------------------------------------------------------- /ui/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('Welcome to pea!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /ui/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ui/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/pea'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pea", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve --open --proxy-config src/proxy.conf.js --host 0.0.0.0", 7 | "test": "ng test --watch", 8 | "test:none": "echo test none", 9 | "test:ci": "ng test --watch=false --browsers ChromeHeadless", 10 | "test:coverage": "ng test --watch=true --code-coverage=true", 11 | "test:coverage:ci": "ng test --watch=false --code-coverage=true --browsers ChromeHeadless", 12 | "build:dev": "ng build --progress --output-path ../public", 13 | "build:prod": "ng build --progress --prod --output-path ../public", 14 | "lint": "ng lint", 15 | "e2e": "ng e2e" 16 | }, 17 | "private": true, 18 | "dependencies": { 19 | "@angular/animations": "~8.2.0", 20 | "@angular/common": "~8.2.0", 21 | "@angular/compiler": "~8.2.0", 22 | "@angular/core": "~8.2.0", 23 | "@angular/forms": "~8.2.0", 24 | "@angular/platform-browser": "~8.2.0", 25 | "@angular/platform-browser-dynamic": "~8.2.0", 26 | "@angular/router": "~8.2.0", 27 | "@ngx-translate/core": "^11.0.1", 28 | "@ngx-translate/http-loader": "^4.0.0", 29 | "ng-zorro-antd": "^8.5.0", 30 | "rxjs": "~6.4.0", 31 | "tslib": "^1.10.0", 32 | "xterm": "^3.14.5", 33 | "zone.js": "~0.9.1" 34 | }, 35 | "devDependencies": { 36 | "@angular-devkit/build-angular": "~0.802.2", 37 | "@angular/cli": "~8.2.2", 38 | "@angular/compiler-cli": "~8.2.0", 39 | "@angular/language-service": "~8.2.0", 40 | "@types/jasmine": "~3.3.8", 41 | "@types/jasminewd2": "~2.0.3", 42 | "@types/node": "~8.9.4", 43 | "codelyzer": "^5.0.0", 44 | "jasmine-core": "~3.4.0", 45 | "jasmine-spec-reporter": "~4.2.1", 46 | "karma": "~4.1.0", 47 | "karma-chrome-launcher": "~2.2.0", 48 | "karma-coverage-istanbul-reporter": "~2.0.1", 49 | "karma-jasmine": "~2.0.1", 50 | "karma-jasmine-html-reporter": "^1.4.0", 51 | "protractor": "~5.4.0", 52 | "ts-node": "~7.0.0", 53 | "tslint": "~5.15.0", 54 | "typescript": "~3.5.3" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ui/src/app/api/api-code.interceptor.ts: -------------------------------------------------------------------------------- 1 | import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse } from '@angular/common/http' 2 | import { Injectable, Injector, isDevMode } from '@angular/core' 3 | import { NzMessageService } from 'ng-zorro-antd' 4 | import { Observable, of } from 'rxjs' 5 | import { mergeMap } from 'rxjs/operators' 6 | 7 | import { APICODE, ApiResObj } from '../model/api.model' 8 | 9 | @Injectable() 10 | export class ApiCodeInterceptor implements HttpInterceptor { 11 | message: NzMessageService 12 | constructor(private inj: Injector) { 13 | this.message = this.inj.get(NzMessageService) 14 | } 15 | intercept(req: HttpRequest, next: HttpHandler): Observable { 16 | if (req.url.startsWith('./assets')) { 17 | return next.handle(req) 18 | } else { 19 | return next.handle(req).pipe(mergeMap((event: HttpEvent, i: number) => { 20 | // tslint:disable-next-line:no-bitwise 21 | if (event instanceof HttpResponse && ~(event.status / 100) < 3) { 22 | const res = event as HttpResponse 23 | if (res.body instanceof Blob) { 24 | return of(event) 25 | } 26 | if (isDevMode()) { 27 | // console.log(req.url, res.body) 28 | } 29 | const code = res.body.code 30 | if (APICODE.NOT_LOGIN === code) { 31 | } else if (APICODE.OK !== code) { 32 | const errMsg = res.body.msg 33 | this.message.error(errMsg) 34 | return Observable.create(obs => obs.error(event)) 35 | } else { 36 | return of(event) 37 | } 38 | } else { 39 | return of(event) 40 | } 41 | })) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/app/api/base.service.ts: -------------------------------------------------------------------------------- 1 | export class BaseService { 2 | 3 | DEFAULT_DEBOUNCE_TIME = 300 4 | 5 | API_BASE = '/api' 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/app/api/gatling.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http' 2 | import { Injectable } from '@angular/core' 3 | import { NzMessageService } from 'ng-zorro-antd' 4 | 5 | import { PeaMember } from '../model/pea.model' 6 | import { newWS } from '../util/ws' 7 | import { BaseService } from './base.service' 8 | 9 | @Injectable({ 10 | providedIn: 'root' 11 | }) 12 | export class GatlingService extends BaseService { 13 | 14 | wsErrMsg = 'Create Websocket error, see the console for more details.' 15 | 16 | constructor( 17 | private http: HttpClient, 18 | private msgService: NzMessageService, 19 | ) { super() } 20 | 21 | monitor(member: PeaMember) { 22 | const ws = newWS(`${this.API_BASE}/gatling/monitor`, `${member.address}:${member.port}`) 23 | ws.onerror = (event) => { 24 | console.error(event) 25 | this.msgService.warning(this.wsErrMsg) 26 | } 27 | return ws 28 | } 29 | 30 | compiler(member: PeaMember) { 31 | const ws = newWS(`${this.API_BASE}/gatling/compiler`, `${member.address}:${member.port}`) 32 | ws.onerror = (event) => { 33 | console.error(event) 34 | this.msgService.warning(this.wsErrMsg) 35 | } 36 | return ws 37 | } 38 | 39 | response(member: PeaMember) { 40 | const ws = newWS(`${this.API_BASE}/gatling/response`, `${member.address}:${member.port}`) 41 | ws.onerror = (event) => { 42 | console.error(event) 43 | this.msgService.warning(this.wsErrMsg) 44 | } 45 | return ws 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /ui/src/app/api/home.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http' 2 | import { Injectable } from '@angular/core' 3 | 4 | import { ApiRes } from '../model/api.model' 5 | import { 6 | JobWorkerStatus, 7 | PeaMember, 8 | ReporterJobStatus, 9 | RunScriptJob, 10 | Simulations, 11 | SingleHttpScenarioJob, 12 | UnionLoadMessage, 13 | WorkersAvailable, 14 | } from '../model/pea.model' 15 | import { BaseService } from './base.service' 16 | 17 | @Injectable({ 18 | providedIn: 'root' 19 | }) 20 | export class HomeService extends BaseService { 21 | 22 | constructor(private http: HttpClient) { super() } 23 | 24 | getRunningJobs() { 25 | return this.http.get>(`${this.API_BASE}/jobs`) 26 | } 27 | 28 | getJobDetails(runId: string) { 29 | return this.http.get>(`${this.API_BASE}/job/${runId}`) 30 | } 31 | 32 | getLocalReports() { 33 | return this.http.get>(`${this.API_BASE}/reports`) 34 | } 35 | 36 | getWorkers() { 37 | return this.http.get>(`${this.API_BASE}/workers`) 38 | } 39 | 40 | stopWorkers(workers: PeaMember[]) { 41 | return this.http.post>(`${this.API_BASE}/stop`, { workers: workers }) 42 | } 43 | 44 | compile(workers: PeaMember[], pull: boolean) { 45 | return this.http.post>(`${this.API_BASE}/compile`, { workers: workers, pull: pull }) 46 | } 47 | 48 | getSimulations() { 49 | return this.http.get>(`${this.API_BASE}/simulations`) 50 | } 51 | 52 | runSingleHttpScenarioJob(load: SingleHttpScenarioJob) { 53 | return this.http.post>(`${this.API_BASE}/single`, load) 54 | } 55 | 56 | runScriptJob(load: RunScriptJob) { 57 | return this.http.post>(`${this.API_BASE}/script`, load) 58 | } 59 | 60 | } 61 | 62 | export interface WorkerData { 63 | member?: PeaMember 64 | status?: JobWorkerStatus 65 | request?: UnionLoadMessage 66 | } 67 | 68 | export interface WorkersBoolResponse { 69 | result: boolean 70 | errors: object 71 | } 72 | 73 | export interface LocalReport { 74 | name?: string 75 | last?: string 76 | } 77 | -------------------------------------------------------------------------------- /ui/src/app/api/resource.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http' 2 | import { Injectable } from '@angular/core' 3 | 4 | import { ApiRes } from '../model/api.model' 5 | import { ResourceInfo } from '../model/pea.model' 6 | import { BaseService } from './base.service' 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ResourceService extends BaseService { 12 | 13 | API_BASE_RESOURCE = `${this.API_BASE}/resource` 14 | constructor(private http: HttpClient) { super() } 15 | 16 | read1k(path: string, isLibs: boolean) { 17 | return this.http.get>(`${this.API_BASE_RESOURCE}${isLibs ? '/jar' : ''}/read1k?path=${path}`) 18 | } 19 | 20 | list(file: string, isLibs: boolean) { 21 | return this.http.post>(`${this.API_BASE_RESOURCE}${isLibs ? '/jar' : ''}/list`, { file: file || '' }) 22 | } 23 | 24 | remove(file: string, isLibs: boolean) { 25 | return this.http.post>(`${this.API_BASE_RESOURCE}${isLibs ? '/jar' : ''}/remove`, { file: file }) 26 | } 27 | 28 | newFolder(path: string, name: string, isLibs: boolean) { 29 | return this.http.put>(`${this.API_BASE_RESOURCE}${isLibs ? '/jar' : ''}/folder`, { path: path, name: name }) 30 | } 31 | 32 | getDownloadLink(isLibs: boolean) { 33 | return `${this.API_BASE_RESOURCE}${isLibs ? '/jar' : ''}/download` 34 | } 35 | 36 | download(path: string, isLibs: boolean) { 37 | const url = `${this.API_BASE_RESOURCE}${isLibs ? '/jar' : ''}/download?path=${path}` 38 | this.http.get(url, { responseType: 'blob' as 'json' }).subscribe(res => { 39 | const link = window.URL.createObjectURL(res) 40 | window.open(link) 41 | }) 42 | } 43 | 44 | downloadLink(path: string, isLibs: boolean) { 45 | return `${this.API_BASE_RESOURCE}${isLibs ? '/jar' : ''}/download?path=${path}` 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /ui/src/app/api/xterm.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { ITerminalOptions, ITheme } from 'xterm' 3 | 4 | import { BaseService } from './base.service' 5 | 6 | @Injectable({ 7 | providedIn: 'root' 8 | }) 9 | export class XtermService extends BaseService { 10 | 11 | theme: ITheme = { 12 | foreground: 'lightslategray', 13 | background: 'white', 14 | } 15 | 16 | option: ITerminalOptions = { 17 | theme: this.theme, 18 | allowTransparency: true, 19 | cursorBlink: false, 20 | cursorStyle: 'block', 21 | fontFamily: 'monospace', 22 | fontSize: 12, 23 | disableStdin: true, 24 | } 25 | 26 | getDefaultTheme() { 27 | return this.theme 28 | } 29 | 30 | getDefaultOption() { 31 | return this.option 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { path: '', pathMatch: 'full', redirectTo: '/shoot' }, 6 | { path: 'jobs', loadChildren: () => import('./pages/running-jobs/running-jobs.module').then(m => m.RunningJobsModule) }, 7 | { path: 'peas', loadChildren: () => import('./pages/worker-peas/worker-peas.module').then(m => m.WorkerPeasModule) }, 8 | { path: 'shoot', loadChildren: () => import('./pages/lets-shoot/lets-shoot.module').then(m => m.LetsShootModule) }, 9 | { path: 'reports', loadChildren: () => import('./pages/local-reports/local-reports.module').then(m => m.LocalReportsModule) }, 10 | { path: 'simulations', loadChildren: () => import('./pages/simulations/simulations.module').then(m => m.SimulationsModule) }, 11 | { path: 'resources', loadChildren: () => import('./pages/local-resources/local-resources.module').then(m => m.LocalResourcesModule) }, 12 | { path: 'libs', loadChildren: () => import('./pages/local-resources/local-resources.module').then(m => m.LocalResourcesModule) }, 13 | { path: '**', redirectTo: '' }, 14 | ] 15 | 16 | @NgModule({ 17 | imports: [RouterModule.forRoot(routes)], 18 | exports: [RouterModule] 19 | }) 20 | export class AppRoutingModule { } 21 | -------------------------------------------------------------------------------- /ui/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | text-rendering: optimizeLegibility; 4 | -webkit-font-smoothing: antialiased; 5 | -moz-osx-font-smoothing: grayscale; 6 | } 7 | 8 | .app-layout { 9 | height: 100vh; 10 | } 11 | 12 | .menu-sidebar { 13 | position: relative; 14 | z-index: 10; 15 | min-height: 100vh; 16 | box-shadow: 2px 0 6px rgba(0, 21, 41, .35); 17 | } 18 | 19 | .header-trigger { 20 | height: 64px; 21 | padding: 20px 24px; 22 | font-size: 20px; 23 | cursor: pointer; 24 | transition: all .3s, padding 0s; 25 | } 26 | 27 | .header-help { 28 | float: right; 29 | margin-right: 32px; 30 | } 31 | 32 | .header-help button { 33 | margin-left: 4px; 34 | } 35 | 36 | .trigger:hover { 37 | color: #1890ff; 38 | } 39 | 40 | .sidebar-logo { 41 | position: relative; 42 | height: 48px; 43 | padding-left: 24px; 44 | overflow: hidden; 45 | line-height: 48px; 46 | background: #001529; 47 | transition: all .3s; 48 | } 49 | 50 | .sidebar-logo img { 51 | border-radius: 16px; 52 | display: inline-block; 53 | height: 32px; 54 | width: 32px; 55 | vertical-align: middle; 56 | } 57 | 58 | .sidebar-logo h1 { 59 | display: inline-block; 60 | margin: 0 0 0 16px; 61 | color: orange; 62 | font-weight: 600; 63 | font-size: 14px; 64 | font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif; 65 | vertical-align: middle; 66 | } 67 | 68 | nz-header { 69 | padding: 0; 70 | width: 100%; 71 | z-index: 2; 72 | } 73 | 74 | .app-header { 75 | position: relative; 76 | height: 48px; 77 | padding: 0; 78 | background: #fff; 79 | box-shadow: 0 1px 4px rgba(0, 21, 41, .08); 80 | } 81 | 82 | nz-content { 83 | margin: 24px; 84 | } 85 | 86 | .inner-content { 87 | padding: 24px; 88 | background: #fff; 89 | height: 100%; 90 | } 91 | -------------------------------------------------------------------------------- /ui/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core' 2 | import { TranslateService } from '@ngx-translate/core' 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.css'] 8 | }) 9 | export class AppComponent implements OnInit { 10 | 11 | isCollapsed = false 12 | lang = 'en' 13 | KEY_LANG = 'PEA_LANG' 14 | 15 | ngOnInit() { 16 | const lang = localStorage.getItem(this.KEY_LANG) 17 | if (lang === 'cn' || lang === 'en') { 18 | this.lang = lang 19 | this.changeLang() 20 | } 21 | } 22 | 23 | changeLang() { 24 | const except = this.lang 25 | switch (this.lang) { 26 | case 'cn': 27 | this.lang = 'en' 28 | break 29 | case 'en': 30 | this.lang = 'cn' 31 | break 32 | } 33 | this.translate.use(except) 34 | localStorage.setItem(this.KEY_LANG, except) 35 | } 36 | 37 | constructor(private translate: TranslateService) { 38 | // translate.setDefaultLang('en') 39 | translate.use('cn') 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { registerLocaleData } from '@angular/common' 2 | import { HTTP_INTERCEPTORS, HttpClient, HttpClientModule } from '@angular/common/http' 3 | import en from '@angular/common/locales/en' 4 | import { NgModule } from '@angular/core' 5 | import { FormsModule } from '@angular/forms' 6 | import { BrowserModule } from '@angular/platform-browser' 7 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations' 8 | import { TranslateLoader, TranslateModule } from '@ngx-translate/core' 9 | import { TranslateHttpLoader } from '@ngx-translate/http-loader' 10 | import { en_US, NgZorroAntdModule, NZ_I18N } from 'ng-zorro-antd' 11 | 12 | import { ApiCodeInterceptor } from './api/api-code.interceptor' 13 | import { AppRoutingModule } from './app-routing.module' 14 | import { AppComponent } from './app.component' 15 | import { IconsProviderModule } from './icons-provider.module' 16 | 17 | registerLocaleData(en) 18 | 19 | export function I18nHttpLoaderFactory(http: HttpClient) { 20 | return new TranslateHttpLoader(http, './assets/i18n/', '.json') 21 | } 22 | 23 | @NgModule({ 24 | declarations: [ 25 | AppComponent 26 | ], 27 | imports: [ 28 | BrowserModule, 29 | FormsModule, 30 | HttpClientModule, 31 | BrowserAnimationsModule, 32 | TranslateModule.forRoot({ 33 | loader: { 34 | provide: TranslateLoader, 35 | useFactory: I18nHttpLoaderFactory, 36 | deps: [HttpClient] 37 | } 38 | }), 39 | NgZorroAntdModule, 40 | IconsProviderModule, 41 | AppRoutingModule, 42 | ], 43 | providers: [ 44 | { provide: NZ_I18N, useValue: en_US }, 45 | { provide: HTTP_INTERCEPTORS, useClass: ApiCodeInterceptor, multi: true }, 46 | ], 47 | bootstrap: [AppComponent] 48 | }) 49 | export class AppModule { } 50 | -------------------------------------------------------------------------------- /ui/src/app/icons-provider.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { 3 | CheckSquareOutline, 4 | CloudOutline, 5 | CodeOutline, 6 | DashboardOutline, 7 | DeleteOutline, 8 | DesktopOutline, 9 | DotChartOutline, 10 | DownloadOutline, 11 | FileOutline, 12 | FolderOpenOutline, 13 | FolderOutline, 14 | HomeOutline, 15 | InfoCircleOutline, 16 | MenuFoldOutline, 17 | MenuUnfoldOutline, 18 | NumberOutline, 19 | PlusOutline, 20 | RadarChartOutline, 21 | RiseOutline, 22 | ScanOutline, 23 | ScheduleOutline, 24 | StopOutline, 25 | SyncOutline, 26 | UploadOutline, 27 | } from '@ant-design/icons-angular/icons' 28 | import { NZ_ICONS } from 'ng-zorro-antd' 29 | 30 | const icons = [ 31 | MenuFoldOutline, 32 | MenuUnfoldOutline, 33 | DashboardOutline, 34 | DotChartOutline, 35 | NumberOutline, 36 | DesktopOutline, 37 | ScanOutline, 38 | DeleteOutline, 39 | RiseOutline, 40 | CodeOutline, 41 | StopOutline, 42 | ScheduleOutline, 43 | RadarChartOutline, 44 | SyncOutline, 45 | InfoCircleOutline, 46 | FolderOutline, 47 | FileOutline, 48 | DeleteOutline, 49 | HomeOutline, 50 | UploadOutline, 51 | PlusOutline, 52 | FolderOpenOutline, 53 | DownloadOutline, 54 | CheckSquareOutline, 55 | CloudOutline, 56 | ] 57 | 58 | @NgModule({ 59 | providers: [ 60 | { provide: NZ_ICONS, useValue: icons } 61 | ] 62 | }) 63 | export class IconsProviderModule { 64 | } 65 | -------------------------------------------------------------------------------- /ui/src/app/model/api.model.ts: -------------------------------------------------------------------------------- 1 | export interface ApiReq { 2 | path?: string 3 | body?: Object 4 | extra?: Object 5 | } 6 | 7 | export interface DataBody { 8 | total?: number 9 | list?: T 10 | } 11 | 12 | export interface ApiRes { 13 | code: string 14 | msg: string 15 | data?: T & DataBody 16 | } 17 | 18 | export interface ApiResObj { 19 | code: string 20 | msg: string 21 | data?: Object 22 | } 23 | 24 | export interface SelectOption { 25 | label?: string 26 | value?: any 27 | } 28 | 29 | export class ActorEvent { 30 | code?: string 31 | msg?: string 32 | data?: T & DataBody 33 | type?: string = 'init' || 'list' || 'item' || 'over' || 'notify' 34 | } 35 | 36 | export const APICODE = { 37 | DEFAULT: '00000', 38 | OK: '10000', 39 | INVALID: '20000', 40 | ERROR: '90000', 41 | NOT_LOGIN: '90001', 42 | PERMISSION_DENIED: '90002', 43 | } 44 | 45 | export const ActorEventType = { 46 | INIT: 'init', 47 | LIST: 'list', 48 | ITEM: 'item', 49 | OVER: 'over', 50 | NOTIFY: 'notify', 51 | ERROR: 'error', 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/app/pages/lets-shoot/lets-shoot.component.css: -------------------------------------------------------------------------------- 1 | .option-title { 2 | border-bottom: 1px solid lightgrey; 3 | } 4 | 5 | .option-title>i { 6 | margin-right: 4px; 7 | } 8 | 9 | .option-desc { 10 | max-height: 80px; 11 | overflow: auto; 12 | color: lightgrey; 13 | white-space: normal; 14 | word-break: break-all; 15 | word-wrap: break-word; 16 | } 17 | 18 | .desc { 19 | cursor: pointer; 20 | margin-top: 4px; 21 | font-size: small; 22 | color: lightslategray; 23 | border: 1px solid lightgray; 24 | border-radius: 4px; 25 | padding: 8px; 26 | min-height: 120px; 27 | max-height: 200px; 28 | overflow: auto; 29 | white-space: normal; 30 | word-break: break-all; 31 | word-wrap: break-word; 32 | } 33 | 34 | .desc:hover { 35 | color: lightseagreen; 36 | } 37 | 38 | .btn-shoot { 39 | padding: 2px; 40 | cursor: pointer; 41 | border: 1px solid lightgrey; 42 | box-shadow: 0px 0px 5px lightgrey; 43 | } 44 | 45 | .btn-shoot:hover { 46 | box-shadow: 0px 0px 5px lightcoral; 47 | transform: scale(1.1); 48 | } 49 | 50 | .btn-shoot img { 51 | height: 36px; 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/app/pages/lets-shoot/lets-shoot.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { SharedModule } from '../shared/shared.module' 4 | import { LetsShootComponent } from './lets-shoot.component' 5 | import { LetsShootRoutingModule } from './lets-shoot.routing.module' 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | LetsShootRoutingModule, 11 | ], 12 | declarations: [ 13 | LetsShootComponent, 14 | ], 15 | exports: [] 16 | }) 17 | export class LetsShootModule { } 18 | -------------------------------------------------------------------------------- /ui/src/app/pages/lets-shoot/lets-shoot.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { LetsShootComponent } from './lets-shoot.component' 5 | 6 | const routes: Routes = [ 7 | { path: '', component: LetsShootComponent }, 8 | ] 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class LetsShootRoutingModule { } 15 | -------------------------------------------------------------------------------- /ui/src/app/pages/local-reports/local-reports.component.css: -------------------------------------------------------------------------------- 1 | .desc { 2 | color: lightslategray; 3 | font-size: small; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/app/pages/local-reports/local-reports.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | {{item.name}} 7 | 8 | {{item.last}} 9 | 10 | 11 | 12 | 13 | {{'item.total'|translate}} {{total}} {{'item.items'|translate}} 14 | 15 | 16 | -------------------------------------------------------------------------------- /ui/src/app/pages/local-reports/local-reports.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core' 2 | import { HomeService, LocalReport } from 'src/app/api/home.service' 3 | 4 | @Component({ 5 | selector: 'app-local-reports', 6 | templateUrl: './local-reports.component.html', 7 | styleUrls: ['./local-reports.component.css'] 8 | }) 9 | export class LocalReportsComponent implements OnInit { 10 | 11 | items: LocalReport[] = [] 12 | 13 | constructor( 14 | private homeService: HomeService, 15 | ) { } 16 | 17 | open(item: LocalReport) { 18 | window.open(`${location.protocol}//${location.host}/report/${item.name}`) 19 | } 20 | 21 | ngOnInit() { 22 | this.homeService.getLocalReports().subscribe(res => { 23 | this.items = res.data 24 | }) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/src/app/pages/local-reports/local-reports.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { SharedModule } from '../shared/shared.module' 4 | import { LocalReportsComponent } from './local-reports.component' 5 | import { LocalReportsRoutingModule } from './local-reports.routing.module' 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | LocalReportsRoutingModule, 11 | ], 12 | declarations: [ 13 | LocalReportsComponent, 14 | ], 15 | exports: [] 16 | }) 17 | export class LocalReportsModule { } 18 | -------------------------------------------------------------------------------- /ui/src/app/pages/local-reports/local-reports.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { LocalReportsComponent } from './local-reports.component' 5 | 6 | const routes: Routes = [ 7 | { path: '', component: LocalReportsComponent }, 8 | ] 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class LocalReportsRoutingModule { } 15 | -------------------------------------------------------------------------------- /ui/src/app/pages/local-resources/local-resources.component.css: -------------------------------------------------------------------------------- 1 | .file { 2 | cursor: pointer; 3 | margin-left: 2px; 4 | } 5 | 6 | .md5 { 7 | font-size: small; 8 | color: lightslategray; 9 | } 10 | 11 | .tail { 12 | float: right; 13 | font-size: small; 14 | } 15 | 16 | .actions { 17 | width: 32px; 18 | text-align: right; 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/pages/local-resources/local-resources.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | 11 | 12 |
13 | 14 | 15 |
16 | 17 | 18 | 19 | {{item.value}} 20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 | 29 | 30 | {{item.filename}} 31 | 32 | 33 | md5:{{item.md5}} 34 | 35 | 36 | {{itemSize(item)}} 37 | 38 | {{itemDate(item)}} 39 | 40 |
41 | 42 | 50 | 51 |
52 |
53 |
54 | 56 | 57 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /ui/src/app/pages/local-resources/local-resources.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { SharedModule } from '../shared/shared.module' 4 | import { LocalResourcesComponent } from './local-resources.component' 5 | import { LocalResourcesRoutingModule } from './local-resources.routing.module' 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | LocalResourcesRoutingModule, 11 | ], 12 | declarations: [ 13 | LocalResourcesComponent, 14 | ], 15 | exports: [] 16 | }) 17 | export class LocalResourcesModule { } 18 | -------------------------------------------------------------------------------- /ui/src/app/pages/local-resources/local-resources.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { LocalResourcesComponent } from './local-resources.component' 5 | 6 | const routes: Routes = [ 7 | { path: '', component: LocalResourcesComponent }, 8 | ] 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class LocalResourcesRoutingModule { } 15 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/job-summary/job-summary.component.css: -------------------------------------------------------------------------------- 1 | .title { 2 | font-weight: 500; 3 | } 4 | 5 | .item i { 6 | color: lightseagreen; 7 | } 8 | 9 | .item span { 10 | margin-left: 4px; 11 | } 12 | 13 | .time { 14 | margin-left: 8px; 15 | } 16 | 17 | .status { 18 | margin-left: 16px; 19 | color: purple; 20 | } 21 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/job-summary/job-summary.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | {{job.runId}} 5 | 6 | 7 | 8 | {{startTime}} 9 | 10 | 11 | 12 | {{job.status}} 13 | 14 |
15 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/job-summary/job-summary.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core' 2 | import { ReporterJobStatus } from 'src/app/model/pea.model' 3 | 4 | @Component({ 5 | selector: 'app-job-summary', 6 | templateUrl: './job-summary.component.html', 7 | styleUrls: ['./job-summary.component.css'] 8 | }) 9 | export class JobSummaryComponent { 10 | 11 | startTime = '' 12 | job: ReporterJobStatus = {} 13 | @Input() 14 | set data(data: ReporterJobStatus) { 15 | if (data) { 16 | this.job = data 17 | this.startTime = new Date(this.job.start).toLocaleString() 18 | } 19 | } 20 | 21 | constructor() { } 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/running-job/running-job.component.css: -------------------------------------------------------------------------------- 1 | .member { 2 | margin: 10px 0; 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/running-job/running-job.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | 6 |
7 |
8 |
9 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/running-job/running-job.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnDestroy, OnInit } from '@angular/core' 2 | import { ActivatedRoute } from '@angular/router' 3 | import { HomeService, WorkerData } from 'src/app/api/home.service' 4 | import { ReporterJobStatus } from 'src/app/model/pea.model' 5 | 6 | @Component({ 7 | selector: 'app-running-job', 8 | templateUrl: './running-job.component.html', 9 | styleUrls: ['./running-job.component.css'] 10 | }) 11 | export class RunningJobComponent implements OnInit, OnDestroy { 12 | 13 | runId = '' 14 | job: ReporterJobStatus = {} 15 | members: WorkerData[] = [] 16 | constructor( 17 | private route: ActivatedRoute, 18 | private homeService: HomeService, 19 | ) { } 20 | 21 | loadJobDetails() { 22 | if (this.runId) { 23 | this.homeService.getJobDetails(this.runId).subscribe(res => { 24 | this.job = res.data 25 | const load = res.data.load 26 | let tmp: WorkerData[] = [] 27 | if (load) { 28 | if (load.jobs && load.jobs.length > 0) { 29 | load.jobs.forEach(item => { 30 | tmp.push({ member: item.worker, request: item.load }) 31 | }) 32 | } else if (load.load && load.workers && load.workers.length > 0) { 33 | load.workers.forEach(item => { 34 | tmp.push({ member: item, request: load.load }) 35 | }) 36 | } 37 | } 38 | this.members = tmp 39 | }) 40 | } 41 | } 42 | 43 | ngOnInit(): void { 44 | this.route.params.subscribe(params => { 45 | this.runId = params['runId'] 46 | this.loadJobDetails() 47 | }) 48 | } 49 | 50 | ngOnDestroy(): void { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/running-jobs.component.css: -------------------------------------------------------------------------------- 1 | .workers { 2 | cursor: pointer; 3 | } 4 | 5 | .tail { 6 | float: right; 7 | font-size: small; 8 | } 9 | 10 | .tail .status { 11 | font-weight: 500; 12 | } 13 | 14 | .time .label { 15 | color: lightseagreen; 16 | } 17 | 18 | .time .value { 19 | color: lightslategray; 20 | font-size: smaller; 21 | } 22 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/running-jobs.component.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 9 | 10 | 11 | 12 | 13 | 14 | {{item.load.type}} 15 | {{item.runId}} 16 | 17 | {{item.status}} 18 | 19 | 20 | 21 | 22 | {{'tips.startTime'|translate}}: 23 | 24 | {{itemDate(item)}} 25 | 26 | 27 | 28 | 29 | 30 | 31 | {{'item.total'|translate}} {{total}} {{'item.items'|translate}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/running-jobs.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core' 2 | import { Router } from '@angular/router' 3 | import { HomeService } from 'src/app/api/home.service' 4 | import { LoadTypes, ReporterJobStatus, StatusColors } from 'src/app/model/pea.model' 5 | 6 | @Component({ 7 | selector: 'app-running-jobs', 8 | templateUrl: './running-jobs.component.html', 9 | styleUrls: ['./running-jobs.component.css'] 10 | }) 11 | export class RunningJobsComponent implements OnInit { 12 | 13 | items: ReporterJobStatus[] = [] 14 | 15 | constructor( 16 | private homeService: HomeService, 17 | private router: Router, 18 | ) { } 19 | 20 | open(item: ReporterJobStatus) { 21 | this.router.navigateByUrl(`/jobs/${item.runId}`) 22 | } 23 | 24 | statusColor(item: ReporterJobStatus) { 25 | return StatusColors[item.status] 26 | } 27 | 28 | itemColor(item: ReporterJobStatus) { 29 | if (item.load) { 30 | switch (item.load.type) { 31 | case LoadTypes.SINGLE: 32 | return 'magenta' 33 | case LoadTypes.SCRIPT: 34 | return 'cyan' 35 | case LoadTypes.PROGRAM: 36 | return 'blue' 37 | } 38 | } else { 39 | return '' 40 | } 41 | } 42 | 43 | itemDate(item: ReporterJobStatus) { 44 | return new Date(item.start).toLocaleString() 45 | } 46 | 47 | ngOnInit() { 48 | this.homeService.getRunningJobs().subscribe(res => { 49 | this.items = res.data 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/running-jobs.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { SharedModule } from '../shared/shared.module' 4 | import { JobSummaryComponent } from './job-summary/job-summary.component' 5 | import { RunningJobComponent } from './running-job/running-job.component' 6 | import { RunningJobsComponent } from './running-jobs.component' 7 | import { RunningJobsRoutingModule } from './running-jobs.routing.module' 8 | import { WorkerStatusComponent } from './worker-status/worker-status.component' 9 | 10 | @NgModule({ 11 | imports: [ 12 | SharedModule, 13 | RunningJobsRoutingModule, 14 | ], 15 | declarations: [ 16 | RunningJobsComponent, 17 | RunningJobComponent, 18 | JobSummaryComponent, 19 | WorkerStatusComponent, 20 | ], 21 | exports: [] 22 | }) 23 | export class RunningJobsModule { } 24 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/running-jobs.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { RunningJobComponent } from './running-job/running-job.component' 5 | import { RunningJobsComponent } from './running-jobs.component' 6 | 7 | const routes: Routes = [ 8 | { path: '', component: RunningJobsComponent }, 9 | { path: ':runId', component: RunningJobComponent }, 10 | ] 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class RunningJobsRoutingModule { } 17 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/worker-status/worker-status.component.css: -------------------------------------------------------------------------------- 1 | .item { 2 | margin-bottom: 4px; 3 | border-bottom: 1px solid lightgray; 4 | } 5 | 6 | .label { 7 | font-weight: 500; 8 | margin-right: 4px; 9 | } 10 | 11 | .value { 12 | color: darkcyan; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/worker-status/worker-status.component.html: -------------------------------------------------------------------------------- 1 |
2 | {{item.worker}}: 3 | {{item.status}} 4 |
5 | -------------------------------------------------------------------------------- /ui/src/app/pages/running-jobs/worker-status/worker-status.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core' 2 | import { JobWorkerStatus } from 'src/app/model/pea.model' 3 | 4 | @Component({ 5 | selector: 'app-worker-status', 6 | templateUrl: './worker-status.component.html', 7 | styleUrls: ['./worker-status.component.css'] 8 | }) 9 | export class WorkerStatusComponent { 10 | 11 | items: { worker: string, status: string }[] = [] 12 | 13 | @Input() 14 | set data(data: { [k: string]: JobWorkerStatus }) { 15 | if (data) { 16 | this.items = Object.keys(data).map(k => { 17 | return { worker: k, status: data[k].status } 18 | }) 19 | } 20 | } 21 | 22 | constructor() { } 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/assertions/assertions.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/ui/src/app/pages/shared/assertions/assertions.component.css -------------------------------------------------------------------------------- /ui/src/app/pages/shared/assertions/assertions.component.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/assertions/assertions.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core' 2 | import { TranslateService } from '@ngx-translate/core' 3 | import { HttpAssertionParam } from 'src/app/model/pea.model' 4 | 5 | @Component({ 6 | selector: 'app-assertions', 7 | templateUrl: './assertions.component.html', 8 | styleUrls: ['./assertions.component.css'] 9 | }) 10 | export class AssertionsComponent { 11 | 12 | str = `` 13 | placeholder = `{ 14 | "status" : {"list":[{"op":"eq", "except":200}]}, 15 | "body" : {"list": [{"op":"jsonpath", "path":"$.msg", "except":"success"}]} 16 | }` 17 | assertions: HttpAssertionParam = { status: { list: [] }, header: { list: [] }, body: { list: [] } } 18 | @Input() 19 | set data(value: HttpAssertionParam) { 20 | if (value) this.assertions = value 21 | } 22 | @Output() 23 | dataChange = new EventEmitter() 24 | 25 | modelChange() { 26 | if (this.str) { 27 | try { 28 | this.assertions = JSON.parse(this.str) as HttpAssertionParam 29 | this.dataChange.emit(this.assertions) 30 | } catch (error) { 31 | } 32 | } 33 | } 34 | 35 | constructor( 36 | private i18nService: TranslateService, 37 | ) { } 38 | } 39 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/feeder/feeder.component.css: -------------------------------------------------------------------------------- 1 | .file { 2 | cursor: pointer; 3 | margin-left: 2px; 4 | } 5 | 6 | .md5 { 7 | font-size: small; 8 | color: lightslategray; 9 | } 10 | 11 | .tail { 12 | float: right; 13 | font-size: small; 14 | } 15 | 16 | .actions { 17 | width: 32px; 18 | text-align: right; 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/feeder/feeder.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | {{item.value}} 8 | 9 | 10 | 11 | {{feeder.path}} 12 | 13 | 14 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 |
24 | 26 | 27 | {{item.filename}} 28 | 29 | 30 | md5:{{item.md5}} 31 | 32 | 33 | {{itemSize(item)}} 34 | 35 | {{itemDate(item)}} 36 | 37 |
38 | 39 |
40 | 41 | 42 | 43 |
44 |
45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/injections-builder/injections-builder.component.css: -------------------------------------------------------------------------------- 1 | .workload { 2 | width: 100%; 3 | padding-bottom: 4px; 4 | border-bottom: 1px solid lightgray; 5 | } 6 | 7 | .line { 8 | width: 100%; 9 | margin-top: 4px; 10 | } 11 | 12 | .label { 13 | width: 84px; 14 | text-align: left; 15 | } 16 | 17 | .inj-type>i { 18 | color: lightseagreen; 19 | } 20 | 21 | .inj-type>span { 22 | color: gray; 23 | font-size: small; 24 | padding-left: 4px; 25 | } 26 | 27 | .inj-sum { 28 | margin-left: 4px; 29 | } 30 | 31 | .inj-sum>i { 32 | color: lightseagreen; 33 | } 34 | 35 | .inj-sum>span { 36 | color: gray; 37 | font-size: small; 38 | padding-left: 4px; 39 | } 40 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/member-selector/member-selector.component.css: -------------------------------------------------------------------------------- 1 | .item i { 2 | color: lightseagreen; 3 | } 4 | 5 | .hostname { 6 | margin-left: 4px; 7 | color: lightgray; 8 | font-size: small; 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/member-selector/member-selector.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | {{'tips.refreshNodes'|translate}} 6 | 7 |
8 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | {{item.member.address}}:{{item.member.port}} 21 | {{item.status.status}} 22 | 23 | {{item.member.hostname}} 24 | 25 | 26 |
27 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/oshi-info/oshi-info.component.css: -------------------------------------------------------------------------------- 1 | .item { 2 | margin-bottom: 4px; 3 | border-bottom: 1px solid lightgray; 4 | } 5 | 6 | .label { 7 | font-weight: 500; 8 | margin-right: 4px; 9 | } 10 | 11 | .value { 12 | color: darkgrey; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/oshi-info/oshi-info.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | label: 4 | {{label}} 5 |
6 |
7 | os: 8 | {{oshi.os}} 9 |
10 |
11 | cpu.name: 12 | {{oshi['cpu.name']}} 13 |
14 |
15 | cpu.physical.processor.count: 16 | {{oshi['cpu.physical.processor.count']}} 17 |
18 |
19 | cpu.logical.processor.count: 20 | {{oshi['cpu.logical.processor.count']}} 21 |
22 |
23 | memory.total: 24 | {{formatMemorySize(oshi['memory.total'])}} GB 25 |
26 |
27 | memory.available: 28 | {{formatMemorySize(oshi['memory.available'])}} GB 29 |
30 |
31 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/oshi-info/oshi-info.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core' 2 | import { Oshi } from 'src/app/model/pea.model' 3 | 4 | @Component({ 5 | selector: 'app-oshi-info', 6 | templateUrl: './oshi-info.component.html', 7 | styleUrls: ['./oshi-info.component.css'] 8 | }) 9 | export class OshiInfoComponent { 10 | 11 | GB = 1024 * 1024 * 1024 12 | oshi: Oshi = {} 13 | 14 | @Input() label: string = '' 15 | @Input() 16 | set data(data: Oshi) { 17 | this.oshi = data || {} 18 | } 19 | 20 | formatMemorySize(size: number) { 21 | return (size / this.GB).toFixed(2) 22 | } 23 | 24 | constructor() { } 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/pea-member/pea-member.component.css: -------------------------------------------------------------------------------- 1 | .title { 2 | padding-left: 8px; 3 | } 4 | 5 | .label { 6 | color: lightslategray; 7 | font-weight: 500; 8 | } 9 | 10 | .value { 11 | color: lightslategrey; 12 | font-weight: 500; 13 | } 14 | 15 | .margin { 16 | margin-left: 16px; 17 | } 18 | 19 | .table { 20 | margin-top: 8px; 21 | } 22 | 23 | .table th { 24 | padding: 0px; 25 | color: lightslategray; 26 | } 27 | 28 | .console { 29 | margin-top: 4px; 30 | padding: 4px; 31 | border: 1px solid #e8e8e8; 32 | border-radius: 4px; 33 | } 34 | 35 | .inj-type>i { 36 | color: lightseagreen; 37 | } 38 | 39 | .inj-type>span { 40 | color: gray; 41 | font-size: small; 42 | padding-left: 4px; 43 | } 44 | 45 | .inj-sum { 46 | margin-left: 4px; 47 | } 48 | 49 | .inj-sum>i { 50 | color: lightseagreen; 51 | } 52 | 53 | .inj-sum>span { 54 | color: gray; 55 | font-size: small; 56 | padding-left: 4px; 57 | } 58 | 59 | .injections { 60 | padding-bottom: 4px; 61 | margin-bottom: 8px; 62 | border-bottom: 1px solid whitesmoke; 63 | } 64 | 65 | .addr { 66 | margin-right: 8px; 67 | color: lightslategray; 68 | } 69 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/response-monitor/response-monitor.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/ui/src/app/pages/shared/response-monitor/response-monitor.component.css -------------------------------------------------------------------------------- /ui/src/app/pages/shared/response-monitor/response-monitor.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/response-monitor/response-monitor.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, ElementRef, Input, OnDestroy } from '@angular/core' 2 | import { NzMessageService } from 'ng-zorro-antd' 3 | import { Subject } from 'rxjs' 4 | import { GatlingService } from 'src/app/api/gatling.service' 5 | import { XtermService } from 'src/app/api/xterm.service' 6 | import { ActorEvent, ActorEventType } from 'src/app/model/api.model' 7 | import { PeaMember } from 'src/app/model/pea.model' 8 | import { Terminal } from 'xterm' 9 | import { fit } from 'xterm/lib/addons/fit/fit' 10 | import { webLinksInit } from 'xterm/lib/addons/webLinks/webLinks' 11 | 12 | @Component({ 13 | selector: 'app-response-monitor', 14 | templateUrl: './response-monitor.component.html', 15 | styleUrls: ['./response-monitor.component.css'] 16 | }) 17 | export class ResponseMonitorComponent implements AfterViewInit, OnDestroy { 18 | 19 | ws: WebSocket 20 | log: Subject = new Subject() 21 | xterm = new Terminal(this.xtermService.getDefaultOption()) 22 | 23 | @Input() 24 | set data(data: PeaMember) { 25 | if (data) { 26 | this.ws = this.gatlingService.response(data) 27 | this.ws.onopen = (event) => { } 28 | this.ws.onmessage = (event) => { 29 | if (event.data) { 30 | try { 31 | const res = JSON.parse(event.data) as ActorEvent 32 | if (ActorEventType.NOTIFY === res.type) { 33 | this.log.next(res.msg) 34 | } 35 | } catch (error) { 36 | this.msgService.error(error) 37 | this.ws.close() 38 | } 39 | } 40 | } 41 | } 42 | } 43 | 44 | constructor( 45 | private gatlingService: GatlingService, 46 | private msgService: NzMessageService, 47 | private xtermService: XtermService, 48 | private el: ElementRef, 49 | ) { } 50 | 51 | ngAfterViewInit(): void { 52 | const xtermEl = this.el.nativeElement.getElementsByClassName('xterm')[0] as HTMLElement 53 | this.xterm.open(xtermEl) 54 | fit(this.xterm) 55 | webLinksInit(this.xterm) 56 | this.log.subscribe(log => { 57 | this.xterm.writeln(log) 58 | }) 59 | } 60 | 61 | ngOnDestroy(): void { 62 | if (this.ws) { 63 | this.ws.close() 64 | this.ws = null 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common' 2 | import { NgModule } from '@angular/core' 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms' 4 | import { RouterModule } from '@angular/router' 5 | import { TranslateModule } from '@ngx-translate/core' 6 | import { NgZorroAntdModule } from 'ng-zorro-antd' 7 | 8 | import { AssertionsComponent } from './assertions/assertions.component' 9 | import { FeederComponent } from './feeder/feeder.component' 10 | import { InjectionsBuilderComponent } from './injections-builder/injections-builder.component' 11 | import { MemberSelectorComponent } from './member-selector/member-selector.component' 12 | import { OshiInfoComponent } from './oshi-info/oshi-info.component' 13 | import { PeaMemberComponent } from './pea-member/pea-member.component' 14 | import { ResponseMonitorComponent } from './response-monitor/response-monitor.component' 15 | import { ThrottleComponent } from './throttle/throttle.component' 16 | 17 | const COMPONENTS = [ 18 | PeaMemberComponent, 19 | InjectionsBuilderComponent, 20 | MemberSelectorComponent, 21 | OshiInfoComponent, 22 | ResponseMonitorComponent, 23 | FeederComponent, 24 | ThrottleComponent, 25 | AssertionsComponent, 26 | ] 27 | 28 | const ENTRY_COMPONENTS = [ 29 | PeaMemberComponent, 30 | ] 31 | 32 | @NgModule({ 33 | imports: [ 34 | CommonModule, 35 | FormsModule, 36 | RouterModule, 37 | ReactiveFormsModule, 38 | TranslateModule, 39 | NgZorroAntdModule, 40 | ], 41 | declarations: [ 42 | ...COMPONENTS, 43 | ], 44 | entryComponents: [ 45 | ...ENTRY_COMPONENTS, 46 | ], 47 | exports: [ 48 | CommonModule, 49 | FormsModule, 50 | RouterModule, 51 | ReactiveFormsModule, 52 | TranslateModule, 53 | NgZorroAntdModule, 54 | ...COMPONENTS, 55 | ] 56 | }) 57 | export class SharedModule { } 58 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/throttle/throttle.component.css: -------------------------------------------------------------------------------- 1 | .step-type>i { 2 | color: lightseagreen; 3 | } 4 | 5 | .step-type>span { 6 | color: gray; 7 | font-size: small; 8 | padding-left: 4px; 9 | } 10 | 11 | .step-sum { 12 | margin-left: 4px; 13 | } 14 | 15 | .step-sum>i { 16 | color: lightseagreen; 17 | } 18 | 19 | .step-sum>span { 20 | color: gray; 21 | font-size: small; 22 | padding-left: 4px; 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/app/pages/shared/throttle/throttle.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Input, Output } from '@angular/core' 2 | import { TranslateService } from '@ngx-translate/core' 3 | import { SelectOption } from 'src/app/model/api.model' 4 | import { ThrottleParam, ThrottleStep, ThrottleTypes, TimeUnit } from 'src/app/model/pea.model' 5 | 6 | @Component({ 7 | selector: 'app-throttle', 8 | templateUrl: './throttle.component.html', 9 | styleUrls: ['./throttle.component.css'] 10 | }) 11 | export class ThrottleComponent { 12 | 13 | THROTTLE_TYPES: SelectOption[] = Object.keys(ThrottleTypes).map(k => { 14 | const v = ThrottleTypes[k] 15 | return { label: this.i18nService.instant(`throttle.${v}`), value: v } 16 | }) 17 | TIME_UNITS: SelectOption[] = Object.keys(TimeUnit).map(k => { 18 | const v = TimeUnit[k] 19 | return { label: this.i18nService.instant(`time.${v}`), value: v } 20 | }) 21 | throttle: ThrottleParam = { steps: [] } 22 | current: ThrottleStep = { 23 | type: ThrottleTypes.REACH, 24 | rps: 1000, 25 | duration: { 26 | value: 1, 27 | unit: TimeUnit.TIME_UNIT_MINUTE, 28 | } 29 | } 30 | @Input() 31 | set data(value: ThrottleParam) { 32 | if (value) this.throttle = value 33 | } 34 | @Output() 35 | dataChange = new EventEmitter() 36 | 37 | constructor( 38 | private i18nService: TranslateService, 39 | ) { } 40 | 41 | add() { 42 | this.throttle.steps.push(this.copyStep(this.current)) 43 | this.throttle.steps = [...this.throttle.steps] 44 | this.dataChange.emit(this.throttle) 45 | } 46 | 47 | remove(item: ThrottleStep, index: number) { 48 | this.throttle.steps.splice(index, 1) 49 | this.throttle.steps = [...this.throttle.steps] 50 | this.dataChange.emit(this.throttle) 51 | } 52 | 53 | sumText(item: ThrottleStep) { 54 | let sum = '' 55 | if (item.rps) { 56 | sum = `${sum} Rps: ${item.rps}` 57 | } 58 | if (item.duration && item.duration.value && item.duration.unit) { 59 | sum = `${sum} ${this.i18nService.instant('item.duration')}: ${item.duration.value} ${this.i18nService.instant(`time.${item.duration.unit}`)}.` 60 | } 61 | return sum 62 | } 63 | 64 | copyStep(step: ThrottleStep): ThrottleStep { 65 | switch (step.type) { 66 | case ThrottleTypes.REACH: 67 | return { type: step.type, rps: step.rps, duration: { ...step.duration } } 68 | case ThrottleTypes.HOLD: 69 | return { type: step.type, duration: { ...step.duration } } 70 | case ThrottleTypes.JUMP: 71 | return { type: step.type, rps: step.rps } 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ui/src/app/pages/simulations/compiler-output/compiler-output.component.css: -------------------------------------------------------------------------------- 1 | .title { 2 | border-bottom: 1px solid rgb(233, 233, 233); 3 | margin-bottom: 4px; 4 | color: lightslategray; 5 | font-weight: 500; 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/app/pages/simulations/compiler-output/compiler-output.component.html: -------------------------------------------------------------------------------- 1 |
2 |
{{addr}}
3 |
4 |
5 | -------------------------------------------------------------------------------- /ui/src/app/pages/simulations/compiler-output/compiler-output.component.ts: -------------------------------------------------------------------------------- 1 | import { AfterViewInit, Component, ElementRef, Input, OnDestroy } from '@angular/core' 2 | import { NzMessageService } from 'ng-zorro-antd' 3 | import { Subject } from 'rxjs' 4 | import { GatlingService } from 'src/app/api/gatling.service' 5 | import { WorkerData } from 'src/app/api/home.service' 6 | import { XtermService } from 'src/app/api/xterm.service' 7 | import { ActorEvent, ActorEventType } from 'src/app/model/api.model' 8 | import { Terminal } from 'xterm' 9 | import { fit } from 'xterm/lib/addons/fit/fit' 10 | import { webLinksInit } from 'xterm/lib/addons/webLinks/webLinks' 11 | 12 | @Component({ 13 | selector: 'app-compiler-output', 14 | templateUrl: './compiler-output.component.html', 15 | styleUrls: ['./compiler-output.component.css'] 16 | }) 17 | export class CompilerOutputComponent implements AfterViewInit, OnDestroy { 18 | 19 | addr = '' 20 | ws: WebSocket 21 | log: Subject = new Subject() 22 | xterm = new Terminal(this.xtermService.getDefaultOption()) 23 | 24 | @Input() 25 | set data(data: WorkerData) { 26 | this.addr = `${data.member.address}:${data.member.port}` 27 | this.ws = this.gatlingService.compiler(data.member) 28 | this.ws.onopen = (event) => { } 29 | this.ws.onmessage = (event) => { 30 | if (event.data) { 31 | try { 32 | const res = JSON.parse(event.data) as ActorEvent 33 | if (ActorEventType.NOTIFY === res.type) { 34 | this.log.next(res.msg) 35 | } 36 | } catch (error) { 37 | this.msgService.error(error) 38 | this.ws.close() 39 | } 40 | } 41 | } 42 | } 43 | 44 | constructor( 45 | private gatlingService: GatlingService, 46 | private msgService: NzMessageService, 47 | private xtermService: XtermService, 48 | private el: ElementRef, 49 | ) { } 50 | 51 | ngAfterViewInit(): void { 52 | const xtermEl = this.el.nativeElement.getElementsByClassName('xterm')[0] as HTMLElement 53 | this.xterm.open(xtermEl) 54 | fit(this.xterm) 55 | webLinksInit(this.xterm) 56 | this.log.subscribe(log => { 57 | this.xterm.writeln(log) 58 | }) 59 | } 60 | 61 | ngOnDestroy(): void { 62 | if (this.ws) { 63 | this.ws.close() 64 | this.ws = null 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ui/src/app/pages/simulations/simulations.component.css: -------------------------------------------------------------------------------- 1 | @keyframes spin { 2 | from { 3 | transform: rotate(0deg); 4 | } 5 | 6 | to { 7 | transform: rotate(360deg); 8 | } 9 | } 10 | 11 | .list { 12 | max-height: 360px; 13 | overflow: auto; 14 | } 15 | 16 | i.reload { 17 | color: lightseagreen; 18 | font-size: 14px; 19 | margin-left: 4px; 20 | } 21 | 22 | i.reload:hover { 23 | color: lightcoral; 24 | animation: spin 1s linear infinite; 25 | } 26 | 27 | .sim { 28 | width: 100%; 29 | } 30 | 31 | .sim>.desc { 32 | color: lightgray; 33 | font-size: small; 34 | } 35 | 36 | .check-all { 37 | border-bottom: 1px solid rgb(233, 233, 233); 38 | margin-bottom: 4px; 39 | padding-left: 4px; 40 | padding-bottom: 4px; 41 | } 42 | 43 | .tool { 44 | float: right; 45 | } 46 | 47 | .tool>button { 48 | margin-right: 8px; 49 | font-weight: 500; 50 | } 51 | 52 | .worker { 53 | padding: 4px 0; 54 | padding-left: 4px; 55 | cursor: pointer; 56 | } 57 | 58 | .worker:hover { 59 | background-color: lightpink; 60 | } 61 | 62 | .item i { 63 | color: lightseagreen; 64 | } 65 | 66 | .hostname { 67 | margin-left: 4px; 68 | color: lightgray; 69 | font-size: small; 70 | } 71 | -------------------------------------------------------------------------------- /ui/src/app/pages/simulations/simulations.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { SharedModule } from '../shared/shared.module' 4 | import { CompilerOutputComponent } from './compiler-output/compiler-output.component' 5 | import { SimulationsComponent } from './simulations.component' 6 | import { SimulationsRoutingModule } from './simulations.routing.module' 7 | 8 | @NgModule({ 9 | imports: [ 10 | SharedModule, 11 | SimulationsRoutingModule, 12 | ], 13 | declarations: [ 14 | SimulationsComponent, 15 | CompilerOutputComponent, 16 | ], 17 | exports: [] 18 | }) 19 | export class SimulationsModule { } 20 | -------------------------------------------------------------------------------- /ui/src/app/pages/simulations/simulations.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { SimulationsComponent } from './simulations.component' 5 | 6 | const routes: Routes = [ 7 | { path: '', component: SimulationsComponent }, 8 | ] 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class SimulationsRoutingModule { } 15 | -------------------------------------------------------------------------------- /ui/src/app/pages/worker-peas/worker-peas.component.css: -------------------------------------------------------------------------------- 1 | .pea-item { 2 | width: 100%; 3 | cursor: pointer; 4 | } 5 | 6 | .pea-item i { 7 | margin-right: 4px; 8 | } 9 | 10 | .pea-item button { 11 | float: right; 12 | color: lightskyblue; 13 | margin: 0 4px; 14 | } 15 | 16 | .pea-addr i { 17 | color: lightseagreen; 18 | } 19 | 20 | .pea-hostname { 21 | margin-left: 12px; 22 | } 23 | 24 | .pea-hostname i { 25 | color: purple; 26 | } 27 | 28 | .pea-status span { 29 | margin-left: 12px; 30 | font-weight: bold; 31 | color: purple; 32 | } 33 | -------------------------------------------------------------------------------- /ui/src/app/pages/worker-peas/worker-peas.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{item.member.address}}:{{item.member.port}} 14 | 15 | 16 | 17 | {{item.member.hostname}} 18 | 19 | 20 | {{item.status.status}} 21 | 22 | 25 | 28 | 31 |
32 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /ui/src/app/pages/worker-peas/worker-peas.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core' 2 | import { NzDrawerService, NzMessageService, NzModalService } from 'ng-zorro-antd' 3 | import { HomeService, WorkerData } from 'src/app/api/home.service' 4 | import { getDefaultDrawerWidth } from 'src/app/util/drawer' 5 | 6 | import { PeaMemberComponent } from '../shared/pea-member/pea-member.component' 7 | 8 | @Component({ 9 | selector: 'app-worker-peas', 10 | templateUrl: './worker-peas.component.html', 11 | styleUrls: ['./worker-peas.component.css'] 12 | }) 13 | export class WorkerPeasComponent implements OnInit { 14 | 15 | drawerWidth = getDefaultDrawerWidth() 16 | items: WorkerData[] = [] 17 | 18 | constructor( 19 | private homeService: HomeService, 20 | private messageService: NzMessageService, 21 | private modalService: NzModalService, 22 | private drawerService: NzDrawerService, 23 | ) { } 24 | 25 | monitor(item: WorkerData) { 26 | this.drawerService.create({ 27 | nzWidth: this.drawerWidth, 28 | nzContent: PeaMemberComponent, 29 | nzContentParams: { 30 | data: item 31 | }, 32 | nzBodyStyle: { 33 | padding: '16px' 34 | }, 35 | nzClosable: false, 36 | }) 37 | } 38 | 39 | openResource(item: WorkerData) { 40 | const worker = item.member 41 | window.open(`http://${worker.address}:${worker.port}/resources`) 42 | } 43 | 44 | stop(item: WorkerData) { 45 | this.homeService.stopWorkers([item.member]).subscribe(res => { 46 | if (res.data.result) { 47 | this.loadData() 48 | this.messageService.success('OK') 49 | } else { 50 | this.modalService.create({ 51 | nzTitle: 'Fail', 52 | nzContent: JSON.stringify(res.data.errors), 53 | nzClosable: true, 54 | nzOkDisabled: true, 55 | }) 56 | } 57 | }) 58 | } 59 | 60 | loadData() { 61 | this.homeService.getWorkers().subscribe(res => { 62 | this.items = res.data 63 | }) 64 | } 65 | 66 | ngOnInit() { 67 | this.loadData() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /ui/src/app/pages/worker-peas/worker-peas.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | 3 | import { SharedModule } from '../shared/shared.module' 4 | import { WorkerPeasComponent } from './worker-peas.component' 5 | import { WorkerPeasRoutingModule } from './worker-peas.routing.module' 6 | 7 | @NgModule({ 8 | imports: [ 9 | SharedModule, 10 | WorkerPeasRoutingModule, 11 | ], 12 | declarations: [WorkerPeasComponent], 13 | exports: [] 14 | }) 15 | export class WorkerPeasModule { } 16 | -------------------------------------------------------------------------------- /ui/src/app/pages/worker-peas/worker-peas.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | import { WorkerPeasComponent } from './worker-peas.component' 5 | 6 | const routes: Routes = [ 7 | { path: '', component: WorkerPeasComponent }, 8 | ] 9 | 10 | @NgModule({ 11 | imports: [RouterModule.forChild(routes)], 12 | exports: [RouterModule] 13 | }) 14 | export class WorkerPeasRoutingModule { } 15 | -------------------------------------------------------------------------------- /ui/src/app/util/drawer.ts: -------------------------------------------------------------------------------- 1 | export function getDefaultDrawerWidth(dot = 0.6) { 2 | return Math.round(window.innerWidth * dot) 3 | } 4 | -------------------------------------------------------------------------------- /ui/src/app/util/file.ts: -------------------------------------------------------------------------------- 1 | const UNIT = 1024 2 | export function formatFileSize(size: number) { 3 | if (size < UNIT) { 4 | return `${size}B` 5 | } 6 | let tmp = size / UNIT 7 | if (tmp < UNIT) { 8 | return `${tmp.toFixed(1)}K` 9 | } 10 | tmp = tmp / UNIT 11 | if (tmp < UNIT) { 12 | return `${tmp.toFixed(1)}M` 13 | } 14 | tmp = tmp / UNIT 15 | if (tmp < UNIT) { 16 | return `${tmp.toFixed(1)}G` 17 | } 18 | return `${size}B` 19 | } 20 | -------------------------------------------------------------------------------- /ui/src/app/util/ws.ts: -------------------------------------------------------------------------------- 1 | export function newWS(path: string, host: string = null): WebSocket { 2 | let url: string 3 | if (location.protocol.startsWith('https')) { 4 | url = `wss://${host || location.host}${path}` 5 | } else { 6 | url = `ws://${host || location.host}${path}` 7 | } 8 | return new WebSocket(url) 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/ui/src/assets/.gitkeep -------------------------------------------------------------------------------- /ui/src/assets/img/logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/ui/src/assets/img/logo.gif -------------------------------------------------------------------------------- /ui/src/assets/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/ui/src/assets/img/logo.png -------------------------------------------------------------------------------- /ui/src/assets/img/shoot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/ui/src/assets/img/shoot.jpg -------------------------------------------------------------------------------- /ui/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /ui/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /ui/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ojlm/pea/6669f6af77d9bd4dee249ffa19cc2e781291e79b/ui/src/favicon.ico -------------------------------------------------------------------------------- /ui/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Pea 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /ui/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core' 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' 3 | 4 | import { AppModule } from './app/app.module' 5 | import { environment } from './environments/environment' 6 | 7 | if (environment.production) { 8 | enableProdMode() 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)) 13 | -------------------------------------------------------------------------------- /ui/src/proxy.conf.js: -------------------------------------------------------------------------------- 1 | const PROXY_CONFIG = { 2 | "**": { 3 | target: "http://localhost:9000", 4 | changeOrigin: true, 5 | secure: false, 6 | bypass: function (req) { 7 | if (req && req.headers && req.headers.accept && req.headers.accept.indexOf("html") !== -1) { 8 | console.log("Skipping proxy for browser request.") 9 | return "/index.html" 10 | } 11 | } 12 | } 13 | } 14 | 15 | module.exports = PROXY_CONFIG 16 | -------------------------------------------------------------------------------- /ui/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | 3 | i.click-icon { 4 | cursor: pointer; 5 | } 6 | 7 | i.click-icon:hover { 8 | transform: scale(1.2); 9 | } 10 | 11 | i.click-icon:active { 12 | font-weight: bold; 13 | cursor: pointer; 14 | } 15 | 16 | *::-webkit-scrollbar { 17 | width: 5px; 18 | height: 5px; 19 | } 20 | 21 | *::-webkit-scrollbar-track { 22 | box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3); 23 | border-radius: 1px; 24 | } 25 | 26 | *::-webkit-scrollbar-thumb { 27 | border-radius: 1px; 28 | box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.7); 29 | } 30 | 31 | .ant-layout-content { 32 | min-height: auto; 33 | } 34 | 35 | .ant-input-group-addon { 36 | background-color: transparent; 37 | } 38 | 39 | .ant-transfer-list-header { 40 | text-align: left; 41 | } 42 | 43 | .ant-transfer-list-content-item { 44 | text-align: left; 45 | } 46 | 47 | .ant-breadcrumb-separator { 48 | margin: 0 4px; 49 | } 50 | -------------------------------------------------------------------------------- /ui/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /ui/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/app", 5 | "types": [] 6 | }, 7 | "files": [ 8 | "src/main.ts", 9 | "src/polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.ts" 13 | ], 14 | "exclude": [ 15 | "src/test.ts", 16 | "src/**/*.spec.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "downlevelIteration": true, 9 | "experimentalDecorators": true, 10 | "module": "esnext", 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | }, 22 | "angularCompilerOptions": { 23 | "fullTemplateTypeCheck": true, 24 | "strictInjectionParameters": true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ui/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /ui/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rules": { 4 | "array-type": false, 5 | "arrow-parens": false, 6 | "deprecation": { 7 | "severity": "warning" 8 | }, 9 | "component-class-suffix": true, 10 | "contextual-lifecycle": true, 11 | "directive-class-suffix": true, 12 | "directive-selector": [ 13 | true, 14 | "attribute", 15 | "app", 16 | "camelCase" 17 | ], 18 | "component-selector": [ 19 | true, 20 | "element", 21 | "app", 22 | "kebab-case" 23 | ], 24 | "import-blacklist": [ 25 | true, 26 | "rxjs/Rx" 27 | ], 28 | "interface-name": false, 29 | "max-classes-per-file": false, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-consecutive-blank-lines": false, 47 | "no-console": [ 48 | true, 49 | "debug", 50 | "info", 51 | "time", 52 | "timeEnd", 53 | "trace" 54 | ], 55 | "no-empty": false, 56 | "no-inferrable-types": [ 57 | true, 58 | "ignore-params" 59 | ], 60 | "no-non-null-assertion": true, 61 | "no-redundant-jsdoc": true, 62 | "no-switch-case-fall-through": true, 63 | "no-use-before-declare": true, 64 | "no-var-requires": false, 65 | "object-literal-key-quotes": [ 66 | true, 67 | "as-needed" 68 | ], 69 | "object-literal-sort-keys": false, 70 | "ordered-imports": false, 71 | "quotemark": [ 72 | true, 73 | "single" 74 | ], 75 | "semicolon": [ 76 | true, 77 | "never" 78 | ], 79 | "trailing-comma": false, 80 | "no-conflicting-lifecycle": true, 81 | "no-host-metadata-property": true, 82 | "no-input-rename": true, 83 | "no-inputs-metadata-property": true, 84 | "no-output-native": true, 85 | "no-output-on-prefix": true, 86 | "no-output-rename": true, 87 | "no-outputs-metadata-property": true, 88 | "template-banana-in-box": true, 89 | "template-no-negated-async": true, 90 | "use-lifecycle-interface": true, 91 | "use-pipe-transform-interface": true 92 | }, 93 | "rulesDirectory": [ 94 | "codelyzer" 95 | ] 96 | } 97 | --------------------------------------------------------------------------------