From 675b98e8093217bbdd4e7fada9b20eec55645415 Mon Sep 17 00:00:00 2001 From: yl-yue Date: Mon, 17 Jan 2022 18:47:36 +0800 Subject: [PATCH 1/4] - fullMethodField Drop - down box edit auto-complement - Canonical method name - Highlight requestJsonArea content --- .../vn/zalopay/benchmark/GRPCSamplerGui.java | 100 +++++++++++++----- .../core/sampler/GRPCSamplerGuiTest.java | 2 +- 2 files changed, 74 insertions(+), 28 deletions(-) diff --git a/src/main/java/vn/zalopay/benchmark/GRPCSamplerGui.java b/src/main/java/vn/zalopay/benchmark/GRPCSamplerGui.java index 2efce5c1..125276ed 100644 --- a/src/main/java/vn/zalopay/benchmark/GRPCSamplerGui.java +++ b/src/main/java/vn/zalopay/benchmark/GRPCSamplerGui.java @@ -6,6 +6,7 @@ import kg.apc.jmeter.JMeterPluginsUtils; import kg.apc.jmeter.gui.BrowseAction; import kg.apc.jmeter.gui.GuiBuilderHelper; +import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.jmeter.gui.util.HorizontalPanel; import org.apache.jmeter.gui.util.JSyntaxTextArea; @@ -28,6 +29,7 @@ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; +import java.util.Arrays; import java.util.List; import java.util.UUID; @@ -38,6 +40,7 @@ public class GRPCSamplerGui extends AbstractSamplerGui { private static final String WIKI_PAGE = "https://github.com/zalopay-oss/jmeter-grpc-request"; private GRPCSampler grpcSampler; + private String[] protoMethods; private JTextField protoFolderField; private JButton protoBrowseButton; @@ -160,7 +163,6 @@ private void initGui() { /** * Helper function */ - private void addToPanel(JPanel panel, GridBagConstraints constraints, int col, int row, JComponent component) { constraints.gridx = col; @@ -191,6 +193,13 @@ private JPanel getWebServerPanel() { private JPanel getRequestJSONPanel() { requestJsonArea = JSyntaxTextArea.getInstance(30, 50); requestJsonArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_JSON); + requestJsonArea.setBracketMatchingEnabled(true); + requestJsonArea.setPaintMatchedBracketPair(true); + requestJsonArea.setAutoIndentEnabled(true); + requestJsonArea.setMarkOccurrences(true); + requestJsonArea.setPaintMarkOccurrencesBorder(true); + requestJsonArea.setPaintTabLines(true); + requestJsonArea.setShowMatchedBracketPopup(true); JPanel webServerPanel = new JPanel(new BorderLayout()); webServerPanel.setBorder(BorderFactory.createCompoundBorder( @@ -261,13 +270,14 @@ private JPanel getGRPCRequestPanel() { addToPanel(requestPanel, labelConstraints, 0, row, new JLabel("Full Method: ", JLabel.RIGHT)); addToPanel(requestPanel, editConstraints, 1, row, fullMethodField = new JComboBox<>()); fullMethodField.setEditable(true); + fullMethodField.setMaximumRowCount(12); addToPanel(requestPanel, labelConstraints, 2, row, fullMethodButton = new JButton("Listing...")); fullMethodButton.addActionListener(new ActionListener() { // fullMethodButton click listener @Override public void actionPerformed(ActionEvent e) { - getMethods(fullMethodField); + reloadProtoMethods(); } }); fullMethodField.addPopupMenuListener(new PopupMenuListener() { @@ -277,6 +287,7 @@ public void popupMenuWillBecomeVisible(PopupMenuEvent e) { // fullMethod list checked listener @Override public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { + // Request Mock Auto Create requestMock(); } @Override @@ -288,6 +299,30 @@ public void popupMenuCanceled(PopupMenuEvent e) { @Override public void actionPerformed(ActionEvent e) { if ("comboBoxEdited".equals(e.getActionCommand())) { + // fullMethodField Drop - down box edit auto-complement + String fullMethod = fullMethodField.getSelectedItem().toString(); + if (StringUtils.isBlank(fullMethod)) { + return; + } + + String[] protoMethods = getProtoMethods(false); + try { + for (String protoMethod : protoMethods) { + boolean startsWith = protoMethod.startsWith(fullMethod); + if (startsWith == true) { + fullMethodField.setSelectedItem(protoMethod); + if (protoMethod.equals(fullMethod) == false) { + fullMethodField.showPopup(); + } + + break; + } + } + } catch (Exception ex) { + ex.printStackTrace(); + } + + // Request Mock Auto Create requestMock(); } } @@ -303,24 +338,34 @@ public void actionPerformed(ActionEvent e) { return container; } - private void getMethods(JComboBox fullMethodField) { - if (StringUtils.isNotBlank(grpcSampler.getProtoFolder())) { - JMeterVariableUtils.undoVariableReplacement(grpcSampler); - ServiceResolver serviceResolver = ClientList.getServiceResolver(grpcSampler.getProtoFolder(), grpcSampler.getLibFolder(), true); - List methods = ClientList.listServices(serviceResolver); - - log.info("Full Methods: " + methods.toString()); - String[] methodsArr = new String[methods.size()]; - methods.toArray(methodsArr); - - fullMethodField.setModel(new DefaultComboBoxModel<>(methodsArr)); - try { - Object selectedItem = fullMethodField.getSelectedItem(); - fullMethodField.setSelectedItem(selectedItem); - } catch (Exception e) { - fullMethodField.setSelectedIndex(0); + private void reloadProtoMethods() { + Object selectedItem = fullMethodField.getSelectedItem(); + getProtoMethods(true); + try { + if (selectedItem != null && StringUtils.isBlank(selectedItem.toString())) { + selectedItem = fullMethodField.getSelectedItem(); } + fullMethodField.setSelectedItem(selectedItem); + } catch (Exception e) { + fullMethodField.setSelectedIndex(0); + } finally { + fullMethodField.showPopup(); + } + } + + private String[] getProtoMethods(boolean reload) { + if (StringUtils.isNotBlank(grpcSampler.getProtoFolder()) && (ArrayUtils.isEmpty(protoMethods) || reload == true)) { + JMeterVariableUtils.undoVariableReplacement(grpcSampler); + ServiceResolver serviceResolver = ClientList.getServiceResolver(grpcSampler.getProtoFolder(), grpcSampler.getLibFolder(), reload); + List methodList = ClientList.listServices(serviceResolver); + protoMethods = new String[methodList.size()]; + methodList.toArray(protoMethods); + Arrays.sort(protoMethods); + fullMethodField.setModel(new DefaultComboBoxModel<>(protoMethods)); + log.info("Full Methods Length: {}", protoMethods.length); } + + return protoMethods; } private void requestMock() { @@ -339,7 +384,7 @@ private void requestMock() { JSONObject requestBody = new JSONObject(true); for (Descriptors.FieldDescriptor field : fields) { String name = field.getName(); - Object defaultValue = getValue(field); + Object defaultValue = getMockValue(field); requestBody.put(name, defaultValue); } String text = requestBody.toString( @@ -354,25 +399,25 @@ private void requestMock() { } } - private Object getValue(Descriptors.FieldDescriptor field) { + private Object getMockValue(Descriptors.FieldDescriptor field) { String name = field.getName(); String type = field.getType().name().toLowerCase(); if ("message".equals(type)) { List fields = field.getMessageType().getFields(); JSONObject repeatedField = new JSONObject(true); for (Descriptors.FieldDescriptor repeatedFieldDescriptor : fields) { - repeatedField.put(repeatedFieldDescriptor.getName(), this.getValue(repeatedFieldDescriptor)); + repeatedField.put(repeatedFieldDescriptor.getName(), this.getMockValue(repeatedFieldDescriptor)); } return repeatedField; } else { - return getDefaultValue(name, type); + return getMockDefaultValue(name, type); } } - private Object getDefaultValue(String name, String type) { + private Object getMockDefaultValue(String name, String type) { switch (type) { case "string": - return interpretMockViaFieldName(name); + return fieldNameGenerateMock(name); case "bool": return true; case "number": @@ -406,10 +451,11 @@ private Object getDefaultValue(String name, String type) { } /** - * Tries to guess a mock value from the field name. - * Default Hello. + * Mock generation from fieldName. + * + *

Default: Hello

*/ - private String interpretMockViaFieldName(String fieldName) { + private String fieldNameGenerateMock(String fieldName) { String fieldNameLower = fieldName.toLowerCase(); if (fieldNameLower.startsWith("id") || fieldNameLower.endsWith("id")) { diff --git a/src/test/java/vn/zalopay/benchmark/core/sampler/GRPCSamplerGuiTest.java b/src/test/java/vn/zalopay/benchmark/core/sampler/GRPCSamplerGuiTest.java index 14146d3c..429e6fd1 100644 --- a/src/test/java/vn/zalopay/benchmark/core/sampler/GRPCSamplerGuiTest.java +++ b/src/test/java/vn/zalopay/benchmark/core/sampler/GRPCSamplerGuiTest.java @@ -153,7 +153,7 @@ public void verifyCanPerformGetMethodName() throws NoSuchFieldException, Illegal grpcSampler.setRequestJson("dummyRequest"); grpRequestPluginGUI.configure(grpcSampler); fullMethodButton.doClick(); - Assert.assertEquals(fullMethodComboBox.getSelectedItem(), "bookstore.Bookstore/ListShelves"); + Assert.assertEquals(fullMethodComboBox.getSelectedItem(), "bookstore.Bookstore/CreateShelf"); Assert.assertNotNull(grpcSampler); Assert.assertNotNull(grpRequestPluginGUI); frame.dispose(); From 5e7202c0b8a87be407fe6373d81f7083e870a139 Mon Sep 17 00:00:00 2001 From: yl-yue Date: Tue, 18 Jan 2022 10:41:18 +0800 Subject: [PATCH 2/4] - add Chinese document --- README.md | 15 ++++---- README.zh-CN.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 README.zh-CN.md diff --git a/README.md b/README.md index a955e4c4..ae4489af 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ [![Javadocs](https://www.javadoc.io/badge/org.apache.jmeter/ApacheJMeter_core.svg)](https://www.javadoc.io/doc/org.apache.jmeter/ApacheJMeter_core) [![Stack Overflow](https://img.shields.io/:stack%20overflow-jmeter-brightgreen.svg)](https://stackoverflow.com/questions/tagged/jmeter) +[简体中文](./README.zh-CN.md) | English + ## What is it This is a simpler of JMeter used to test for any gRPC server, it is not necessary to generate gRPC classes or to compile the protos binary for the service. Just a very simple for input: @@ -68,12 +70,13 @@ Run test: | 1 | Server Name or IP | Domain/IP for gRPC server | | 2 | Port Number | Port for gRPC server (80/ 443) | | 3 | SSL/TLS | SSL/TLS to authenticate the server | -| 4 | Proto Root Directory | Root directory contains proto files | -| 5 | Library Directory (Optional) | Using a different underlying library (googleapis) | -| 6 | Full Method | Full Method to test | -| 7 | Metadata | Metadata can be use for Store token, authentication method, etc.
Two Ways to use metadata,

 1. Comma separated Key:Value :
  - key1:value1,key2:value2
  - Value should url encode with utf-8

 2. Json String :
   - {"key1":"Value1", "key2":"value2"}

Note: In gRPC Metadata value is (Key, value) both in format of (String, String), in case of nested Json Objects values, will go to request as a JsonString. | -| 8 | Deadline | How long gRPC clients are willing to wait for an RPC to complete | -| 9 | Send JSON Format With the Request | Data request with JSON format | +| 4 | Disable SSL/TLS Cert Verification | Disable SSL/TLS certificate verification (enable this function when using self-signed certificates) | +| 5 | Proto Root Directory | Root directory contains proto files | +| 6 | Library Directory (Optional) | Using a different underlying library (googleapis) | +| 7 | Full Method | Full Method to test | +| 8 | Metadata | Metadata can be use for Store token, authentication method, etc.
Two Ways to use metadata,

 1. Comma separated Key:Value :
  - key1:value1,key2:value2
  - Value should url encode with utf-8

 2. Json String :
   - {"key1":"Value1", "key2":"value2"}

Note: In gRPC Metadata value is (Key, value) both in format of (String, String), in case of nested Json Objects values, will go to request as a JsonString. | +| 9 | Deadline | How long gRPC clients are willing to wait for an RPC to complete | +| 10 | Send JSON Format With the Request | Data request with JSON format | ## Running the examples diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 00000000..a316842f --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,96 @@ +# jmeter-grpc-request +

Apache JMeter and gRPC logo

+ +

这个JMeter采样器允许您向服务器发送一个gRPC请求

+

它和HTTP请求一样简单

+ +

+ + Javadocs + + + Stack Overflow + +

+ +[English](./README.md) | 简体中文 + +## 介绍 +他是一个功能强大的JMeter Grpc插件,可用于测试任何gRPC服务器,它不需要生成gRPC类或编译服务的protos二进制文件,只是一个非常简单的输入: + +- gRPC服务的主机和端口 +- 需要测试的RPC方法 +- proto文件路径 +- 格式化的JSON请求数据 + +## 特性 +- 支持压测阻塞等调用方式 +- 支持在运行时解析proto文件 +- 支持TLS连接 +- 支持元数据认证(JWT/Token) +- 支持JSON格式的请求数据 +- 支持运行在Windows、Mac、Linux中 +- 支持自动列出proto文件中的所有完整方法 +- 支持根据proto文件自动生成请求Mock +- 支持各种报告生成 +- 支持自动化测试 + +## 如何使用 +

jmeter-create-testscript-grpc

+ +### 插件安装 +你需要将 **jmeter-grpc-request** 插件的 `jar` 包复制到JMeter的 `lib/ext` 目录下面,然后重启你的JMeter工具。 + +**jmeter-grpc-request** 插件的 `jar` 包,可以从 [Releases Page](https://github.com/zalopay-oss/jmeter-grpc-request/releases) 获得,也可以 在 [JMeter Plugins Manager](https://jmeter-plugins.org/?search=jmeter-grpc-request) 中找到 + +### 使用 JMeter 发出 gRPC 请求 +创建测试脚本: + +- 添加线程组:右键单击测试计划 → 添加 → 线程(用户) → 线程组 +- 添加GRPC Request:右键单击新建的线程组 → 添加 → 取样器 → GRPC Request +- 填写请求信息:主机、端口、proto文件夹、rpc方法、请求数据 +- 保存测试脚本 + +运行测试: + +- 通过JMeter GUI在顶部栏点击启动按钮 +- 通过命令行:`bin/jmeter -n -t .jmx -l .csv -j .log -e -o ` + +### 使用说明 +| 序号 | 选项 | 描述 | +|----- |:----------------------------------- |:--------------------------------------------------------------------- | +| 1 | Server Name or IP | gRPC服务器地址(域名或IP) | +| 2 | Port Number | gRPC服务器端口 (80/ 443) | +| 3 | SSL/TLS | 开启SSL/TLS认证 | +| 4 | Disable SSL/TLS Cert Verification | 禁用SSL/TLS证书校验(自签证书需开启) | +| 5 | Proto Root Directory | proto文件的根路径 | +| 6 | Library Directory (Optional) | proto文件解析需要依赖的额外库的文件夹路径 (googleapis) | +| 7 | Full Method | 用于请求测试的RPC方法 | +| 8 | Metadata | Metadata可以用于token身份验证等方式,支持以下两种方式传输(UTF-8):
1. 使用键值对(Key: Value):
  - key1: value1, key2: value2
2. 使用 JSON String:
  - {"key1":"Value1", "key2":"value2"} | +| 9 | Deadline | 请求超时时间(单位:毫秒) | +| 10 | Send JSON Format With the Request | 格式化的JSON请求数据 | + +## 运行示例 +运行示例说明见 [./dist/example](./dist/example) 目录 + +## 基准测试 +通过基准测试验证,jmeter-grpc-request 插件在对gRPC系统进行负载测试时是稳定的。 + +了解更多 [Benchmark: jmter-grpc-request](./dist/benchmark) + +- CCU: 120 user +- Duration: 30 min + + + +## 开发构建 +### 构建环境 +从源码构建 JMeter GRPC Request 插件,进行开发调试,必须拥有以下环境: + +- [Java 8](https://www.oracle.com/downloads/index.html) +- [Apache Maven 3](https://maven.apache.org/) + +### 构建命令 +``` +mvn clean package +``` \ No newline at end of file From 097bf50bb9f96ab93f445ed9b197b6e59ca122b4 Mon Sep 17 00:00:00 2001 From: yl-yue Date: Tue, 18 Jan 2022 11:29:22 +0800 Subject: [PATCH 3/4] - Prints exceptions that occur before the request is initiated --- src/main/java/vn/zalopay/benchmark/GRPCSampler.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/vn/zalopay/benchmark/GRPCSampler.java b/src/main/java/vn/zalopay/benchmark/GRPCSampler.java index 35a5002b..941f22e0 100644 --- a/src/main/java/vn/zalopay/benchmark/GRPCSampler.java +++ b/src/main/java/vn/zalopay/benchmark/GRPCSampler.java @@ -109,6 +109,10 @@ private String whoAmI() { } private void errorResult(GrpcResponse grpcResponse, SampleResult sampleResult, Exception e) { + if (sampleResult.getStartTime() == 0) { + // Prints exceptions that occur before the request is initiated + e.printStackTrace(); + } sampleResult.sampleEnd(); sampleResult.setSuccessful(false); sampleResult.setResponseData(String.format("Exception: %s. %s", e.getCause().getMessage(), grpcResponse.getGrpcMessageString()), "UTF-8"); @@ -120,7 +124,6 @@ private void errorResult(GrpcResponse grpcResponse, SampleResult sampleResult, E /** * GETTER AND SETTER */ - public String getMetadata() { return getPropertyAsString(METADATA); } From 135cfafde43b6143627260be01264caab1559228 Mon Sep 17 00:00:00 2001 From: yl-yue Date: Wed, 23 Feb 2022 17:27:33 +0800 Subject: [PATCH 4/4] - Prints exceptions that occur before the request is initiated2 --- README.zh-CN.md | 4 ++-- .../java/vn/zalopay/benchmark/GRPCSampler.java | 16 +++++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.zh-CN.md b/README.zh-CN.md index a316842f..425f72b8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -60,8 +60,8 @@ | 序号 | 选项 | 描述 | |----- |:----------------------------------- |:--------------------------------------------------------------------- | | 1 | Server Name or IP | gRPC服务器地址(域名或IP) | -| 2 | Port Number | gRPC服务器端口 (80/ 443) | -| 3 | SSL/TLS | 开启SSL/TLS认证 | +| 2 | Port Number | gRPC服务器端口 (80/443) | +| 3 | SSL/TLS | 开启SSL/TLS认证(https) | | 4 | Disable SSL/TLS Cert Verification | 禁用SSL/TLS证书校验(自签证书需开启) | | 5 | Proto Root Directory | proto文件的根路径 | | 6 | Library Directory (Optional) | proto文件解析需要依赖的额外库的文件夹路径 (googleapis) | diff --git a/src/main/java/vn/zalopay/benchmark/GRPCSampler.java b/src/main/java/vn/zalopay/benchmark/GRPCSampler.java index 941f22e0..ba0274d2 100644 --- a/src/main/java/vn/zalopay/benchmark/GRPCSampler.java +++ b/src/main/java/vn/zalopay/benchmark/GRPCSampler.java @@ -109,16 +109,18 @@ private String whoAmI() { } private void errorResult(GrpcResponse grpcResponse, SampleResult sampleResult, Exception e) { - if (sampleResult.getStartTime() == 0) { + try { + sampleResult.setSuccessful(false); + sampleResult.setResponseCode("500"); + sampleResult.setDataType(SampleResult.TEXT); + sampleResult.sampleEnd(); + sampleResult.setResponseMessage("Exception: " + e.getCause().getMessage()); + sampleResult.setResponseData(String.format("Exception: %s. %s", e.getCause().getMessage(), grpcResponse.getGrpcMessageString()), "UTF-8"); + } catch (Exception ex) { // Prints exceptions that occur before the request is initiated e.printStackTrace(); + log.error("GrpcMessage: {}", grpcResponse.getGrpcMessageString()); } - sampleResult.sampleEnd(); - sampleResult.setSuccessful(false); - sampleResult.setResponseData(String.format("Exception: %s. %s", e.getCause().getMessage(), grpcResponse.getGrpcMessageString()), "UTF-8"); - sampleResult.setResponseMessage("Exception: " + e.getCause().getMessage()); - sampleResult.setDataType(SampleResult.TEXT); - sampleResult.setResponseCode("500"); } /**