├── .github └── workflows │ └── run_test_cases.yaml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── docs ├── api_examples.md ├── cli_examples.md └── design.md ├── etc └── emqx_rule_engine.conf ├── include ├── rule_engine.hrl └── rule_events.hrl ├── priv └── emqx_rule_engine.schema ├── rebar.config ├── rebar.config.script ├── src ├── emqx_rule_actions.erl ├── emqx_rule_engine.app.src ├── emqx_rule_engine.app.src.script ├── emqx_rule_engine.appup.src ├── emqx_rule_engine.erl ├── emqx_rule_engine_api.erl ├── emqx_rule_engine_app.erl ├── emqx_rule_engine_cli.erl ├── emqx_rule_engine_sup.erl ├── emqx_rule_events.erl ├── emqx_rule_funcs.erl ├── emqx_rule_id.erl ├── emqx_rule_maps.erl ├── emqx_rule_metrics.erl ├── emqx_rule_registry.erl ├── emqx_rule_runtime.erl ├── emqx_rule_sqlparser.erl ├── emqx_rule_sqltester.erl ├── emqx_rule_utils.erl └── emqx_rule_validator.erl └── test ├── emqx_rule_engine_SUITE.erl ├── emqx_rule_events_SUITE.erl ├── emqx_rule_funcs_SUITE.erl ├── emqx_rule_id_SUITE.erl ├── emqx_rule_maps_SUITE.erl ├── emqx_rule_metrics_SUITE.erl ├── emqx_rule_registry_SUITE.erl ├── emqx_rule_utils_SUITE.erl ├── emqx_rule_validator_SUITE.erl └── prop_rule_maps.erl /.github/workflows/run_test_cases.yaml: -------------------------------------------------------------------------------- 1 | name: Run test cases 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | run_test_cases: 7 | runs-on: ubuntu-latest 8 | 9 | container: 10 | image: erlang:22.1 11 | 12 | steps: 13 | - uses: actions/checkout@v1 14 | - name: run test cases 15 | run: | 16 | #make xref 17 | make eunit 18 | make ct 19 | make cover 20 | - uses: actions/upload-artifact@v1 21 | if: always() 22 | with: 23 | name: logs 24 | path: _build/test/logs 25 | - uses: actions/upload-artifact@v1 26 | with: 27 | name: cover 28 | path: _build/test/cover 29 | 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | ebin/* 8 | rel/example_project 9 | .concrete/DEV_MODE 10 | .rebar/ 11 | .rebar3/ 12 | erlang.mk 13 | emqx_rule_engine.d 14 | .erlang.mk/ 15 | data/ 16 | _build/ 17 | TODO 18 | ct.coverdata 19 | logs/ 20 | test/ct.cover.spec 21 | rebar.lock 22 | cover/ 23 | eunit.coverdata 24 | rebar3.crashdump 25 | .DS_Store 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ## shallow clone for speed 2 | 3 | REBAR_GIT_CLONE_OPTIONS += --depth 1 4 | 5 | export REBAR_GIT_CLONE_OPTIONS 6 | 7 | REBAR = rebar3 8 | 9 | all: compile 10 | 11 | compile: 12 | $(REBAR) compile 13 | 14 | proper: compile 15 | $(REBAR) as test proper -v -n 1000 16 | 17 | ct: compile 18 | $(REBAR) as test ct -v 19 | 20 | eunit: compile 21 | $(REBAR) as test eunit 22 | 23 | test: ct proper eunit 24 | 25 | tests: test 26 | 27 | xref: 28 | $(REBAR) xref 29 | 30 | cover: 31 | $(REBAR) cover 32 | 33 | clean: 34 | $(REBAR) clean 35 | 36 | distclean: 37 | @rm -rf _build 38 | @rm -f data/app.*.config data/vm.*.args rebar.lock 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # emqx-rule-engine 3 | 4 | IoT Rule Engine for EMQ X Broker. 5 | 6 | ## Concept 7 | 8 | ``` 9 | iot rule "Rule Name" 10 | when 11 | match TopicFilters and Conditions 12 | select 13 | para1 = val1 14 | para2 = val2 15 | then 16 | take action(#{para2 => val1, #para2 => val2}) 17 | ``` 18 | 19 | ## Architecture 20 | 21 | ``` 22 | |-----------------| 23 | Pub ---->| Message Routing |----> Sub 24 | |-----------------| 25 | | /|\ 26 | \|/ | 27 | |-----------------| 28 | | Rule Engine | 29 | |-----------------| 30 | | | 31 | Backends Services Bridges 32 | ``` 33 | 34 | ## SQL for Rule query statement 35 | 36 | ``` 37 | select id, time, temperature as t from "topic/a" where t > 50; 38 | ``` 39 | 40 | ## License 41 | 42 | Copyright (c) 2019 [EMQ Technologies Co., Ltd](https://emqx.io). All Rights Reserved. 43 | 44 | Licensed under the Apache License, Version 2.0 (the "License");you may not use this file except in compliance with the License.You may obtain a copy of the License at 45 | 46 | [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) 47 | 48 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 49 | See the License for the specific language governing permissions and limitations under the License. 50 | 51 | -------------------------------------------------------------------------------- /docs/api_examples.md: -------------------------------------------------------------------------------- 1 | #Rule-Engine-APIs 2 | 3 | ## ENVs 4 | 5 | APPSECRET="88ebdd6569afc:Mjg3MzUyNTI2Mjk2NTcyOTEwMDEwMDMzMTE2NTM1MTkzNjA" 6 | 7 | ## Rules 8 | 9 | ### test sql 10 | 11 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules?test' -d \ 12 | '{"rawsql":"select * from \"message.publish\" where topic=\"t/a\"","ctx":{}}' 13 | 14 | 15 | 16 | ### create 17 | 18 | ```shell 19 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules' -d \ 20 | '{"rawsql":"select * from \"t/a\"","actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule"}' 21 | 22 | {"code":0,"data":{"actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule","enabled":true,"id":"rule:bc987915","rawsql":"select * from \"t/a\""}} 23 | 24 | ## with a resource id in the action args 25 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules' -d \ 26 | '{"rawsql":"select * from \"t/a\"","actions":[{"name":"inspect","params":{"$resource":"resource:3a7b44a1"}}],"description":"test-rule"}' 27 | 28 | {"code":0,"data":{"actions":[{"name":"inspect","params":{"$resource":"resource:3a7b44a1","a":1}}],"description":"test-rule","enabled":true,"id":"rule:6fce0ca9","rawsql":"select * from \"t/a\""}} 29 | ``` 30 | 31 | ### modify 32 | 33 | ```shell 34 | ## modify all of the params 35 | $ curl -XPUT -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915' -d \ 36 | '{"rawsql":"select * from \"t/a\"","actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule"}' 37 | 38 | ## modify some of the params: disable it 39 | $ curl -XPUT -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915' -d \ 40 | '{"enabled": false}' 41 | 42 | ## modify some of the params: add fallback actions 43 | $ curl -XPUT -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915' -d \ 44 | '{"actions":[{"name":"inspect","params":{"a":1}, "fallbacks": [{"name":"donothing"}]}]}' 45 | ``` 46 | 47 | ### show 48 | 49 | ```shell 50 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915' 51 | 52 | {"code":0,"data":{"actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule","enabled":true,"id":"rule:bc987915","rawsql":"select * from \"t/a\""}} 53 | ``` 54 | 55 | ### list 56 | 57 | ```shell 58 | $ curl -v --basic -u $APPSECRET -k http://localhost:8081/api/v4/rules 59 | 60 | {"code":0,"data":[{"actions":[{"name":"inspect","params":{"a":1}}],"description":"test-rule","enabled":true,"id":"rule:bc987915","rawsql":"select * from \"t/a\""},{"actions":[{"name":"inspect","params":{"$resource":"resource:3a7b44a1","a":1}}],"description":"test-rule","enabled":true,"id":"rule:6fce0ca9","rawsql":"select * from \"t/a\""}]} 61 | ``` 62 | 63 | ### delete 64 | 65 | ```shell 66 | $ curl -XDELETE -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules/rule:bc987915' 67 | 68 | {"code":0} 69 | ``` 70 | 71 | ## Actions 72 | 73 | ### list 74 | 75 | ```shell 76 | $ curl -v --basic -u $APPSECRET -k http://localhost:8081/api/v4/actions 77 | 78 | {"code":0,"data":[{"app":"emqx_rule_engine","description":"Republish a MQTT message to a another topic","name":"republish","params":{...},"types":[]},{"app":"emqx_rule_engine","description":"Inspect the details of action params for debug purpose","name":"inspect","params":{},"types":[]},{"app":"emqx_web_hook","description":"Forward Messages to Web Server","name":"data_to_webserver","params":{...},"types":["web_hook"]}]} 79 | ``` 80 | 81 | ### show 82 | 83 | ```shell 84 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/actions/inspect' 85 | 86 | {"code":0,"data":{"app":"emqx_rule_engine","description":"Debug Action","name":"inspect","params":{"$resource":"built_in"}}} 87 | ``` 88 | 89 | ## Resource Types 90 | 91 | ### list 92 | 93 | ```shell 94 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resource_types' 95 | 96 | {"code":0,"data":[{"description":"Debug resource type","name":"built_in","params":{},"provider":"emqx_rule_engine"}]} 97 | ``` 98 | 99 | ### list all resources of a type 100 | 101 | ```shell 102 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resource_types/built_in/resources' 103 | 104 | {"code":0,"data":[{"attrs":"undefined","config":{"a":1},"description":"test-rule","id":"resource:71df3086","type":"built_in"}]} 105 | ``` 106 | 107 | ### show 108 | 109 | ```shell 110 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resource_types/built_in' 111 | 112 | {"code":0,"data":{"description":"Debug resource type","name":"built_in","params":{},"provider":"emqx_rule_engine"}} 113 | ``` 114 | 115 | 116 | 117 | ## Resources 118 | 119 | ### create 120 | 121 | ```shell 122 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources' -d \ 123 | '{"type": "built_in", "config": {"a":1}, "description": "test-resource"}' 124 | 125 | {"code":0,"data":{"attrs":"undefined","config":{"a":1},"description":"test-resource","id":"resource:71df3086","type":"built_in"}} 126 | ``` 127 | 128 | ### start 129 | 130 | ```shell 131 | $ curl -XPOST -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources/resource:71df3086' 132 | 133 | {"code":0} 134 | ``` 135 | 136 | ### list 137 | 138 | ```shell 139 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources' 140 | 141 | {"code":0,"data":[{"attrs":"undefined","config":{"a":1},"description":"test-resource","id":"resource:71df3086","type":"built_in"}]} 142 | ``` 143 | 144 | ### show 145 | 146 | ```shell 147 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources/resource:71df3086' 148 | 149 | {"code":0,"data":{"attrs":"undefined","config":{"a":1},"description":"test-resource","id":"resource:71df3086","type":"built_in"}} 150 | ``` 151 | 152 | ### get resource status 153 | 154 | ```shell 155 | curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resource_status/resource:71df3086' 156 | 157 | {"code":0,"data":{"is_alive":true}} 158 | ``` 159 | 160 | ### delete 161 | 162 | ```shell 163 | $ curl -XDELETE -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources/resource:71df3086' 164 | 165 | {"code":0} 166 | ``` 167 | 168 | ## Rule example using webhook 169 | 170 | ``` shell 171 | 172 | $ curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/resources' -d \ 173 | '{"type": "web_hook", "config": {"url": "http://127.0.0.1:9910", "headers": {"token":"axfw34y235wrq234t4ersgw4t"}, "method": "POST"}, "description": "web hook resource-1"}' 174 | 175 | {"code":0,"data":{"attrs":"undefined","config":{"headers":{"token":"axfw34y235wrq234t4ersgw4t"},"method":"POST","url":"http://127.0.0.1:9910"},"description":"web hook resource-1","id":"resource:8531a11f","type":"web_hook"}} 176 | 177 | curl -v --basic -u $APPSECRET -k 'http://localhost:8081/api/v4/rules' -d \ 178 | '{"rawsql":"SELECT clientid as c, username as u.name FROM \"#\"","actions":[{"name":"data_to_webserver","params":{"$resource": "resource:8531a11f"}}],"description":"Forward connected events to webhook"}' 179 | 180 | {"code":0,"data":{"actions":[{"name":"data_to_webserver","params":{"$resource":"resource:8531a11f","headers":{"token":"axfw34y235wrq234t4ersgw4t"},"method":"POST","url":"http://127.0.0.1:9910"}}],"description":"Forward connected events to webhook","enabled":true,"id":"rule:4fe05936","rawsql":"select * from \"#\""}} 181 | ``` 182 | 183 | Start a `web server` using `nc`, and then connect to emqx broker using a mqtt client with username = 'Shawn': 184 | 185 | ```shell 186 | $ echo -e "HTTP/1.1 200 OK\n\n $(date)" | nc -l 127.0.0.1 9910 187 | 188 | POST / HTTP/1.1 189 | content-type: application/json 190 | content-length: 48 191 | te: 192 | host: 127.0.0.1:9910 193 | connection: keep-alive 194 | token: axfw34y235wrq234t4ersgw4t 195 | 196 | {"c":"clientId-bP70ymeIyo","u":{"name":"Shawn"} 197 | ``` 198 | -------------------------------------------------------------------------------- /docs/cli_examples.md: -------------------------------------------------------------------------------- 1 | #Rule-Engine-CLIs 2 | 3 | ## Rules 4 | 5 | ### create 6 | 7 | ```shell 8 | $ ./bin/emqx_ctl rules create 'SELECT payload FROM "t/#" username="Steven"' '[{"name":"data_to_webserver", "params": {"$resource": "resource:9093f1cb"}}]' --descr="Msg From Steven to WebServer" 9 | 10 | Rule rule:98a75239 created 11 | ``` 12 | 13 | ### modify 14 | 15 | 16 | ```shell 17 | ## update sql, action, description 18 | $ ./bin/emqx_ctl rules update 'rule:98a75239' \ 19 | -s "select * from \"t/a\" " \ 20 | -a '[{"name":"do_nothing", "fallbacks": []' -g continue \ 21 | -d 'Rule for debug2' \ 22 | 23 | ## update sql only 24 | $ ./bin/emqx_ctl rules update 'rule:98a75239' -s 'SELECT * FROM "t/a"' 25 | 26 | ## disable the rule 27 | $ ./bin/emqx_ctl rules update 'rule:98a75239' -e false 28 | 29 | ``` 30 | 31 | ### show 32 | 33 | ```shell 34 | $ ./bin/emqx_ctl rules show rule:98a75239 35 | 36 | rule(id='rule:98a75239', rawsql='SELECT payload FROM "t/#" username="Steven"', actions=[{"name":"data_to_webserver","params":{"$resource":"resource:9093f1cb","url":"http://host-name/chats"}}], enabled='true', description='Msg From Steven to WebServer') 37 | ``` 38 | 39 | ### list 40 | 41 | ```shell 42 | $ ./bin/emqx_ctl rules list 43 | 44 | rule(id='rule:98a75239', rawsql='SELECT payload FROM "t/#" username="Steven"', actions=[{"name":"data_to_webserver","params":{"$resource":"resource:9093f1cb","url":"http://host-name/chats"}}], enabled='true', description='Msg From Steven to WebServer') 45 | 46 | ``` 47 | 48 | ### delete 49 | 50 | ```shell 51 | $ ./bin/emqx_ctl rules delete 'rule:98a75239' 52 | 53 | ok 54 | ``` 55 | 56 | ## Actions 57 | 58 | ### list 59 | 60 | ```shell 61 | $ ./bin/emqx_ctl rule-actions list 62 | 63 | action(name='republish', app='emqx_rule_engine', types=[], params=#{...}, description='Republish a MQTT message to a another topic') 64 | action(name='inspect', app='emqx_rule_engine', types=[], params=#{...}, description='Inspect the details of action params for debug purpose') 65 | action(name='data_to_webserver', app='emqx_web_hook', types=[], params=#{...}, description='Forward Messages to Web Server') 66 | ``` 67 | 68 | ### show 69 | 70 | ```shell 71 | $ ./bin/emqx_ctl rule-actions show 'data_to_webserver' 72 | 73 | action(name='data_to_webserver', app='emqx_web_hook', types=['web_hook'], params=#{...}, description='Forward Messages to Web Server') 74 | ``` 75 | 76 | ## Resource 77 | 78 | ### create 79 | 80 | ```shell 81 | $ ./bin/emqx_ctl resources create 'web_hook' -c '{"url": "http://host-name/chats"}' --descr 'Resource towards http://host-name/chats' 82 | 83 | Resource resource:19addfef created 84 | ``` 85 | 86 | ### list 87 | 88 | ```shell 89 | $ ./bin/emqx_ctl resources list 90 | 91 | resource(id='resource:19addfef', type='web_hook', config=#{<<"url">> => <<"http://host-name/chats">>}, attrs=undefined, description='Resource towards http://host-name/chats') 92 | 93 | ``` 94 | 95 | ### list all resources of a type 96 | 97 | ```shell 98 | $ ./bin/emqx_ctl resources list -t 'web_hook' 99 | 100 | resource(id='resource:19addfef', type='web_hook', config=#{<<"url">> => <<"http://host-name/chats">>}, attrs=undefined, description='Resource towards http://host-name/chats') 101 | ``` 102 | 103 | ### show 104 | 105 | ```shell 106 | $ ./bin/emqx_ctl resources show 'resource:19addfef' 107 | 108 | resource(id='resource:19addfef', type='web_hook', config=#{<<"url">> => <<"http://host-name/chats">>}, attrs=undefined, description='Resource towards http://host-name/chats') 109 | ``` 110 | 111 | ### delete 112 | 113 | ```shell 114 | $ ./bin/emqx_ctl resources delete 'resource:19addfef' 115 | 116 | ok 117 | ``` 118 | 119 | ## Resources Types 120 | 121 | ### list 122 | 123 | ```shell 124 | $ ./bin/emqx_ctl resource-types list 125 | 126 | resource_type(name='built_in', provider='emqx_rule_engine', params=#{...}, on_create={emqx_rule_actions,on_resource_create}, description='The built in resource type for debug purpose') 127 | resource_type(name='web_hook', provider='emqx_web_hook', params=#{...}, on_create={emqx_web_hook_actions,on_resource_create}, description='WebHook Resource') 128 | ``` 129 | 130 | ### show 131 | 132 | ```shell 133 | $ ./bin/emqx_ctl resource-types show built_in 134 | 135 | resource_type(name='built_in', provider='emqx_rule_engine', params=#{}, description='The built in resource type for debug purpose') 136 | ``` 137 | 138 | ## Rule example using webhook 139 | 140 | ``` shell 141 | 1. Create a webhook resource to URL http://127.0.0.1:9910. 142 | ./bin/emqx_ctl resources create 'web_hook' --config '{"url": "http://127.0.0.1:9910", "headers": {"token":"axfw34y235wrq234t4ersgw4t"}, "method": "POST"}' 143 | Resource resource:3128243e created 144 | 145 | 2. Create a rule using action data_to_webserver, and bind above resource to that action. 146 | ./bin/emqx_ctl rules create 'client.connected' 'SELECT clientid as c, username as u.name FROM "#"' '[{"name":"data_to_webserver", "params": {"$resource": "resource:3128243e"}}]' --descr "Forward Connected Events to WebServer" 147 | Rule rule:222b59f7 created 148 | ``` 149 | 150 | Start a simple `Web Server` using `nc`, and then connect to emqx broker using a mqtt client with username = 'Shawn': 151 | 152 | ```shell 153 | $ echo -e "HTTP/1.1 200 OK\n\n $(date)" | nc -l 127.0.0.1 9910 154 | 155 | POST / HTTP/1.1 156 | content-type: application/json 157 | content-length: 48 158 | te: 159 | host: 127.0.0.1:9910 160 | connection: keep-alive 161 | token: axfw34y235wrq234t4ersgw4t 162 | 163 | {"c":"clientId-bP70ymeIyo","u":{"name":"Shawn"} 164 | ``` 165 | -------------------------------------------------------------------------------- /docs/design.md: -------------------------------------------------------------------------------- 1 | 2 | # EMQ X Rule Engine 3 | 4 | This is the design guide of message routing rule engine for the EMQ X Broker. 5 | 6 | ## Concept 7 | 8 | A rule is: 9 | 10 | ``` 11 | when 12 | Match | 13 | then 14 | Select and Take ; 15 | ``` 16 | 17 | or: 18 | 19 | ``` 20 | rule "Rule Name" 21 | when 22 | rule match 23 | select 24 | para1 = val1 25 | para2 = val2 26 | then 27 | action(#{para2 => val1, #para2 => val2}) 28 | ``` 29 | 30 | ## Architecture 31 | 32 | ``` 33 | |-----------------| 34 | P ---->| Message Routing |----> S 35 | |-----------------| 36 | | /|\ 37 | \|/ | 38 | |-----------------| 39 | | Rule Engine | 40 | |-----------------| 41 | | | 42 | Backends Services Bridges 43 | ``` 44 | 45 | ## Design 46 | 47 | ``` 48 | Event | Message -> Rules -> Actions -> Resources 49 | ``` 50 | 51 | ``` 52 | P -> |--------------------| |---------------------------------------| 53 | | Messages (Routing) | -> | Rules (Select Data, Match Conditions) | 54 | S <- |--------------------| |---------------------------------------| 55 | |---------| |-----------| |-------------------------------| 56 | ->| Actions | -> | Resources | -> | (Backends, Bridges, WebHooks) | 57 | |---------| |-----------| |-------------------------------| 58 | ``` 59 | 60 | 61 | 62 | ## Rule 63 | 64 | A rule consists of a SELECT statement, a topic filter, and a rule action 65 | 66 | Rules consist of the following: 67 | 68 | - Id 69 | - Name 70 | - Topic 71 | - Description 72 | - Action 73 | - Enabled 74 | 75 | The operations on a rule: 76 | 77 | - Create 78 | - Enable 79 | - Disable 80 | - Delete 81 | 82 | 83 | 84 | ## Action 85 | 86 | Actions consist of the following: 87 | 88 | - Id 89 | - Name 90 | - For 91 | - App 92 | - Module 93 | - Func 94 | - Args 95 | - Descr 96 | 97 | Define a rule action in ADT: 98 | 99 | ``` 100 | action :: Application -> Resource -> Params -> IO () 101 | ``` 102 | 103 | A rule action: 104 | 105 | Module:function(Args) 106 | 107 | 108 | 109 | ## Resource 110 | 111 | ### Resource Name 112 | 113 | ``` 114 | backend:mysql:localhost:port:db 115 | backend:redis:localhost: 116 | webhook:url 117 | bridge:kafka: 118 | bridge:rabbit:localhost 119 | ``` 120 | 121 | ### Resource Properties 122 | 123 | - Name 124 | - Descr or Description 125 | - Config #{} 126 | - Instances 127 | - State: Running | Stopped 128 | 129 | ### Resource Management 130 | 131 | 1. Create Resource 132 | 2. List Resources 133 | 3. Lookup Resource 134 | 4. Delete Resource 135 | 5. Test Resource 136 | 137 | ### Resource State (Lifecircle) 138 | 139 | 0. Create Resource and Validate a Resource 140 | 1. Start/Connect Resource 141 | 2. Bind resource name to instance 142 | 3. Stop/Disconnect Resource 143 | 4. Unbind resource name with instance 144 | 5. Is Resource Alive? 145 | 146 | ### Resource Type 147 | 148 | The properties and behaviors of resources is defined by resource types. A resoure type is provided(contributed) by a plugin. 149 | 150 | ### Resource Type Provider 151 | 152 | Provider of resource type is a EMQ X Plugin. 153 | 154 | ### Resource Manager 155 | 156 | ``` 157 | Supervisor 158 | | 159 | \|/ 160 | Action ----> Proxy(Batch|Write) ----> Connection -----> ExternalResource 161 | | /|\ 162 | |------------------Fetch----------------| 163 | ``` 164 | 165 | 166 | 167 | ## REST API 168 | 169 | Rules API 170 | Actions API 171 | Resources API 172 | 173 | ## CLI 174 | 175 | ``` 176 | rules list 177 | rules show 178 | 179 | rule-actions list 180 | rule-actions show 181 | 182 | resources list 183 | resources show 184 | 185 | resource_templates list 186 | resource_templates show 187 | ``` 188 | 189 | -------------------------------------------------------------------------------- /etc/emqx_rule_engine.conf: -------------------------------------------------------------------------------- 1 | ##==================================================================== 2 | ## Rule Engine for EMQ X R4.0 3 | ##==================================================================== 4 | 5 | rule_engine.ignore_sys_message = on 6 | 7 | ## Event Messages 8 | ## 9 | ## If enabled (on), rule engine publishes the event as an MQTT message 10 | ## with topic='$events/' on the occurrence of an emqx event. 11 | ## 12 | ## If disabled, rule engine stops publishing the event messages, but 13 | ## the event message can still be processed by the rule SQL. e.g. rule SQL: 14 | ## 15 | ## SELECT * FROM "$events/client_connected" 16 | ## 17 | ## will still work even if 'rule_engine.events.client_connected' is set to 'off' 18 | ## 19 | ## EMQ Event to event message mapping: 20 | ## 21 | ## - client.connected -> $events/client_connected 22 | ## - client.disconnected -> $events/client_disconnected 23 | ## - session.subscribed -> $events/session_subscribed 24 | ## - session.unsubscribed -> $events/session_unsubscribed 25 | ## - message.delivered -> $events/message_delivered 26 | ## - message.acked -> $events/message_acked 27 | ## - message.dropped -> $events/message_dropped 28 | ## 29 | ## Config Value Format: Toggle, QoS-Level 30 | ## 31 | ## Toggle: on/off 32 | ## 33 | ## QoS-Level: qos0/qos1/qos2 34 | 35 | #rule_engine.events.client_connected = on, qos1 36 | rule_engine.events.client_connected = off 37 | rule_engine.events.client_disconnected = off 38 | rule_engine.events.session_subscribed = off 39 | rule_engine.events.session_unsubscribed = off 40 | rule_engine.events.message_delivered = off 41 | rule_engine.events.message_acked = off 42 | rule_engine.events.message_dropped = off 43 | -------------------------------------------------------------------------------- /include/rule_engine.hrl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -define(APP, emqx_rule_engine). 18 | 19 | -type(maybe(T) :: T | undefined). 20 | 21 | -type(rule_id() :: binary()). 22 | -type(rule_name() :: binary()). 23 | 24 | -type(resource_id() :: binary()). 25 | -type(action_instance_id() :: binary()). 26 | 27 | -type(action_name() :: atom()). 28 | -type(resource_type_name() :: atom()). 29 | 30 | -type(category() :: data_persist| data_forward | offline_msgs | debug | other). 31 | 32 | -type(descr() :: #{en := binary(), zh => binary()}). 33 | 34 | -type(mf() :: {Module::atom(), Fun::atom()}). 35 | 36 | -type(hook() :: atom() | 'any'). 37 | 38 | -type(resource_status() :: #{ alive := boolean() 39 | , atom() => binary() | atom() | list(binary()|atom()) 40 | }). 41 | 42 | -define(descr, #{en => <<>>, zh => <<>>}). 43 | 44 | -record(action, 45 | { name :: action_name() 46 | , category :: category() 47 | , for :: hook() 48 | , app :: atom() 49 | , types = [] :: list(resource_type_name()) 50 | , module :: module() 51 | , on_create :: mf() 52 | , on_destroy :: maybe(mf()) 53 | , hidden = false :: boolean() 54 | , params_spec :: #{atom() => term()} %% params specs 55 | , title = ?descr :: descr() 56 | , description = ?descr :: descr() 57 | }). 58 | 59 | -record(action_instance, 60 | { id :: action_instance_id() 61 | , name :: action_name() 62 | , fallbacks :: list(#action_instance{}) 63 | , args :: #{atom() => term()} %% the args got from API for initializing action_instance 64 | }). 65 | 66 | -record(rule, 67 | { id :: rule_id() 68 | , for :: hook() 69 | , rawsql :: binary() 70 | , is_foreach :: boolean() 71 | , fields :: list() 72 | , doeach :: term() 73 | , incase :: list() 74 | , conditions :: tuple() 75 | , on_action_failed :: continue | stop 76 | , actions :: list(#action_instance{}) 77 | , enabled :: boolean() 78 | , description :: binary() 79 | }). 80 | 81 | -record(resource, 82 | { id :: resource_id() 83 | , type :: resource_type_name() 84 | , config :: #{} %% the configs got from API for initializing resource 85 | , created_at :: erlang:timestamp() 86 | , description :: binary() 87 | }). 88 | 89 | -record(resource_type, 90 | { name :: resource_type_name() 91 | , provider :: atom() 92 | , params_spec :: #{atom() => term()} %% params specs 93 | , on_create :: mf() 94 | , on_status :: mf() 95 | , on_destroy :: mf() 96 | , title = ?descr :: descr() 97 | , description = ?descr :: descr() 98 | }). 99 | 100 | -record(rule_hooks, 101 | { hook :: atom() 102 | , rule_id :: rule_id() 103 | }). 104 | 105 | -record(resource_params, 106 | { id :: resource_id() 107 | , params :: #{} %% the params got after initializing the resource 108 | , status = #{is_alive => false} :: #{is_alive := boolean(), atom() => term()} 109 | }). 110 | 111 | -record(action_instance_params, 112 | { id :: action_instance_id() 113 | , params :: #{} %% the params got after initializing the action 114 | , apply :: fun((Data::map(), Envs::map()) -> any()) %% the func got after initializing the action 115 | }). 116 | 117 | %% Arithmetic operators 118 | -define(is_arith(Op), (Op =:= '+' orelse 119 | Op =:= '-' orelse 120 | Op =:= '*' orelse 121 | Op =:= '/' orelse 122 | Op =:= 'div')). 123 | 124 | %% Compare operators 125 | -define(is_comp(Op), (Op =:= '=' orelse 126 | Op =:= '=~' orelse 127 | Op =:= '>' orelse 128 | Op =:= '<' orelse 129 | Op =:= '<=' orelse 130 | Op =:= '>=' orelse 131 | Op =:= '<>' orelse 132 | Op =:= '!=')). 133 | 134 | %% Logical operators 135 | -define(is_logical(Op), (Op =:= 'and' orelse Op =:= 'or')). 136 | 137 | -define(RAISE(_EXP_, _ERROR_), 138 | begin 139 | fun() -> 140 | try (_EXP_) catch _:_REASON_:_ST_ -> throw(_ERROR_) end 141 | end() 142 | end). 143 | 144 | -define(THROW(_EXP_, _ERROR_), 145 | begin 146 | try (_EXP_) catch _:_ -> throw(_ERROR_) end 147 | end). 148 | 149 | %% Tables 150 | -define(RULE_TAB, emqx_rule). 151 | -define(ACTION_TAB, emqx_rule_action). 152 | -define(ACTION_INST_PARAMS_TAB, emqx_action_instance_params). 153 | -define(RES_TAB, emqx_resource). 154 | -define(RES_PARAMS_TAB, emqx_resource_params). 155 | -define(RULE_HOOKS, emqx_rule_hooks). 156 | -define(RES_TYPE_TAB, emqx_resource_type). 157 | -------------------------------------------------------------------------------- /include/rule_events.hrl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -define(COLUMNS(EVENT), [Key || {Key, _ExampleVal} <- ?EG_COLUMNS(EVENT)]). 18 | 19 | -define(EG_COLUMNS(EVENT), 20 | case EVENT of 21 | 'message.publish' -> 22 | [ {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())} 23 | , {<<"clientid">>, <<"c_emqx">>} 24 | , {<<"username">>, <<"u_emqx">>} 25 | , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} 26 | , {<<"peerhost">>, <<"192.168.0.10">>} 27 | , {<<"topic">>, <<"t/a">>} 28 | , {<<"qos">>, 1} 29 | , {<<"flags">>, #{}} 30 | , {<<"headers">>, undefined} 31 | , {<<"publish_received_at">>, erlang:system_time(millisecond)} 32 | , {<<"timestamp">>, erlang:system_time(millisecond)} 33 | , {<<"node">>, node()} 34 | ]; 35 | 'message.delivered' -> 36 | [ {<<"event">>, 'message.delivered'} 37 | , {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())} 38 | , {<<"from_clientid">>, <<"c_emqx_1">>} 39 | , {<<"from_username">>, <<"u_emqx_1">>} 40 | , {<<"clientid">>, <<"c_emqx_2">>} 41 | , {<<"username">>, <<"u_emqx_2">>} 42 | , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} 43 | , {<<"peerhost">>, <<"192.168.0.10">>} 44 | , {<<"topic">>, <<"t/a">>} 45 | , {<<"qos">>, 1} 46 | , {<<"flags">>, #{}} 47 | , {<<"publish_received_at">>, erlang:system_time(millisecond)} 48 | , {<<"timestamp">>, erlang:system_time(millisecond)} 49 | , {<<"node">>, node()} 50 | ]; 51 | 'message.acked' -> 52 | [ {<<"event">>, 'message.acked'} 53 | , {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())} 54 | , {<<"from_clientid">>, <<"c_emqx_1">>} 55 | , {<<"from_username">>, <<"u_emqx_1">>} 56 | , {<<"clientid">>, <<"c_emqx_2">>} 57 | , {<<"username">>, <<"u_emqx_2">>} 58 | , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} 59 | , {<<"peerhost">>, <<"192.168.0.10">>} 60 | , {<<"topic">>, <<"t/a">>} 61 | , {<<"qos">>, 1} 62 | , {<<"flags">>, #{}} 63 | , {<<"publish_received_at">>, erlang:system_time(millisecond)} 64 | , {<<"timestamp">>, erlang:system_time(millisecond)} 65 | , {<<"node">>, node()} 66 | ]; 67 | 'message.dropped' -> 68 | [ {<<"event">>, 'message.dropped'} 69 | , {<<"id">>, emqx_guid:to_hexstr(emqx_guid:gen())} 70 | , {<<"reason">>, no_subscribers} 71 | , {<<"clientid">>, <<"c_emqx">>} 72 | , {<<"username">>, <<"u_emqx">>} 73 | , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} 74 | , {<<"peerhost">>, <<"192.168.0.10">>} 75 | , {<<"topic">>, <<"t/a">>} 76 | , {<<"qos">>, 1} 77 | , {<<"flags">>, #{}} 78 | , {<<"publish_received_at">>, erlang:system_time(millisecond)} 79 | , {<<"timestamp">>, erlang:system_time(millisecond)} 80 | , {<<"node">>, node()} 81 | ]; 82 | 'client.connected' -> 83 | [ {<<"event">>, 'client.connected'} 84 | , {<<"clientid">>, <<"c_emqx">>} 85 | , {<<"username">>, <<"u_emqx">>} 86 | , {<<"mountpoint">>, undefined} 87 | , {<<"peername">>, <<"192.168.0.10:56431">>} 88 | , {<<"sockname">>, <<"0.0.0.0:1883">>} 89 | , {<<"proto_name">>, <<"MQTT">>} 90 | , {<<"proto_ver">>, 5} 91 | , {<<"keepalive">>, 60} 92 | , {<<"clean_start">>, true} 93 | , {<<"expiry_interval">>, 3600} 94 | , {<<"is_bridge">>, false} 95 | , {<<"connected_at">>, erlang:system_time(millisecond)} 96 | , {<<"timestamp">>, erlang:system_time(millisecond)} 97 | , {<<"node">>, node()} 98 | ]; 99 | 'client.disconnected' -> 100 | [ {<<"event">>, 'client.disconnected'} 101 | , {<<"reason">>, normal} 102 | , {<<"clientid">>, <<"c_emqx">>} 103 | , {<<"username">>, <<"u_emqx">>} 104 | , {<<"peername">>, <<"192.168.0.10:56431">>} 105 | , {<<"sockname">>, <<"0.0.0.0:1883">>} 106 | , {<<"disconnected_at">>, erlang:system_time(millisecond)} 107 | , {<<"timestamp">>, erlang:system_time(millisecond)} 108 | , {<<"node">>, node()} 109 | ]; 110 | 'session.subscribed' -> 111 | [ {<<"event">>, 'session.subscribed'} 112 | , {<<"clientid">>, <<"c_emqx">>} 113 | , {<<"username">>, <<"u_emqx">>} 114 | , {<<"peerhost">>, <<"192.168.0.10">>} 115 | , {<<"topic">>, <<"t/a">>} 116 | , {<<"qos">>, 1} 117 | , {<<"timestamp">>, erlang:system_time(millisecond)} 118 | , {<<"node">>, node()} 119 | ]; 120 | 'session.unsubscribed' -> 121 | [ {<<"event">>, 'session.unsubscribed'} 122 | , {<<"clientid">>, <<"c_emqx">>} 123 | , {<<"username">>, <<"u_emqx">>} 124 | , {<<"peerhost">>, <<"192.168.0.10">>} 125 | , {<<"topic">>, <<"t/a">>} 126 | , {<<"qos">>, 1} 127 | , {<<"timestamp">>, erlang:system_time(millisecond)} 128 | , {<<"node">>, node()} 129 | ]; 130 | RuleType -> 131 | error({unknown_rule_type, RuleType}) 132 | end). 133 | 134 | -define(TEST_COLUMNS_MESSGE, 135 | [ {<<"clientid">>, <<"c_emqx">>} 136 | , {<<"username">>, <<"u_emqx">>} 137 | , {<<"topic">>, <<"t/a">>} 138 | , {<<"qos">>, 1} 139 | , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} 140 | ]). 141 | 142 | -define(TEST_COLUMNS_MESSGE_DELIVERED_ACKED, 143 | [ {<<"from_clientid">>, <<"c_emqx_1">>} 144 | , {<<"from_username">>, <<"u_emqx_1">>} 145 | , {<<"clientid">>, <<"c_emqx_2">>} 146 | , {<<"username">>, <<"u_emqx_2">>} 147 | , {<<"topic">>, <<"t/a">>} 148 | , {<<"qos">>, 1} 149 | , {<<"payload">>, <<"{\"msg\": \"hello\"}">>} 150 | ]). 151 | 152 | -define(TEST_COLUMNS(EVENT), 153 | case EVENT of 154 | 'message.publish' -> ?TEST_COLUMNS_MESSGE; 155 | 'message.dropped' -> ?TEST_COLUMNS_MESSGE; 156 | 'message.delivered' -> ?TEST_COLUMNS_MESSGE_DELIVERED_ACKED; 157 | 'message.acked' -> ?TEST_COLUMNS_MESSGE_DELIVERED_ACKED; 158 | 'client.connected' -> 159 | [ {<<"clientid">>, <<"c_emqx">>} 160 | , {<<"username">>, <<"u_emqx">>} 161 | , {<<"peername">>, <<"127.0.0.1:52918">>} 162 | ]; 163 | 'client.disconnected' -> 164 | [ {<<"clientid">>, <<"c_emqx">>} 165 | , {<<"username">>, <<"u_emqx">>} 166 | , {<<"reason">>, <<"normal">>} 167 | ]; 168 | 'session.subscribed' -> 169 | [ {<<"clientid">>, <<"c_emqx">>} 170 | , {<<"username">>, <<"u_emqx">>} 171 | , {<<"topic">>, <<"t/a">>} 172 | , {<<"qos">>, 1} 173 | ]; 174 | 'session.unsubscribed' -> 175 | [ {<<"clientid">>, <<"c_emqx">>} 176 | , {<<"username">>, <<"u_emqx">>} 177 | , {<<"topic">>, <<"t/a">>} 178 | , {<<"qos">>, 1} 179 | ]; 180 | RuleType -> 181 | error({unknown_rule_type, RuleType}) 182 | end). 183 | 184 | -define(EVENT_INFO_MESSAGE_PUBLISH, 185 | #{ event => '$events/message_publish', 186 | title => #{en => <<"message publish">>, zh => <<"消息发布"/utf8>>}, 187 | description => #{en => <<"message publish">>, zh => <<"消息发布"/utf8>>}, 188 | test_columns => ?TEST_COLUMNS('message.publish'), 189 | columns => ?COLUMNS('message.publish'), 190 | sql_example => <<"SELECT payload.msg as msg FROM \"t/#\" WHERE msg = 'hello'">> 191 | }). 192 | 193 | -define(EVENT_INFO_MESSAGE_DELIVER, 194 | #{ event => '$events/message_delivered', 195 | title => #{en => <<"message delivered">>, zh => <<"消息投递"/utf8>>}, 196 | description => #{en => <<"message delivered">>, zh => <<"消息投递"/utf8>>}, 197 | test_columns => ?TEST_COLUMNS('message.delivered'), 198 | columns => ?COLUMNS('message.delivered'), 199 | sql_example => <<"SELECT * FROM \"$events/message_delivered\" WHERE topic =~ 't/#'">> 200 | }). 201 | 202 | -define(EVENT_INFO_MESSAGE_ACKED, 203 | #{ event => '$events/message_acked', 204 | title => #{en => <<"message acked">>, zh => <<"消息应答"/utf8>>}, 205 | description => #{en => <<"message acked">>, zh => <<"消息应答"/utf8>>}, 206 | test_columns => ?TEST_COLUMNS('message.acked'), 207 | columns => ?COLUMNS('message.acked'), 208 | sql_example => <<"SELECT * FROM \"$events/message_acked\" WHERE topic =~ 't/#'">> 209 | }). 210 | 211 | -define(EVENT_INFO_MESSAGE_DROPPED, 212 | #{ event => '$events/message_dropped', 213 | title => #{en => <<"message dropped">>, zh => <<"消息丢弃"/utf8>>}, 214 | description => #{en => <<"message dropped">>, zh => <<"消息丢弃"/utf8>>}, 215 | test_columns => ?TEST_COLUMNS('message.dropped'), 216 | columns => ?COLUMNS('message.dropped'), 217 | sql_example => <<"SELECT * FROM \"$events/message_dropped\" WHERE topic =~ 't/#'">> 218 | }). 219 | 220 | -define(EVENT_INFO_CLIENT_CONNECTED, 221 | #{ event => '$events/client_connected', 222 | title => #{en => <<"client connected">>, zh => <<"连接建立"/utf8>>}, 223 | description => #{en => <<"client connected">>, zh => <<"连接建立"/utf8>>}, 224 | test_columns => ?TEST_COLUMNS('client.connected'), 225 | columns => ?COLUMNS('client.connected'), 226 | sql_example => <<"SELECT * FROM \"$events/client_connected\"">> 227 | }). 228 | 229 | -define(EVENT_INFO_CLIENT_DISCONNECTED, 230 | #{ event => '$events/client_disconnected', 231 | title => #{en => <<"client disconnected">>, zh => <<"连接断开"/utf8>>}, 232 | description => #{en => <<"client disconnected">>, zh => <<"连接断开"/utf8>>}, 233 | test_columns => ?TEST_COLUMNS('client.disconnected'), 234 | columns => ?COLUMNS('client.disconnected'), 235 | sql_example => <<"SELECT * FROM \"$events/client_disconnected\"">> 236 | }). 237 | 238 | -define(EVENT_INFO_SESSION_SUBSCRIBED, 239 | #{ event => '$events/session_subscribed', 240 | title => #{en => <<"session subscribed">>, zh => <<"会话订阅完成"/utf8>>}, 241 | description => #{en => <<"session subscribed">>, zh => <<"会话订阅完成"/utf8>>}, 242 | test_columns => ?TEST_COLUMNS('session.subscribed'), 243 | columns => ?COLUMNS('session.subscribed'), 244 | sql_example => <<"SELECT * FROM \"$events/session_subscribed\" WHERE topic =~ 't/#'">> 245 | }). 246 | 247 | -define(EVENT_INFO_SESSION_UNSUBSCRIBED, 248 | #{ event => '$events/session_unsubscribed', 249 | title => #{en => <<"session unsubscribed">>, zh => <<"会话取消订阅完成"/utf8>>}, 250 | description => #{en => <<"session unsubscribed">>, zh => <<"会话取消订阅完成"/utf8>>}, 251 | test_columns => ?TEST_COLUMNS('session.unsubscribed'), 252 | columns => ?COLUMNS('session.unsubscribed'), 253 | sql_example => <<"SELECT * FROM \"$events/session_unsubscribed\" WHERE topic =~ 't/#'">> 254 | }). 255 | 256 | -define(EVENT_INFO, 257 | [ ?EVENT_INFO_MESSAGE_PUBLISH 258 | , ?EVENT_INFO_MESSAGE_DELIVER 259 | , ?EVENT_INFO_MESSAGE_ACKED 260 | , ?EVENT_INFO_MESSAGE_DROPPED 261 | , ?EVENT_INFO_CLIENT_CONNECTED 262 | , ?EVENT_INFO_CLIENT_DISCONNECTED 263 | , ?EVENT_INFO_SESSION_SUBSCRIBED 264 | , ?EVENT_INFO_SESSION_UNSUBSCRIBED 265 | ]). 266 | 267 | -define(EG_ENVS(EVENT_TOPIC), 268 | case EVENT_TOPIC of 269 | <<"$events/", _/binary>> -> 270 | EventName = emqx_rule_events:event_name(EVENT_TOPIC), 271 | emqx_rule_maps:atom_key_map(maps:from_list(?EG_COLUMNS(EventName))); 272 | _PublishTopic -> 273 | #{id => emqx_guid:to_hexstr(emqx_guid:gen()), 274 | clientid => <<"c_emqx">>, 275 | username => <<"u_emqx">>, 276 | payload => <<"{\"id\": 1, \"name\": \"ha\"}">>, 277 | peerhost => <<"127.0.0.1">>, 278 | topic => <<"t/a">>, 279 | qos => 1, 280 | flags => #{sys => true, event => true}, 281 | publish_received_at => emqx_rule_utils:now_ms(), 282 | timestamp => emqx_rule_utils:now_ms(), 283 | node => node() 284 | } 285 | end). 286 | -------------------------------------------------------------------------------- /priv/emqx_rule_engine.schema: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | %% emqx_rule_engine config mapping 3 | 4 | {mapping, "rule_engine.ignore_sys_message", "emqx_rule_engine.ignore_sys_message", [ 5 | {default, on}, 6 | {datatype, flag} 7 | ]}. 8 | 9 | {mapping, "rule_engine.events.$name", "emqx_rule_engine.events", [ 10 | {default, "off, qos1"}, 11 | {datatype, string} 12 | ]}. 13 | 14 | {translation, "emqx_rule_engine.events", fun(Conf) -> 15 | SupportedHooks = 16 | [ 'client.connected' 17 | , 'client.disconnected' 18 | , 'session.subscribed' 19 | , 'session.unsubscribed' 20 | , 'message.delivered' 21 | , 'message.acked' 22 | , 'message.dropped' 23 | ], 24 | 25 | HookPoint = fun(Event) -> 26 | case string:split(Event, "_") of 27 | [Prefix, Name] -> 28 | Point = list_to_atom(lists:append([Prefix, ".", Name])), 29 | case lists:member(Point, SupportedHooks) of 30 | true -> Point; 31 | false -> error({unsupported_event, Event}) 32 | end; 33 | [_] -> 34 | error({invalid_event, Event}) 35 | end 36 | end, 37 | 38 | QoS = fun ("qos"++Level = QoSLevel) -> 39 | case list_to_integer(Level) of 40 | QoSL when QoSL =:= 0; QoSL =:= 1; QoSL =:= 2 -> 41 | QoSL; 42 | _ -> 43 | error({invalid_qos_level, QoSLevel}) 44 | end; 45 | (QoSLevel) -> 46 | error({invalid_qos, QoSLevel}) 47 | end, 48 | 49 | lists:foldl( 50 | fun({EE=[_,"events",EvtName], Val}, Acc) -> 51 | case string:split(string:trim(Val), ",", all) of 52 | ["on"++_, Snd] -> 53 | [{HookPoint(EvtName), on, QoS(string:trim(Snd))} | Acc]; 54 | ["on"++_] -> 55 | [{HookPoint(EvtName), on, 1} | Acc]; 56 | [_] -> 57 | Acc 58 | end; 59 | ({_, _}, Acc) -> Acc 60 | end, [], cuttlefish_variable:filter_by_prefix("rule_engine.events", Conf)) 61 | end}. 62 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {minimum_otp_vsn, "21.0"}. 2 | 3 | {deps, 4 | [{minirest, {git, "https://github.com/emqx/minirest", {tag, "0.3.2"}}}, 5 | {rulesql, {git, "https://github.com/emqx/rulesql", {tag, "0.1.2"}}}, 6 | {getopt, "1.0.1"} 7 | ]}. 8 | 9 | {erl_opts, [warn_unused_vars, 10 | warn_shadow_vars, 11 | warn_unused_import, 12 | warn_obsolete_guard, 13 | no_debug_info, 14 | compressed, %% for edge 15 | {parse_transform} 16 | ]}. 17 | 18 | {overrides, [{add, [{erl_opts, [no_debug_info, compressed]}]}]}. 19 | 20 | {edoc_opts, [{preprocess, true}]}. 21 | 22 | {xref_checks, [undefined_function_calls, undefined_functions, 23 | locals_not_used, deprecated_function_calls, 24 | warnings_as_errors, deprecated_functions 25 | ]}. 26 | 27 | {cover_enabled, true}. 28 | {cover_opts, [verbose]}. 29 | {cover_export_enabled, true}. 30 | 31 | {plugins, [rebar3_proper]}. 32 | 33 | {profiles, 34 | [{test, 35 | [{deps, 36 | [{emqx_ct_helpers, {git, "https://github.com/emqx/emqx-ct-helpers", {tag, "1.2.1"}}}, 37 | {emqtt, {git, "https://github.com/emqx/emqtt", {tag, "1.1.1"}}} 38 | ]}, 39 | {erl_opts, [debug_info]} 40 | ]} 41 | ]}. 42 | 43 | -------------------------------------------------------------------------------- /rebar.config.script: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | 3 | DEPS = case lists:keyfind(deps, 1, CONFIG) of 4 | {_, Deps} -> Deps; 5 | _ -> [] 6 | end, 7 | 8 | ComparingFun = fun 9 | _Fun([C1|R1], [C2|R2]) when is_list(C1), is_list(C2); 10 | is_integer(C1), is_integer(C2) -> C1 < C2 orelse _Fun(R1, R2); 11 | _Fun([C1|R1], [C2|R2]) when is_integer(C1), is_list(C2) -> _Fun(R1, R2); 12 | _Fun([C1|R1], [C2|R2]) when is_list(C1), is_integer(C2) -> true; 13 | _Fun(_, _) -> false 14 | end, 15 | 16 | SortFun = fun(T1, T2) -> 17 | C = fun(T) -> 18 | [case catch list_to_integer(E) of 19 | I when is_integer(I) -> I; 20 | _ -> E 21 | end || E <- re:split(string:sub_string(T, 2), "[.-]", [{return, list}])] 22 | end, 23 | ComparingFun(C(T1), C(T2)) 24 | end, 25 | 26 | VTags = string:tokens(os:cmd("git tag -l \"v*\" --points-at $(git rev-parse $(git describe --abbrev=0 --tags))"), "\n"), 27 | 28 | Tags = case VTags of 29 | [] -> string:tokens(os:cmd("git tag -l \"e*\" --points-at $(git rev-parse $(git describe --abbrev=0 --tags))"), "\n"); 30 | _ -> VTags 31 | end, 32 | 33 | LatestTag = lists:last(lists:sort(SortFun, Tags)), 34 | 35 | Branch = case os:getenv("GITHUB_RUN_ID") of 36 | false -> os:cmd("git branch | grep -e '^*' | cut -d' ' -f 2") -- "\n"; 37 | _ -> re:replace(os:getenv("GITHUB_REF"), "^refs/heads/|^refs/tags/", "", [global, {return ,list}]) 38 | end, 39 | 40 | GitDescribe = case re:run(Branch, "master|^dev/|^hotfix/", [{capture, none}]) of 41 | match -> {branch, Branch}; 42 | _ -> {tag, LatestTag} 43 | end, 44 | 45 | UrlPrefix = "https://github.com/emqx/", 46 | 47 | EMQX_DEP = {emqx, {git, UrlPrefix ++ "emqx", GitDescribe}}, 48 | EMQX_MGMT_DEP = {emqx_management, {git, UrlPrefix ++ "emqx-management", GitDescribe}}, 49 | 50 | NewDeps = [EMQX_DEP, EMQX_MGMT_DEP | DEPS], 51 | 52 | CONFIG1 = lists:keystore(deps, 1, CONFIG, {deps, NewDeps}), 53 | 54 | CONFIG1. 55 | -------------------------------------------------------------------------------- /src/emqx_rule_actions.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | %% Define the default actions. 18 | -module(emqx_rule_actions). 19 | 20 | -include("rule_engine.hrl"). 21 | -include_lib("emqx/include/emqx.hrl"). 22 | -include_lib("emqx/include/logger.hrl"). 23 | 24 | -define(REPUBLISH_PARAMS_SPEC, #{ 25 | target_topic => #{ 26 | order => 1, 27 | type => string, 28 | required => true, 29 | default => <<"repub/to/${clientid}">>, 30 | title => #{en => <<"Target Topic">>, 31 | zh => <<"目的主题"/utf8>>}, 32 | description => #{en => <<"To which topic the message will be republished">>, 33 | zh => <<"重新发布消息到哪个主题"/utf8>>} 34 | }, 35 | target_qos => #{ 36 | order => 2, 37 | type => number, 38 | enum => [-1, 0, 1, 2], 39 | required => true, 40 | default => 0, 41 | title => #{en => <<"Target QoS">>, 42 | zh => <<"目的 QoS"/utf8>>}, 43 | description => #{en => <<"The QoS Level to be uses when republishing the message. Set to -1 to use the original QoS">>, 44 | zh => <<"重新发布消息时用的 QoS 级别, 设置为 -1 以使用原消息中的 QoS"/utf8>>} 45 | }, 46 | payload_tmpl => #{ 47 | order => 3, 48 | type => string, 49 | input => textarea, 50 | required => true, 51 | default => <<"${payload}">>, 52 | title => #{en => <<"Payload Template">>, 53 | zh => <<"消息内容模板"/utf8>>}, 54 | description => #{en => <<"The payload template, variable interpolation is supported">>, 55 | zh => <<"消息内容模板,支持变量"/utf8>>} 56 | } 57 | }). 58 | 59 | -rule_action(#{name => inspect, 60 | category => debug, 61 | for => '$any', 62 | types => [], 63 | create => on_action_create_inspect, 64 | params => #{}, 65 | title => #{en => <<"Inspect (debug)">>, 66 | zh => <<"检查 (调试)"/utf8>>}, 67 | description => #{en => <<"Inspect the details of action params for debug purpose">>, 68 | zh => <<"检查动作参数 (用以调试)"/utf8>>} 69 | }). 70 | 71 | -rule_action(#{name => republish, 72 | category => data_forward, 73 | for => '$any', 74 | types => [], 75 | create => on_action_create_republish, 76 | params => ?REPUBLISH_PARAMS_SPEC, 77 | title => #{en => <<"Republish">>, 78 | zh => <<"消息重新发布"/utf8>>}, 79 | description => #{en => <<"Republish a MQTT message to another topic">>, 80 | zh => <<"重新发布消息到另一个主题"/utf8>>} 81 | }). 82 | 83 | -rule_action(#{name => do_nothing, 84 | category => debug, 85 | for => '$any', 86 | types => [], 87 | create => on_action_do_nothing, 88 | params => #{}, 89 | title => #{en => <<"Do Nothing (debug)">>, 90 | zh => <<"空动作 (调试)"/utf8>>}, 91 | description => #{en => <<"This action does nothing and never fails. It's for debug purpose">>, 92 | zh => <<"此动作什么都不做,并且不会失败 (用以调试)"/utf8>>} 93 | }). 94 | 95 | -type(action_fun() :: fun((SelectedData::map(), Envs::map()) -> Result::any())). 96 | 97 | -export_type([action_fun/0]). 98 | 99 | -export([on_resource_create/2]). 100 | 101 | -export([ on_action_create_inspect/2 102 | , on_action_create_republish/2 103 | , on_action_do_nothing/2 104 | ]). 105 | 106 | %%------------------------------------------------------------------------------ 107 | %% Default actions for the Rule Engine 108 | %%------------------------------------------------------------------------------ 109 | 110 | -spec(on_resource_create(binary(), map()) -> map()). 111 | on_resource_create(_Name, Conf) -> 112 | Conf. 113 | 114 | -spec(on_action_create_inspect(action_instance_id(), Params :: map()) -> action_fun()). 115 | on_action_create_inspect(_Id, Params) -> 116 | fun(Selected, Envs) -> 117 | io:format("[inspect]~n" 118 | "\tSelected Data: ~p~n" 119 | "\tEnvs: ~p~n" 120 | "\tAction Init Params: ~p~n", [Selected, Envs, Params]) 121 | end. 122 | 123 | %% A Demo Action. 124 | -spec(on_action_create_republish(action_instance_id(), #{binary() := emqx_topic:topic()}) 125 | -> action_fun()). 126 | on_action_create_republish(Id, #{<<"target_topic">> := TargetTopic, <<"target_qos">> := TargetQoS, <<"payload_tmpl">> := PayloadTmpl}) -> 127 | TopicTks = emqx_rule_utils:preproc_tmpl(TargetTopic), 128 | PayloadTks = emqx_rule_utils:preproc_tmpl(PayloadTmpl), 129 | fun (_Selected, Envs = #{headers := #{republish_by := ActId}, 130 | topic := Topic}) when ActId =:= Id -> 131 | ?LOG(error, "[republish] recursively republish detected, msg topic: ~p, target topic: ~p", 132 | [Topic, TargetTopic]), 133 | error({recursive_republish, Envs}); 134 | (Selected, _Envs = #{qos := QoS, flags := Flags, timestamp := Timestamp}) -> 135 | ?LOG(debug, "[republish] republish to: ~p, Payload: ~p", 136 | [TargetTopic, Selected]), 137 | increase_and_publish( 138 | #message{ 139 | id = emqx_guid:gen(), 140 | qos = if TargetQoS =:= -1 -> QoS; true -> TargetQoS end, 141 | from = Id, 142 | flags = Flags, 143 | headers = #{republish_by => Id}, 144 | topic = emqx_rule_utils:proc_tmpl(TopicTks, Selected), 145 | payload = emqx_rule_utils:proc_tmpl(PayloadTks, Selected), 146 | timestamp = Timestamp 147 | }); 148 | %% in case this is not a "message.publish" request 149 | (Selected, _Envs) -> 150 | ?LOG(debug, "[republish] republish to: ~p, Payload: ~p", 151 | [TargetTopic, Selected]), 152 | increase_and_publish( 153 | #message{ 154 | id = emqx_guid:gen(), 155 | qos = if TargetQoS =:= -1 -> 0; true -> TargetQoS end, 156 | from = Id, 157 | flags = #{dup => false, retain => false}, 158 | headers = #{republish_by => Id}, 159 | topic = emqx_rule_utils:proc_tmpl(TopicTks, Selected), 160 | payload = emqx_rule_utils:proc_tmpl(PayloadTks, Selected), 161 | timestamp = erlang:system_time(millisecond) 162 | }) 163 | end. 164 | 165 | increase_and_publish(Msg) -> 166 | emqx_metrics:inc_msg(Msg), 167 | emqx_broker:safe_publish(Msg). 168 | 169 | on_action_do_nothing(_, _) -> 170 | fun(_, _) -> ok end. 171 | -------------------------------------------------------------------------------- /src/emqx_rule_engine.app.src: -------------------------------------------------------------------------------- 1 | {application, emqx_rule_engine, 2 | [{description, "EMQ X Rule Engine"}, 3 | {vsn, "git"}, 4 | {modules, []}, 5 | {registered, [emqx_rule_engine_sup, emqx_rule_registry]}, 6 | {applications, [kernel,stdlib,rulesql,getopt]}, 7 | {mod, {emqx_rule_engine_app, []}}, 8 | {env, []}, 9 | {licenses, ["Apache-2.0"]}, 10 | {maintainers, ["EMQ X Team "]}, 11 | {links, [{"Homepage", "https://emqx.io/"}, 12 | {"Github", "https://github.com/emqx/emqx-rule-engine"} 13 | ]} 14 | ]}. 15 | -------------------------------------------------------------------------------- /src/emqx_rule_engine.app.src.script: -------------------------------------------------------------------------------- 1 | %%-*- mode: erlang -*- 2 | %% .app.src.script 3 | 4 | RemoveLeadingV = 5 | fun(Tag) -> 6 | case re:run(Tag, "^[v|e]?[0-9]\.[0-9]\.([0-9]|(rc|beta|alpha)\.[0-9])", [{capture, none}]) of 7 | nomatch -> 8 | re:replace(Tag, "/", "-", [{return ,list}]); 9 | _ -> 10 | %% if it is a version number prefixed by 'v' or 'e', then remove it 11 | re:replace(Tag, "[v|e]", "", [{return ,list}]) 12 | end 13 | end, 14 | 15 | case os:getenv("EMQX_DEPS_DEFAULT_VSN") of 16 | false -> CONFIG; % env var not defined 17 | [] -> CONFIG; % env var set to empty string 18 | Tag -> 19 | [begin 20 | AppConf0 = lists:keystore(vsn, 1, AppConf, {vsn, RemoveLeadingV(Tag)}), 21 | {application, App, AppConf0} 22 | end || Conf = {application, App, AppConf} <- CONFIG] 23 | end. 24 | 25 | -------------------------------------------------------------------------------- /src/emqx_rule_engine.appup.src: -------------------------------------------------------------------------------- 1 | %% -*-: erlang -*- 2 | 3 | {VSN, 4 | [ 5 | {"4.2.0", [ 6 | {load_module, emqx_rule_events, brutal_purge, soft_purge, []}, 7 | {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []}, 8 | {load_module, emqx_rule_maps, brutal_purge, soft_purge, []}, 9 | {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}, 10 | {load_module, emqx_rule_actions, brutal_purge, soft_purge, []} 11 | ]}, 12 | {"4.2.1", [ 13 | {load_module, emqx_rule_events, brutal_purge, soft_purge, []}, 14 | {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []}, 15 | {load_module, emqx_rule_maps, brutal_purge, soft_purge, []}, 16 | {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}, 17 | {load_module, emqx_rule_actions, brutal_purge, soft_purge, []} 18 | ]}, 19 | {"4.2.2", [ 20 | {load_module, emqx_rule_events, brutal_purge, soft_purge, []}, 21 | {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []}, 22 | {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}, 23 | {load_module, emqx_rule_actions, brutal_purge, soft_purge, []} 24 | ]}, 25 | {"4.2.3", [ 26 | {load_module, emqx_rule_events, brutal_purge, soft_purge, []} 27 | ]}, 28 | {<<".*">>, []} 29 | ], 30 | [ 31 | {"4.2.0", [ 32 | {load_module, emqx_rule_events, brutal_purge, soft_purge, []}, 33 | {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []}, 34 | {load_module, emqx_rule_maps, brutal_purge, soft_purge, []}, 35 | {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}, 36 | {load_module, emqx_rule_actions, brutal_purge, soft_purge, []} 37 | ]}, 38 | {"4.2.1", [ 39 | {load_module, emqx_rule_events, brutal_purge, soft_purge, []}, 40 | {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []}, 41 | {load_module, emqx_rule_maps, brutal_purge, soft_purge, []}, 42 | {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}, 43 | {load_module, emqx_rule_actions, brutal_purge, soft_purge, []} 44 | ]}, 45 | {"4.2.2", [ 46 | {load_module, emqx_rule_events, brutal_purge, soft_purge, []}, 47 | {load_module, emqx_rule_actions, brutal_purge, soft_purge, []}, 48 | {load_module, emqx_rule_engine, brutal_purge, soft_purge, []}, 49 | {load_module, emqx_rule_funcs, brutal_purge, soft_purge, []} 50 | ]}, 51 | {"4.2.3", [ 52 | {load_module, emqx_rule_events, brutal_purge, soft_purge, []} 53 | ]}, 54 | {<<".*">>, []} 55 | ] 56 | }. 57 | -------------------------------------------------------------------------------- /src/emqx_rule_engine_app.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_engine_app). 18 | 19 | -behaviour(application). 20 | 21 | -emqx_plugin(?MODULE). 22 | 23 | -export([start/2]). 24 | 25 | -export([stop/1]). 26 | 27 | -define(APP, emqx_rule_engine). 28 | 29 | start(_Type, _Args) -> 30 | {ok, Sup} = emqx_rule_engine_sup:start_link(), 31 | ok = emqx_rule_engine:load_providers(), 32 | ok = emqx_rule_engine:refresh_resources(), 33 | ok = emqx_rule_engine:refresh_rules(), 34 | ok = emqx_rule_engine_cli:load(), 35 | ok = emqx_rule_events:load(env()), 36 | {ok, Sup}. 37 | 38 | stop(_State) -> 39 | ok = emqx_rule_events:unload(env()), 40 | ok = emqx_rule_engine_cli:unload(). 41 | 42 | env() -> 43 | application:get_all_env(?APP) 44 | . 45 | -------------------------------------------------------------------------------- /src/emqx_rule_engine_cli.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_engine_cli). 18 | 19 | -include("rule_engine.hrl"). 20 | 21 | -export([ load/0 22 | , commands/0 23 | , unload/0 24 | ]). 25 | 26 | -export([ rules/1 27 | , actions/1 28 | , resources/1 29 | , resource_types/1 30 | ]). 31 | 32 | -import(proplists, [get_value/2]). 33 | 34 | -define(OPTSPEC_RESOURCE_TYPE, 35 | [{type, $t, "type", {atom, undefined}, "Resource Type"}]). 36 | -define(OPTSPEC_ACTION_TYPE, 37 | [ {eventype, $k, "eventype", {atom, undefined}, "Event Type"} 38 | ]). 39 | 40 | -define(OPTSPEC_RESOURCES_CREATE, 41 | [ {type, undefined, undefined, atom, "Resource Type"} 42 | , {id, $i, "id", {binary, <<"">>}, "The resource id. A random resource id will be used if not provided"} 43 | , {config, $c, "config", {binary, <<"{}">>}, "Config"} 44 | , {descr, $d, "descr", {binary, <<"">>}, "Description"} 45 | ]). 46 | 47 | -define(OPTSPEC_RULES_CREATE, 48 | [ {sql, undefined, undefined, binary, "Filter Condition SQL"} 49 | , {actions, undefined, undefined, binary, "Action List in JSON format: [{\"name\": , \"params\": {: }}]"} 50 | , {id, $i, "id", {binary, <<"">>}, "The rule id. A random rule id will be used if not provided"} 51 | , {enabled, $e, "enabled", {atom, true}, "'true' or 'false' to enable or disable the rule"} 52 | , {on_action_failed, $g, "on_action_failed", {atom, continue}, "'continue' or 'stop' when an action in the rule fails"} 53 | , {descr, $d, "descr", {binary, <<"">>}, "Description"} 54 | ]). 55 | 56 | -define(OPTSPEC_RULES_UPDATE, 57 | [ {id, undefined, undefined, binary, "Rule ID"} 58 | , {sql, $s, "sql", {binary, undefined}, "Filter Condition SQL"} 59 | , {actions, $a, "actions", {binary, undefined}, "Action List in JSON format: [{\"name\": , \"params\": {: }}]"} 60 | , {enabled, $e, "enabled", {atom, undefined}, "'true' or 'false' to enable or disable the rule"} 61 | , {on_action_failed, $g, "on_action_failed", {atom, undefined}, "'continue' or 'stop' when an action in the rule fails"} 62 | , {descr, $d, "descr", {binary, undefined}, "Description"} 63 | ]). 64 | 65 | %%----------------------------------------------------------------------------- 66 | %% Load/Unload Commands 67 | %%----------------------------------------------------------------------------- 68 | 69 | -spec(load() -> ok). 70 | load() -> 71 | lists:foreach( 72 | fun({Cmd, Func}) -> 73 | emqx_ctl:register_command(Cmd, {?MODULE, Func}, []); 74 | (Cmd) -> 75 | emqx_ctl:register_command(Cmd, {?MODULE, Cmd}, []) 76 | end, commands()). 77 | 78 | -spec(commands() -> list(atom())). 79 | commands() -> 80 | [rules, {'rule-actions', actions}, resources, {'resource-types', resource_types}]. 81 | 82 | -spec(unload() -> ok). 83 | unload() -> 84 | lists:foreach( 85 | fun({Cmd, _Func}) -> 86 | emqx_ctl:unregister_command(Cmd); 87 | (Cmd) -> 88 | emqx_ctl:unregister_command(Cmd) 89 | end, commands()). 90 | 91 | %%----------------------------------------------------------------------------- 92 | %% 'rules' command 93 | %%----------------------------------------------------------------------------- 94 | 95 | rules(["list"]) -> 96 | print_all(emqx_rule_registry:get_rules()); 97 | 98 | rules(["show", RuleId]) -> 99 | print_with(fun emqx_rule_registry:get_rule/1, list_to_binary(RuleId)); 100 | 101 | rules(["create" | Params]) -> 102 | with_opts(fun({Opts, _}) -> 103 | try emqx_rule_engine:create_rule(make_rule(Opts)) of 104 | {ok, #rule{id = RuleId}} -> 105 | emqx_ctl:print("Rule ~s created~n", [RuleId]); 106 | {error, Reason} -> 107 | emqx_ctl:print("Invalid options: ~0p~n", [Reason]) 108 | catch 109 | throw:Error -> 110 | emqx_ctl:print("Invalid options: ~0p~n", [Error]) 111 | end 112 | end, Params, ?OPTSPEC_RULES_CREATE, {?FUNCTION_NAME, create}); 113 | 114 | rules(["update" | Params]) -> 115 | with_opts(fun({Opts, _}) -> 116 | try emqx_rule_engine:update_rule(make_updated_rule(Opts)) of 117 | {ok, #rule{id = RuleId}} -> 118 | emqx_ctl:print("Rule ~s updated~n", [RuleId]); 119 | {error, Reason} -> 120 | emqx_ctl:print("Invalid options: ~0p~n", [Reason]) 121 | catch 122 | throw:Error -> 123 | emqx_ctl:print("Invalid options: ~0p~n", [Error]) 124 | end 125 | end, Params, ?OPTSPEC_RULES_UPDATE, {?FUNCTION_NAME, update}); 126 | 127 | rules(["delete", RuleId]) -> 128 | ok = emqx_rule_engine:delete_rule(list_to_binary(RuleId)), 129 | emqx_ctl:print("ok~n"); 130 | 131 | rules(_usage) -> 132 | emqx_ctl:usage([{"rules list", "List all rules"}, 133 | {"rules show ", "Show a rule"}, 134 | {"rules create", "Create a rule"}, 135 | {"rules delete ", "Delete a rule"} 136 | ]). 137 | 138 | %%----------------------------------------------------------------------------- 139 | %% 'rule-actions' command 140 | %%----------------------------------------------------------------------------- 141 | 142 | actions(["list"]) -> 143 | print_all(get_actions()); 144 | 145 | actions(["show", ActionId]) -> 146 | print_with(fun emqx_rule_registry:find_action/1, ?RAISE(list_to_existing_atom(ActionId), {not_found, ActionId})); 147 | 148 | actions(_usage) -> 149 | emqx_ctl:usage([{"rule-actions list", "List actions"}, 150 | {"rule-actions show ", "Show a rule action"} 151 | ]). 152 | 153 | %%------------------------------------------------------------------------------ 154 | %% 'resources' command 155 | %%------------------------------------------------------------------------------ 156 | resources(["create" | Params]) -> 157 | with_opts(fun({Opts, _}) -> 158 | try emqx_rule_engine:create_resource(make_resource(Opts)) of 159 | {ok, #resource{id = ResId}} -> 160 | emqx_ctl:print("Resource ~s created~n", [ResId]); 161 | {error, Reason} -> 162 | emqx_ctl:print("Invalid options: ~0p~n", [Reason]) 163 | catch 164 | throw:Reason -> 165 | emqx_ctl:print("Invalid options: ~0p~n", [Reason]) 166 | end 167 | end, Params, ?OPTSPEC_RESOURCES_CREATE, {?FUNCTION_NAME, create}); 168 | 169 | resources(["test" | Params]) -> 170 | with_opts(fun({Opts, _}) -> 171 | try emqx_rule_engine:test_resource(make_resource(Opts)) of 172 | ok -> 173 | emqx_ctl:print("Test creating resource successfully (dry-run)~n"); 174 | {error, Reason} -> 175 | emqx_ctl:print("Test creating resource failed: ~0p~n", [Reason]) 176 | catch 177 | throw:Reason -> 178 | emqx_ctl:print("Test creating resource failed: ~0p~n", [Reason]) 179 | end 180 | end, Params, ?OPTSPEC_RESOURCES_CREATE, {?FUNCTION_NAME, test}); 181 | 182 | resources(["list"]) -> 183 | print_all(emqx_rule_registry:get_resources()); 184 | 185 | resources(["list" | Params]) -> 186 | with_opts(fun({Opts, _}) -> 187 | print_all(emqx_rule_registry:get_resources_by_type( 188 | get_value(type, Opts))) 189 | end, Params, ?OPTSPEC_RESOURCE_TYPE, {?FUNCTION_NAME, list}); 190 | 191 | resources(["show", ResourceId]) -> 192 | print_with(fun emqx_rule_registry:find_resource/1, list_to_binary(ResourceId)); 193 | 194 | resources(["delete", ResourceId]) -> 195 | try 196 | ok = emqx_rule_engine:delete_resource(list_to_binary(ResourceId)), 197 | emqx_ctl:print("ok~n") 198 | catch 199 | _Error:Reason -> 200 | emqx_ctl:print("Cannot delete resource as ~0p~n", [Reason]) 201 | end; 202 | 203 | resources(_usage) -> 204 | emqx_ctl:usage([{"resources create", "Create a resource"}, 205 | {"resources list [-t ]", "List resources"}, 206 | {"resources show ", "Show a resource"}, 207 | {"resources delete ", "Delete a resource"} 208 | ]). 209 | 210 | %%------------------------------------------------------------------------------ 211 | %% 'resource-types' command 212 | %%------------------------------------------------------------------------------ 213 | resource_types(["list"]) -> 214 | print_all(emqx_rule_registry:get_resource_types()); 215 | 216 | resource_types(["show", Name]) -> 217 | print_with(fun emqx_rule_registry:find_resource_type/1, list_to_atom(Name)); 218 | 219 | resource_types(_usage) -> 220 | emqx_ctl:usage([{"resource-types list", "List all resource-types"}, 221 | {"resource-types show ", "Show a resource-type"} 222 | ]). 223 | 224 | %%------------------------------------------------------------------------------ 225 | %% Internal functions 226 | %%------------------------------------------------------------------------------ 227 | 228 | print(Data) -> 229 | emqx_ctl:print(untilde(format(Data))). 230 | 231 | print_all(DataList) -> 232 | lists:map(fun(Data) -> 233 | print(Data) 234 | end, DataList). 235 | 236 | print_with(FindFun, Key) -> 237 | case FindFun(Key) of 238 | {ok, R} -> 239 | print(R); 240 | not_found -> 241 | emqx_ctl:print("Cannot found ~s~n", [Key]) 242 | end. 243 | 244 | format(#rule{id = Id, 245 | for = Hook, 246 | rawsql = Sql, 247 | actions = Actions, 248 | on_action_failed = OnFailed, 249 | enabled = Enabled, 250 | description = Descr}) -> 251 | lists:flatten(io_lib:format("rule(id='~s', for='~0p', rawsql='~s', actions=~0p, on_action_failed='~s', metrics=~0p, enabled='~s', description='~s')~n", [Id, Hook, rmlf(Sql), printable_actions(Actions), OnFailed, get_rule_metrics(Id), Enabled, Descr])); 252 | 253 | format(#action{hidden = true}) -> 254 | ok; 255 | format(#action{name = Name, 256 | for = Hook, 257 | app = App, 258 | types = Types, 259 | title = #{en := Title}, 260 | description = #{en := Descr}}) -> 261 | lists:flatten(io_lib:format("action(name='~s', app='~s', for='~s', types=~0p, title ='~s', description='~s')~n", [Name, App, Hook, Types, Title, Descr])); 262 | 263 | format(#resource{id = Id, 264 | type = Type, 265 | config = Config, 266 | description = Descr}) -> 267 | Status = 268 | [begin 269 | {ok, St} = rpc:call(Node, emqx_rule_engine, get_resource_status, [Id]), 270 | maps:put(node, Node, St) 271 | end || Node <- [node()| nodes()]], 272 | lists:flatten(io_lib:format("resource(id='~s', type='~s', config=~0p, status=~0p, description='~s')~n", [Id, Type, Config, Status, Descr])); 273 | 274 | format(#resource_type{name = Name, 275 | provider = Provider, 276 | title = #{en := Title}, 277 | description = #{en := Descr}}) -> 278 | lists:flatten(io_lib:format("resource_type(name='~s', provider='~s', title ='~s', description='~s')~n", [Name, Provider, Title, Descr])). 279 | 280 | make_rule(Opts) -> 281 | Actions = get_value(actions, Opts), 282 | may_with_opt( 283 | #{rawsql => get_value(sql, Opts), 284 | enabled => get_value(enabled, Opts), 285 | actions => parse_actions(emqx_json:decode(Actions, [return_maps])), 286 | on_action_failed => on_failed(get_value(on_action_failed, Opts)), 287 | description => get_value(descr, Opts)}, id, <<"">>, Opts). 288 | 289 | make_updated_rule(Opts) -> 290 | KeyNameParsers = [{sql, rawsql, fun(SQL) -> SQL end}, 291 | enabled, 292 | {actions, actions, fun(Actions) -> 293 | parse_actions(emqx_json:decode(Actions, [return_maps])) 294 | end}, 295 | on_action_failed, 296 | {descr, description, fun(Descr) -> Descr end}], 297 | lists:foldl(fun 298 | ({Key, Name, Parser}, ParamsAcc) -> 299 | case get_value(Key, Opts) of 300 | undefined -> ParamsAcc; 301 | Val -> ParamsAcc#{Name => Parser(Val)} 302 | end; 303 | (Key, ParamsAcc) -> 304 | case get_value(Key, Opts) of 305 | undefined -> ParamsAcc; 306 | Val -> ParamsAcc#{Key => Val} 307 | end 308 | end, #{id => get_value(id, Opts)}, KeyNameParsers). 309 | 310 | make_resource(Opts) -> 311 | Config = get_value(config, Opts), 312 | may_with_opt( 313 | #{type => get_value(type, Opts), 314 | config => ?RAISE(emqx_json:decode(Config, [return_maps]), {invalid_config, Config}), 315 | description => get_value(descr, Opts)}, id, <<"">>, Opts). 316 | 317 | printable_actions(Actions) when is_list(Actions) -> 318 | emqx_json:encode([#{id => Id, name => Name, params => Args, 319 | metrics => get_action_metrics(Id), 320 | fallbacks => printable_actions(Fallbacks)} 321 | || #action_instance{id = Id, name = Name, args = Args, fallbacks = Fallbacks} <- Actions]). 322 | 323 | may_with_opt(Params, OptName, DefaultVal, Options) when is_map(Params) -> 324 | case get_value(OptName, Options) of 325 | DefaultVal -> Params; 326 | Val -> Params#{OptName => Val} 327 | end. 328 | 329 | with_opts(Action, RawParams, OptSpecList, {CmdObject, CmdName}) -> 330 | case getopt:parse_and_check(OptSpecList, RawParams) of 331 | {ok, Params} -> 332 | Action(Params); 333 | {error, Reason} -> 334 | getopt:usage(OptSpecList, 335 | io_lib:format("emqx_ctl ~s ~s", [CmdObject, CmdName]), standard_io), 336 | emqx_ctl:print("~0p~n", [Reason]) 337 | end. 338 | 339 | parse_actions(Actions) -> 340 | ?RAISE([parse_action(Action) || Action <- Actions], 341 | {invalid_action_params, {_REASON_,_ST_}}). 342 | 343 | parse_action(Action) -> 344 | ActName = maps:get(<<"name">>, Action), 345 | #{name => ?RAISE(binary_to_existing_atom(ActName, utf8), {action_not_found, ActName}), 346 | args => maps:get(<<"params">>, Action, #{}), 347 | fallbacks => parse_actions(maps:get(<<"fallbacks">>, Action, []))}. 348 | 349 | get_actions() -> 350 | emqx_rule_registry:get_actions(). 351 | 352 | get_rule_metrics(Id) -> 353 | [maps:put(node, Node, rpc:call(Node, emqx_rule_metrics, get_rule_metrics, [Id])) 354 | || Node <- [node()| nodes()]]. 355 | 356 | get_action_metrics(Id) -> 357 | [maps:put(node, Node, rpc:call(Node, emqx_rule_metrics, get_action_metrics, [Id])) 358 | || Node <- [node()| nodes()]]. 359 | 360 | on_failed(continue) -> continue; 361 | on_failed(stop) -> stop; 362 | on_failed(OnFailed) -> error({invalid_on_failed, OnFailed}). 363 | 364 | rmlf(Str) -> 365 | re:replace(Str, "\n", "", [global]). 366 | 367 | untilde(Str) -> 368 | re:replace(Str,"~","&&",[{return,list}, global]). 369 | -------------------------------------------------------------------------------- /src/emqx_rule_engine_sup.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_engine_sup). 18 | 19 | -behaviour(supervisor). 20 | 21 | -include("rule_engine.hrl"). 22 | 23 | -export([start_link/0]). 24 | 25 | -export([init/1]). 26 | 27 | start_link() -> 28 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 29 | 30 | init([]) -> 31 | Opts = [public, named_table, set, {read_concurrency, true}], 32 | ets:new(?ACTION_INST_PARAMS_TAB, [{keypos, #action_instance_params.id}|Opts]), 33 | ets:new(?RES_PARAMS_TAB, [{keypos, #resource_params.id}|Opts]), 34 | Registry = #{id => emqx_rule_registry, 35 | start => {emqx_rule_registry, start_link, []}, 36 | restart => permanent, 37 | shutdown => 5000, 38 | type => worker, 39 | modules => [emqx_rule_registry]}, 40 | Metrics = #{id => emqx_rule_metrics, 41 | start => {emqx_rule_metrics, start_link, []}, 42 | restart => permanent, 43 | shutdown => 5000, 44 | type => worker, 45 | modules => [emqx_rule_metrics]}, 46 | {ok, {{one_for_one, 10, 10}, [Registry, Metrics]}}. 47 | 48 | -------------------------------------------------------------------------------- /src/emqx_rule_events.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_events). 18 | 19 | -include_lib("emqx/include/emqx.hrl"). 20 | -include_lib("emqx/include/logger.hrl"). 21 | 22 | -logger_header("[RuleEvents]"). 23 | 24 | -export([ load/1 25 | , unload/1 26 | , event_name/1 27 | , eventmsg_publish/1 28 | ]). 29 | 30 | -export([ on_client_connected/3 31 | , on_client_disconnected/4 32 | , on_session_subscribed/4 33 | , on_session_unsubscribed/4 34 | , on_message_publish/2 35 | , on_message_dropped/4 36 | , on_message_delivered/3 37 | , on_message_acked/3 38 | ]). 39 | 40 | -define(SUPPORTED_HOOK, 41 | [ 'client.connected' 42 | , 'client.disconnected' 43 | , 'session.subscribed' 44 | , 'session.unsubscribed' 45 | , 'message.publish' 46 | , 'message.delivered' 47 | , 'message.acked' 48 | , 'message.dropped' 49 | ]). 50 | 51 | -ifdef(TEST). 52 | -export([ reason/1 53 | , hook_fun/1 54 | , printable_maps/1 55 | ]). 56 | -endif. 57 | 58 | load(Env) -> 59 | [emqx_hooks:add(HookPoint, {?MODULE, hook_fun(HookPoint), [hook_conf(HookPoint, Env)]}) 60 | || HookPoint <- ?SUPPORTED_HOOK], 61 | ok. 62 | 63 | unload(_Env) -> 64 | [emqx_hooks:del(HookPoint, {?MODULE, hook_fun(HookPoint)}) 65 | || HookPoint <- ?SUPPORTED_HOOK], 66 | ok. 67 | 68 | %%-------------------------------------------------------------------- 69 | %% Callbacks 70 | %%-------------------------------------------------------------------- 71 | 72 | on_message_publish(Message = #message{flags = #{event := true}}, 73 | _Env) -> 74 | {ok, Message}; 75 | on_message_publish(Message = #message{flags = #{sys := true}}, 76 | #{ignore_sys_message := true}) -> 77 | {ok, Message}; 78 | on_message_publish(Message = #message{topic = Topic}, _Env) -> 79 | case emqx_rule_registry:get_rules_for(Topic) of 80 | [] -> ok; 81 | Rules -> emqx_rule_runtime:apply_rules(Rules, eventmsg_publish(Message)) 82 | end, 83 | {ok, Message}. 84 | 85 | on_client_connected(ClientInfo, ConnInfo, Env) -> 86 | may_publish_and_apply('client.connected', 87 | fun() -> eventmsg_connected(ClientInfo, ConnInfo) end, Env). 88 | 89 | on_client_disconnected(ClientInfo, Reason, ConnInfo, Env) -> 90 | may_publish_and_apply('client.disconnected', 91 | fun() -> eventmsg_disconnected(ClientInfo, ConnInfo, Reason) end, Env). 92 | 93 | on_session_subscribed(ClientInfo, Topic, SubOpts, Env) -> 94 | may_publish_and_apply('session.subscribed', 95 | fun() -> eventmsg_sub_or_unsub('session.subscribed', ClientInfo, Topic, SubOpts) end, Env). 96 | 97 | on_session_unsubscribed(ClientInfo, Topic, SubOpts, Env) -> 98 | may_publish_and_apply('session.unsubscribed', 99 | fun() -> eventmsg_sub_or_unsub('session.unsubscribed', ClientInfo, Topic, SubOpts) end, Env). 100 | 101 | on_message_dropped(Message = #message{flags = #{sys := true}}, 102 | _, _, #{ignore_sys_message := true}) -> 103 | {ok, Message}; 104 | on_message_dropped(Message, _, Reason, Env) -> 105 | may_publish_and_apply('message.dropped', 106 | fun() -> eventmsg_dropped(Message, Reason) end, Env), 107 | {ok, Message}. 108 | 109 | on_message_delivered(_ClientInfo, Message = #message{flags = #{sys := true}}, 110 | #{ignore_sys_message := true}) -> 111 | {ok, Message}; 112 | on_message_delivered(ClientInfo, Message, Env) -> 113 | may_publish_and_apply('message.delivered', 114 | fun() -> eventmsg_delivered(ClientInfo, Message) end, Env), 115 | {ok, Message}. 116 | 117 | on_message_acked(_ClientInfo, Message = #message{flags = #{sys := true}}, 118 | #{ignore_sys_message := true}) -> 119 | {ok, Message}; 120 | on_message_acked(ClientInfo, Message, Env) -> 121 | may_publish_and_apply('message.acked', 122 | fun() -> eventmsg_acked(ClientInfo, Message) end, Env), 123 | {ok, Message}. 124 | 125 | %%-------------------------------------------------------------------- 126 | %% Event Messages 127 | %%-------------------------------------------------------------------- 128 | 129 | eventmsg_publish(Message = #message{id = Id, from = ClientId, qos = QoS, flags = Flags, topic = Topic, headers = Headers, payload = Payload, timestamp = Timestamp}) -> 130 | with_basic_columns('message.publish', 131 | #{id => emqx_guid:to_hexstr(Id), 132 | clientid => ClientId, 133 | username => emqx_message:get_header(username, Message, undefined), 134 | payload => Payload, 135 | peerhost => ntoa(emqx_message:get_header(peerhost, Message, undefined)), 136 | topic => Topic, 137 | qos => QoS, 138 | flags => Flags, 139 | pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), 140 | %% the column 'headers' will be removed in the next major release 141 | headers => printable_maps(Headers), 142 | publish_received_at => Timestamp 143 | }). 144 | 145 | eventmsg_connected(_ClientInfo = #{ 146 | clientid := ClientId, 147 | username := Username, 148 | is_bridge := IsBridge, 149 | mountpoint := Mountpoint 150 | }, 151 | _ConnInfo = #{ 152 | peername := PeerName, 153 | sockname := SockName, 154 | clean_start := CleanStart, 155 | proto_name := ProtoName, 156 | proto_ver := ProtoVer, 157 | keepalive := Keepalive, 158 | connected_at := ConnectedAt, 159 | conn_props := ConnProps, 160 | receive_maximum := RcvMax, 161 | expiry_interval := ExpiryInterval 162 | }) -> 163 | with_basic_columns('client.connected', 164 | #{clientid => ClientId, 165 | username => Username, 166 | mountpoint => Mountpoint, 167 | peername => ntoa(PeerName), 168 | sockname => ntoa(SockName), 169 | proto_name => ProtoName, 170 | proto_ver => ProtoVer, 171 | keepalive => Keepalive, 172 | clean_start => CleanStart, 173 | receive_maximum => RcvMax, 174 | expiry_interval => ExpiryInterval, 175 | is_bridge => IsBridge, 176 | conn_props => printable_maps(ConnProps), 177 | connected_at => ConnectedAt 178 | }). 179 | 180 | eventmsg_disconnected(_ClientInfo = #{ 181 | clientid := ClientId, 182 | username := Username 183 | }, 184 | ConnInfo = #{ 185 | peername := PeerName, 186 | sockname := SockName, 187 | disconnected_at := DisconnectedAt 188 | }, Reason) -> 189 | with_basic_columns('client.disconnected', 190 | #{reason => reason(Reason), 191 | clientid => ClientId, 192 | username => Username, 193 | peername => ntoa(PeerName), 194 | sockname => ntoa(SockName), 195 | disconn_props => printable_maps(maps:get(disconn_props, ConnInfo, #{})), 196 | disconnected_at => DisconnectedAt 197 | }). 198 | 199 | eventmsg_sub_or_unsub(Event, _ClientInfo = #{ 200 | clientid := ClientId, 201 | username := Username, 202 | peerhost := PeerHost 203 | }, Topic, SubOpts = #{qos := QoS}) -> 204 | PropKey = sub_unsub_prop_key(Event), 205 | with_basic_columns(Event, 206 | #{clientid => ClientId, 207 | username => Username, 208 | peerhost => ntoa(PeerHost), 209 | PropKey => printable_maps(maps:get(PropKey, SubOpts, #{})), 210 | topic => Topic, 211 | qos => QoS 212 | }). 213 | 214 | eventmsg_dropped(Message = #message{id = Id, from = ClientId, qos = QoS, flags = Flags, topic = Topic, headers = Headers, payload = Payload, timestamp = Timestamp}, Reason) -> 215 | with_basic_columns('message.dropped', 216 | #{id => emqx_guid:to_hexstr(Id), 217 | reason => Reason, 218 | clientid => ClientId, 219 | username => emqx_message:get_header(username, Message, undefined), 220 | payload => Payload, 221 | peerhost => ntoa(emqx_message:get_header(peerhost, Message, undefined)), 222 | topic => Topic, 223 | qos => QoS, 224 | flags => Flags, 225 | %% the column 'headers' will be removed in the next major release 226 | headers => printable_maps(Headers), 227 | pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), 228 | publish_received_at => Timestamp 229 | }). 230 | 231 | eventmsg_delivered(_ClientInfo = #{ 232 | peerhost := PeerHost, 233 | clientid := ReceiverCId, 234 | username := ReceiverUsername 235 | }, Message = #message{id = Id, from = ClientId, qos = QoS, flags = Flags, topic = Topic, headers = Headers, payload = Payload, timestamp = Timestamp}) -> 236 | with_basic_columns('message.delivered', 237 | #{id => emqx_guid:to_hexstr(Id), 238 | from_clientid => ClientId, 239 | from_username => emqx_message:get_header(username, Message, undefined), 240 | clientid => ReceiverCId, 241 | username => ReceiverUsername, 242 | payload => Payload, 243 | peerhost => ntoa(PeerHost), 244 | topic => Topic, 245 | qos => QoS, 246 | flags => Flags, 247 | %% the column 'headers' will be removed in the next major release 248 | headers => printable_maps(Headers), 249 | pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), 250 | publish_received_at => Timestamp 251 | }). 252 | 253 | eventmsg_acked(_ClientInfo = #{ 254 | peerhost := PeerHost, 255 | clientid := ReceiverCId, 256 | username := ReceiverUsername 257 | }, Message = #message{id = Id, from = ClientId, qos = QoS, flags = Flags, topic = Topic, headers = Headers, payload = Payload, timestamp = Timestamp}) -> 258 | with_basic_columns('message.acked', 259 | #{id => emqx_guid:to_hexstr(Id), 260 | from_clientid => ClientId, 261 | from_username => emqx_message:get_header(username, Message, undefined), 262 | clientid => ReceiverCId, 263 | username => ReceiverUsername, 264 | payload => Payload, 265 | peerhost => ntoa(PeerHost), 266 | topic => Topic, 267 | qos => QoS, 268 | flags => Flags, 269 | %% the column 'headers' will be removed in the next major release 270 | headers => printable_maps(Headers), 271 | pub_props => printable_maps(emqx_message:get_header(properties, Message, #{})), 272 | puback_props => printable_maps(emqx_message:get_header(puback_props, Message, #{})), 273 | publish_received_at => Timestamp 274 | }). 275 | 276 | sub_unsub_prop_key('session.subscribed') -> sub_props; 277 | sub_unsub_prop_key('session.unsubscribed') -> unsub_props. 278 | 279 | with_basic_columns(EventName, Data) when is_map(Data) -> 280 | Data#{event => EventName, 281 | timestamp => erlang:system_time(millisecond), 282 | node => node() 283 | }. 284 | 285 | %%-------------------------------------------------------------------- 286 | %% Events publishing and rules applying 287 | %%-------------------------------------------------------------------- 288 | 289 | may_publish_and_apply(EventName, GenEventMsg, #{enabled := true, qos := QoS}) -> 290 | EventTopic = event_topic(EventName), 291 | EventMsg = GenEventMsg(), 292 | case emqx_json:safe_encode(EventMsg) of 293 | {ok, Payload} -> 294 | emqx_broker:safe_publish(make_msg(QoS, EventTopic, Payload)); 295 | {error, _Reason} -> 296 | ?LOG(error, "Failed to encode event msg for ~p, msg: ~p", [EventName, EventMsg]) 297 | end, 298 | emqx_rule_runtime:apply_rules(emqx_rule_registry:get_rules_for(EventTopic), EventMsg); 299 | may_publish_and_apply(EventName, GenEventMsg, _Env) -> 300 | EventTopic = event_topic(EventName), 301 | case emqx_rule_registry:get_rules_for(EventTopic) of 302 | [] -> ok; 303 | Rules -> emqx_rule_runtime:apply_rules(Rules, GenEventMsg()) 304 | end. 305 | 306 | make_msg(QoS, Topic, Payload) -> 307 | emqx_message:set_flags(#{sys => true, event => true}, 308 | emqx_message:make(emqx_events, QoS, Topic, iolist_to_binary(Payload))). 309 | 310 | %%-------------------------------------------------------------------- 311 | %% Helper functions 312 | %%-------------------------------------------------------------------- 313 | 314 | hook_conf(HookPoint, Env) -> 315 | Events = proplists:get_value(events, Env, []), 316 | IgnoreSys = proplists:get_value(ignore_sys_message, Env, true), 317 | case lists:keyfind(HookPoint, 1, Events) of 318 | {_, on, QoS} -> #{enabled => true, qos => QoS, ignore_sys_message => IgnoreSys}; 319 | _ -> #{enabled => false, qos => 1, ignore_sys_message => IgnoreSys} 320 | end. 321 | 322 | hook_fun(Event) -> 323 | case string:split(atom_to_list(Event), ".") of 324 | [Prefix, Name] -> 325 | list_to_atom(lists:append(["on_", Prefix, "_", Name])); 326 | [_] -> 327 | error(invalid_event, Event) 328 | end. 329 | 330 | reason(Reason) when is_atom(Reason) -> Reason; 331 | reason({shutdown, Reason}) when is_atom(Reason) -> Reason; 332 | reason({Error, _}) when is_atom(Error) -> Error; 333 | reason(_) -> internal_error. 334 | 335 | ntoa(undefined) -> undefined; 336 | ntoa({IpAddr, Port}) -> 337 | iolist_to_binary([inet:ntoa(IpAddr),":",integer_to_list(Port)]); 338 | ntoa(IpAddr) -> 339 | iolist_to_binary(inet:ntoa(IpAddr)). 340 | 341 | event_name(<<"$events/client_connected", _/binary>>) -> 'client.connected'; 342 | event_name(<<"$events/client_disconnected", _/binary>>) -> 'client.disconnected'; 343 | event_name(<<"$events/session_subscribed", _/binary>>) -> 'session.subscribed'; 344 | event_name(<<"$events/session_unsubscribed", _/binary>>) -> 'session.unsubscribed'; 345 | event_name(<<"$events/message_delivered", _/binary>>) -> 'message.delivered'; 346 | event_name(<<"$events/message_acked", _/binary>>) -> 'message.acked'; 347 | event_name(<<"$events/message_dropped", _/binary>>) -> 'message.dropped'. 348 | 349 | event_topic('client.connected') -> <<"$events/client_connected">>; 350 | event_topic('client.disconnected') -> <<"$events/client_disconnected">>; 351 | event_topic('session.subscribed') -> <<"$events/session_subscribed">>; 352 | event_topic('session.unsubscribed') -> <<"$events/session_unsubscribed">>; 353 | event_topic('message.delivered') -> <<"$events/message_delivered">>; 354 | event_topic('message.acked') -> <<"$events/message_acked">>; 355 | event_topic('message.dropped') -> <<"$events/message_dropped">>. 356 | 357 | printable_maps(undefined) -> #{}; 358 | printable_maps(Headers) -> 359 | maps:fold( 360 | fun (K, V0, AccIn) when K =:= peerhost; K =:= peername; K =:= sockname -> 361 | AccIn#{K => ntoa(V0)}; 362 | ('User-Property', V0, AccIn) when is_list(V0) -> 363 | AccIn#{ 364 | 'User-Property' => maps:from_list(V0), 365 | 'User-Property-Pairs' => [#{ 366 | key => Key, 367 | value => Value 368 | } || {Key, Value} <- V0] 369 | }; 370 | (K, V0, AccIn) -> AccIn#{K => V0} 371 | end, #{}, Headers). 372 | -------------------------------------------------------------------------------- /src/emqx_rule_id.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_id). 18 | 19 | -export([gen/0, gen/1]). 20 | 21 | -define(SHORT, 8). 22 | 23 | %%-------------------------------------------------------------------- 24 | %% APIs 25 | %%-------------------------------------------------------------------- 26 | -spec(gen() -> list()). 27 | gen() -> 28 | gen(?SHORT). 29 | 30 | -spec(gen(integer()) -> list()). 31 | gen(Len) -> 32 | BitLen = Len * 4, 33 | <> = crypto:strong_rand_bytes(Len div 2), 34 | int_to_hex(R, Len). 35 | 36 | %%------------------------------------------------------------------------------ 37 | %% Internal Functions 38 | %%------------------------------------------------------------------------------ 39 | 40 | int_to_hex(I, N) when is_integer(I), I >= 0 -> 41 | int_to_hex([], I, 1, N). 42 | 43 | int_to_hex(L, I, Count, N) 44 | when I < 16 -> 45 | pad([int_to_hex(I) | L], N - Count); 46 | int_to_hex(L, I, Count, N) -> 47 | int_to_hex([int_to_hex(I rem 16) | L], I div 16, Count + 1, N). 48 | 49 | int_to_hex(I) when 0 =< I, I =< 9 -> 50 | I + $0; 51 | int_to_hex(I) when 10 =< I, I =< 15 -> 52 | (I - 10) + $a. 53 | 54 | pad(L, 0) -> 55 | L; 56 | pad(L, Count) -> 57 | pad([$0 | L], Count - 1). 58 | -------------------------------------------------------------------------------- /src/emqx_rule_maps.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_maps). 18 | 19 | -export([ nested_get/2 20 | , nested_get/3 21 | , nested_put/3 22 | , range_gen/2 23 | , range_get/3 24 | , atom_key_map/1 25 | , unsafe_atom_key_map/1 26 | ]). 27 | 28 | nested_get(Key, Data) -> 29 | nested_get(Key, Data, undefined). 30 | 31 | nested_get({var, Key}, Data, Default) when is_map(Data) orelse is_list(Data) -> 32 | general_map_get({key, Key}, Data, Data, Default); 33 | nested_get({path, Path}, Data, Default) when is_map(Data) orelse is_list(Data), 34 | is_list(Path) -> 35 | do_nested_get(Path, Data, Data, Default); 36 | nested_get(Key, JsonStr, Default) when is_binary(JsonStr) -> 37 | try emqx_json:decode(JsonStr, [return_maps]) of 38 | Json -> nested_get(Key, Json, Default) 39 | catch 40 | _:_ -> Default 41 | end; 42 | nested_get(_Key, _InvalidData, Default) -> 43 | Default. 44 | 45 | do_nested_get([Key|More], Data, OrgData, Default) -> 46 | case general_map_get(Key, Data, OrgData, undefined) of 47 | undefined -> Default; 48 | Val -> do_nested_get(More, Val, OrgData, Default) 49 | end; 50 | do_nested_get([], Val, _OrgData, _Default) -> 51 | Val. 52 | 53 | nested_put(Key, Val, Data) when not is_map(Data), 54 | not is_list(Data) -> 55 | nested_put(Key, Val, #{}); 56 | nested_put(_, undefined, Map) -> 57 | Map; 58 | nested_put({var, Key}, Val, Map) -> 59 | general_map_put({key, Key}, Val, Map, Map); 60 | nested_put({path, Path}, Val, Map) when is_list(Path) -> 61 | do_nested_put(Path, Val, Map, Map). 62 | 63 | do_nested_put([Key|More], Val, Map, OrgData) -> 64 | SubMap = general_map_get(Key, Map, OrgData, undefined), 65 | general_map_put(Key, do_nested_put(More, Val, SubMap, OrgData), Map, OrgData); 66 | do_nested_put([], Val, _Map, _OrgData) -> 67 | Val. 68 | 69 | general_map_get(Key, Map, OrgData, Default) -> 70 | general_find(Key, Map, OrgData, 71 | fun 72 | ({equivalent, {_EquiKey, Val}}) -> Val; 73 | ({found, {_Key, Val}}) -> Val; 74 | (not_found) -> Default 75 | end). 76 | 77 | general_map_put(_Key, undefined, Map, _OrgData) -> 78 | Map; 79 | general_map_put(Key, Val, Map, OrgData) -> 80 | general_find(Key, Map, OrgData, 81 | fun 82 | ({equivalent, {EquiKey, _Val}}) -> do_put(EquiKey, Val, Map, OrgData); 83 | (_) -> do_put(Key, Val, Map, OrgData) 84 | end). 85 | 86 | general_find(Key, JsonStr, _OrgData, Handler) when is_binary(JsonStr) -> 87 | try emqx_json:decode(JsonStr, [return_maps]) of 88 | Json -> general_find(Key, Json, _OrgData, Handler) 89 | catch 90 | _:_ -> Handler(not_found) 91 | end; 92 | general_find({key, Key}, Map, _OrgData, Handler) when is_map(Map) -> 93 | case maps:find(Key, Map) of 94 | {ok, Val} -> Handler({found, {{key, Key}, Val}}); 95 | error when is_atom(Key) -> 96 | %% the map may have an equivalent binary-form key 97 | BinKey = emqx_rule_utils:bin(Key), 98 | case maps:find(BinKey, Map) of 99 | {ok, Val} -> Handler({equivalent, {{key, BinKey}, Val}}); 100 | error -> Handler(not_found) 101 | end; 102 | error when is_binary(Key) -> 103 | try %% the map may have an equivalent atom-form key 104 | AtomKey = list_to_existing_atom(binary_to_list(Key)), 105 | case maps:find(AtomKey, Map) of 106 | {ok, Val} -> Handler({equivalent, {{key, AtomKey}, Val}}); 107 | error -> Handler(not_found) 108 | end 109 | catch error:badarg -> 110 | Handler(not_found) 111 | end; 112 | error -> 113 | Handler(not_found) 114 | end; 115 | general_find({key, _Key}, _Map, _OrgData, Handler) -> 116 | Handler(not_found); 117 | general_find({index, {const, Index0}} = IndexP, List, _OrgData, Handler) when is_list(List) -> 118 | handle_getnth(Index0, List, IndexP, Handler); 119 | general_find({index, Index0} = IndexP, List, OrgData, Handler) when is_list(List) -> 120 | Index1 = nested_get(Index0, OrgData), 121 | handle_getnth(Index1, List, IndexP, Handler); 122 | general_find({index, _}, List, _OrgData, Handler) when not is_list(List) -> 123 | Handler(not_found). 124 | 125 | do_put({key, Key}, Val, Map, _OrgData) when is_map(Map) -> 126 | maps:put(Key, Val, Map); 127 | do_put({key, Key}, Val, Data, _OrgData) when not is_map(Data) -> 128 | #{Key => Val}; 129 | do_put({index, {const, Index}}, Val, List, _OrgData) -> 130 | setnth(Index, List, Val); 131 | do_put({index, Index0}, Val, List, OrgData) -> 132 | Index1 = nested_get(Index0, OrgData), 133 | setnth(Index1, List, Val). 134 | 135 | setnth(_, Data, Val) when not is_list(Data) -> 136 | setnth(head, [], Val); 137 | setnth(head, List, Val) when is_list(List) -> [Val | List]; 138 | setnth(head, _List, Val) -> [Val]; 139 | setnth(tail, List, Val) when is_list(List) -> List ++ [Val]; 140 | setnth(tail, _List, Val) -> [Val]; 141 | setnth(I, List, _Val) when not is_integer(I) -> List; 142 | setnth(0, List, _Val) -> List; 143 | setnth(I, List, _Val) when is_integer(I), I > 0 -> 144 | do_setnth(I, List, _Val); 145 | setnth(I, List, _Val) when is_integer(I), I < 0 -> 146 | lists:reverse(do_setnth(-I, lists:reverse(List), _Val)). 147 | 148 | do_setnth(1, [_|Rest], Val) -> [Val|Rest]; 149 | do_setnth(I, [E|Rest], Val) -> [E|setnth(I-1, Rest, Val)]; 150 | do_setnth(_, [], _Val) -> []. 151 | 152 | getnth(0, _) -> 153 | {error, not_found}; 154 | getnth(I, L) when I > 0 -> 155 | do_getnth(I, L); 156 | getnth(I, L) when I < 0 -> 157 | do_getnth(-I, lists:reverse(L)). 158 | 159 | do_getnth(I, L) -> 160 | try {ok, lists:nth(I, L)} 161 | catch error:_ -> {error, not_found} 162 | end. 163 | 164 | handle_getnth(Index, List, IndexPattern, Handler) -> 165 | case getnth(Index, List) of 166 | {ok, Val} -> 167 | Handler({found, {IndexPattern, Val}}); 168 | {error, _} -> 169 | Handler(not_found) 170 | end. 171 | 172 | range_gen(Begin, End) -> 173 | lists:seq(Begin, End). 174 | 175 | range_get(Begin, End, List) when is_list(List) -> 176 | do_range_get(Begin, End, List); 177 | range_get(_, _, _NotList) -> 178 | error({range_get, non_list_data}). 179 | 180 | do_range_get(Begin, End, List) -> 181 | TotalLen = length(List), 182 | BeginIndex = index(Begin, TotalLen), 183 | EndIndex = index(End, TotalLen), 184 | lists:sublist(List, BeginIndex, (EndIndex - BeginIndex + 1)). 185 | 186 | index(0, _) -> error({invalid_index, 0}); 187 | index(Index, _) when Index > 0 -> Index; 188 | index(Index, Len) when Index < 0 -> 189 | Len + Index + 1. 190 | 191 | %%%------------------------------------------------------------------- 192 | %%% atom key map 193 | %%%------------------------------------------------------------------- 194 | atom_key_map(BinKeyMap) when is_map(BinKeyMap) -> 195 | maps:fold( 196 | fun(K, V, Acc) when is_binary(K) -> 197 | Acc#{binary_to_existing_atom(K, utf8) => atom_key_map(V)}; 198 | (K, V, Acc) when is_list(K) -> 199 | Acc#{list_to_existing_atom(K) => atom_key_map(V)}; 200 | (K, V, Acc) when is_atom(K) -> 201 | Acc#{K => atom_key_map(V)} 202 | end, #{}, BinKeyMap); 203 | atom_key_map(ListV) when is_list(ListV) -> 204 | [atom_key_map(V) || V <- ListV]; 205 | atom_key_map(Val) -> Val. 206 | 207 | unsafe_atom_key_map(BinKeyMap) when is_map(BinKeyMap) -> 208 | maps:fold( 209 | fun(K, V, Acc) when is_binary(K) -> 210 | Acc#{binary_to_atom(K, utf8) => unsafe_atom_key_map(V)}; 211 | (K, V, Acc) when is_list(K) -> 212 | Acc#{list_to_atom(K) => unsafe_atom_key_map(V)}; 213 | (K, V, Acc) when is_atom(K) -> 214 | Acc#{K => unsafe_atom_key_map(V)} 215 | end, #{}, BinKeyMap); 216 | unsafe_atom_key_map(ListV) when is_list(ListV) -> 217 | [unsafe_atom_key_map(V) || V <- ListV]; 218 | unsafe_atom_key_map(Val) -> Val. -------------------------------------------------------------------------------- /src/emqx_rule_metrics.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_metrics). 18 | 19 | -behaviour(gen_server). 20 | 21 | -include("rule_engine.hrl"). 22 | 23 | %% API functions 24 | -export([ start_link/0 25 | , stop/0 26 | ]). 27 | 28 | -export([ inc/2 29 | , inc/3 30 | , get/2 31 | , get_overall/1 32 | , get_rule_speed/1 33 | , get_overall_rule_speed/0 34 | , create/1 35 | , clear/1 36 | , overall_metrics/0 37 | ]). 38 | 39 | -export([ get_rule_metrics/1 40 | , get_action_metrics/1 41 | ]). 42 | 43 | %% gen_server callbacks 44 | -export([ init/1 45 | , handle_call/3 46 | , handle_info/2 47 | , handle_cast/2 48 | , terminate/2 49 | ]). 50 | 51 | -ifndef(TEST). 52 | -define(SECS_5M, 300). 53 | -define(SAMPLING, 10). 54 | -else. 55 | %% Use 5 secs average speed instead of 5 mins in case of testing 56 | -define(SECS_5M, 5). 57 | -define(SAMPLING, 1). 58 | -endif. 59 | 60 | -define(CRefID(ID), {?MODULE, ID}). 61 | -define(SAMPCOUNT_5M, (?SECS_5M div ?SAMPLING)). 62 | 63 | -record(rule_speed, { 64 | max = 0 :: number(), 65 | current = 0 :: number(), 66 | last5m = 0 :: number(), 67 | %% metadata for calculating the avg speed 68 | tick = 1 :: number(), 69 | last_v = 0 :: number(), 70 | %% metadata for calculating the 5min avg speed 71 | last5m_acc = 0 :: number(), 72 | last5m_smpl = [] :: list() 73 | }). 74 | 75 | -record(state, { 76 | metric_ids = sets:new(), 77 | rule_speeds :: #{rule_id() => #rule_speed{}}, 78 | overall_rule_speed :: #rule_speed{} 79 | }). 80 | 81 | %%------------------------------------------------------------------------------ 82 | %% APIs 83 | %%------------------------------------------------------------------------------ 84 | -spec(create(rule_id()) -> Ref :: reference()). 85 | create(<<"rule:", _/binary>> = Id) -> 86 | gen_server:call(?MODULE, {create_rule_metrics, Id}); 87 | create(Id) -> 88 | gen_server:call(?MODULE, {create_metrics, Id}). 89 | 90 | -spec(clear(rule_id()) -> ok). 91 | clear(<<"rule:", _/binary>> = Id) -> 92 | gen_server:call(?MODULE, {delete_rule_metrics, Id}); 93 | clear(Id) -> 94 | gen_server:call(?MODULE, {delete_metrics, Id}). 95 | 96 | -spec(get(rule_id(), atom()) -> number()). 97 | get(Id, Metric) -> 98 | case couters_ref(Id) of 99 | not_found -> 0; 100 | Ref -> counters:get(Ref, metrics_idx(Metric)) 101 | end. 102 | 103 | -spec(get_overall(atom()) -> number()). 104 | get_overall(Metric) -> 105 | emqx_metrics:val(Metric). 106 | 107 | -spec(get_rule_speed(atom()) -> map()). 108 | get_rule_speed(Id) -> 109 | gen_server:call(?MODULE, {get_rule_speed, Id}). 110 | 111 | -spec(get_overall_rule_speed() -> map()). 112 | get_overall_rule_speed() -> 113 | gen_server:call(?MODULE, get_overall_rule_speed). 114 | 115 | -spec(get_rule_metrics(rule_id()) -> map()). 116 | get_rule_metrics(Id) -> 117 | #{max := Max, current := Current, last5m := Last5M} = get_rule_speed(Id), 118 | #{matched => get(Id, 'rules.matched'), 119 | speed => Current, 120 | speed_max => Max, 121 | speed_last5m => Last5M 122 | }. 123 | 124 | -spec(get_action_metrics(action_instance_id()) -> map()). 125 | get_action_metrics(Id) -> 126 | #{success => get(Id, 'actions.success'), 127 | failed => get(Id, 'actions.failure') 128 | }. 129 | 130 | -spec(inc(rule_id(), atom()) -> ok). 131 | inc(Id, Metric) -> 132 | inc(Id, Metric, 1). 133 | inc(Id, Metric, Val) -> 134 | case couters_ref(Id) of 135 | not_found -> 136 | counters:add(create(Id), metrics_idx(Metric), Val); 137 | Ref -> 138 | counters:add(Ref, metrics_idx(Metric), Val) 139 | end, 140 | inc_overall(Metric, Val). 141 | 142 | -spec(inc_overall(rule_id(), atom()) -> ok). 143 | inc_overall(Metric, Val) -> 144 | emqx_metrics:inc(Metric, Val). 145 | 146 | start_link() -> 147 | gen_server:start_link({local, ?MODULE}, ?MODULE, [], []). 148 | 149 | init([]) -> 150 | erlang:process_flag(trap_exit, true), 151 | %% the overall counters 152 | [ok = emqx_metrics:ensure(Metric)|| Metric <- overall_metrics()], 153 | %% the speed metrics 154 | erlang:send_after(timer:seconds(?SAMPLING), self(), ticking), 155 | {ok, #state{overall_rule_speed = #rule_speed{}}}. 156 | 157 | handle_call({get_rule_speed, _Id}, _From, State = #state{rule_speeds = undefined}) -> 158 | {reply, format_rule_speed(#rule_speed{}), State}; 159 | handle_call({get_rule_speed, Id}, _From, State = #state{rule_speeds = RuleSpeeds}) -> 160 | {reply, case maps:get(Id, RuleSpeeds, undefined) of 161 | undefined -> format_rule_speed(#rule_speed{}); 162 | Speed -> format_rule_speed(Speed) 163 | end, State}; 164 | 165 | handle_call(get_overall_rule_speed, _From, State = #state{overall_rule_speed = RuleSpeed}) -> 166 | {reply, format_rule_speed(RuleSpeed), State}; 167 | 168 | handle_call({create_metrics, Id}, _From, State = #state{metric_ids = MIDs}) -> 169 | {reply, create_counters(Id), State#state{metric_ids = sets:add_element(Id, MIDs)}}; 170 | 171 | handle_call({create_rule_metrics, Id}, _From, 172 | State = #state{metric_ids = MIDs, rule_speeds = RuleSpeeds}) -> 173 | {reply, create_counters(Id), 174 | State#state{metric_ids = sets:add_element(Id, MIDs), 175 | rule_speeds = case RuleSpeeds of 176 | undefined -> #{Id => #rule_speed{}}; 177 | _ -> RuleSpeeds#{Id => #rule_speed{}} 178 | end}}; 179 | 180 | handle_call({delete_metrics, Id}, _From, 181 | State = #state{metric_ids = MIDs, rule_speeds = undefined}) -> 182 | {reply, delete_counters(Id), State#state{metric_ids = sets:del_element(Id, MIDs)}}; 183 | 184 | handle_call({delete_rule_metrics, Id}, _From, 185 | State = #state{metric_ids = MIDs, rule_speeds = RuleSpeeds}) -> 186 | {reply, delete_counters(Id), 187 | State#state{metric_ids = sets:del_element(Id, MIDs), 188 | rule_speeds = case RuleSpeeds of 189 | undefined -> undefined; 190 | _ -> maps:remove(Id, RuleSpeeds) 191 | end}}; 192 | 193 | handle_call(_Request, _From, State) -> 194 | {reply, ok, State}. 195 | 196 | handle_cast(_Msg, State) -> 197 | {noreply, State}. 198 | 199 | handle_info(ticking, State = #state{rule_speeds = undefined}) -> 200 | async_refresh_resource_status(), 201 | erlang:send_after(timer:seconds(?SAMPLING), self(), ticking), 202 | {noreply, State}; 203 | 204 | handle_info(ticking, State = #state{rule_speeds = RuleSpeeds0, 205 | overall_rule_speed = OverallRuleSpeed0}) -> 206 | RuleSpeeds = maps:map( 207 | fun(Id, RuleSpeed) -> 208 | calculate_speed(get(Id, 'rules.matched'), RuleSpeed) 209 | end, RuleSpeeds0), 210 | OverallRuleSpeed = calculate_speed(get_overall('rules.matched'), OverallRuleSpeed0), 211 | async_refresh_resource_status(), 212 | erlang:send_after(timer:seconds(?SAMPLING), self(), ticking), 213 | {noreply, State#state{rule_speeds = RuleSpeeds, 214 | overall_rule_speed = OverallRuleSpeed}}; 215 | 216 | handle_info(_Info, State) -> 217 | {noreply, State}. 218 | 219 | terminate(_Reason, #state{metric_ids = MIDs}) -> 220 | [delete_counters(Id) || Id <- sets:to_list(MIDs)], 221 | persistent_term:erase(?MODULE). 222 | 223 | stop() -> 224 | gen_server:stop(?MODULE). 225 | 226 | %%------------------------------------------------------------------------------ 227 | %% Internal Functions 228 | %%------------------------------------------------------------------------------ 229 | 230 | async_refresh_resource_status() -> 231 | spawn(emqx_rule_engine, refresh_resource_status, []). 232 | 233 | create_counters(Id) -> 234 | CRef = counters:new(max_counters_size(), [write_concurrency]), 235 | ok = persistent_term:put(?CRefID(Id), CRef), 236 | CRef. 237 | 238 | delete_counters(Id) -> 239 | persistent_term:erase(?CRefID(Id)), 240 | ok. 241 | 242 | couters_ref(Id) -> 243 | try persistent_term:get(?CRefID(Id)) 244 | catch 245 | error:badarg -> not_found 246 | end. 247 | 248 | calculate_speed(_CurrVal, undefined) -> 249 | undefined; 250 | calculate_speed(CurrVal, #rule_speed{max = MaxSpeed0, last_v = LastVal, 251 | tick = Tick, last5m_acc = AccSpeed5Min0, 252 | last5m_smpl = Last5MinSamples0}) -> 253 | %% calculate the current speed based on the last value of the counter 254 | CurrSpeed = (CurrVal - LastVal) / ?SAMPLING, 255 | 256 | %% calculate the max speed since the emqx startup 257 | MaxSpeed = 258 | if MaxSpeed0 >= CurrSpeed -> MaxSpeed0; 259 | true -> CurrSpeed 260 | end, 261 | 262 | %% calculate the average speed in last 5 mins 263 | {Last5MinSamples, Acc5Min, Last5Min} = 264 | if Tick =< ?SAMPCOUNT_5M -> 265 | Acc = AccSpeed5Min0 + CurrSpeed, 266 | {lists:reverse([CurrSpeed | lists:reverse(Last5MinSamples0)]), 267 | Acc, Acc / Tick}; 268 | true -> 269 | [FirstSpeed | Speeds] = Last5MinSamples0, 270 | Acc = AccSpeed5Min0 + CurrSpeed - FirstSpeed, 271 | {lists:reverse([CurrSpeed | lists:reverse(Speeds)]), 272 | Acc, Acc / ?SAMPCOUNT_5M} 273 | end, 274 | 275 | #rule_speed{max = MaxSpeed, current = CurrSpeed, last5m = Last5Min, 276 | last_v = CurrVal, last5m_acc = Acc5Min, 277 | last5m_smpl = Last5MinSamples, tick = Tick + 1}. 278 | 279 | format_rule_speed(#rule_speed{max = Max, current = Current, last5m = Last5Min}) -> 280 | #{max => Max, current => precision(Current, 2), last5m => precision(Last5Min, 2)}. 281 | 282 | precision(Float, N) -> 283 | Base = math:pow(10, N), 284 | round(Float * Base) / Base. 285 | 286 | %%------------------------------------------------------------------------------ 287 | %% Metrics Definitions 288 | %%------------------------------------------------------------------------------ 289 | 290 | max_counters_size() -> 4. 291 | 292 | metrics_idx('rules.matched') -> 1; 293 | metrics_idx('actions.success') -> 2; 294 | metrics_idx('actions.failure') -> 3; 295 | metrics_idx(_) -> 4. 296 | 297 | overall_metrics() -> 298 | ['rules.matched', 'actions.success', 'actions.failure']. 299 | -------------------------------------------------------------------------------- /src/emqx_rule_registry.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_registry). 18 | 19 | -behaviour(gen_server). 20 | 21 | -include("rule_engine.hrl"). 22 | -include("rule_events.hrl"). 23 | -include_lib("emqx/include/logger.hrl"). 24 | 25 | -export([start_link/0]). 26 | 27 | %% Rule Management 28 | -export([ get_rules/0 29 | , get_rules_for/1 30 | , get_rule/1 31 | , add_rule/1 32 | , add_rules/1 33 | , remove_rule/1 34 | , remove_rules/1 35 | ]). 36 | 37 | %% Action Management 38 | -export([ add_action/1 39 | , add_actions/1 40 | , get_actions/0 41 | , find_action/1 42 | , remove_action/1 43 | , remove_actions/1 44 | , remove_actions_of/1 45 | , add_action_instance_params/1 46 | , get_action_instance_params/1 47 | , remove_action_instance_params/1 48 | ]). 49 | 50 | %% Resource Management 51 | -export([ get_resources/0 52 | , add_resource/1 53 | , add_resource_params/1 54 | , find_resource/1 55 | , find_resource_params/1 56 | , get_resources_by_type/1 57 | , remove_resource/1 58 | , remove_resource_params/1 59 | ]). 60 | 61 | %% Resource Types 62 | -export([ get_resource_types/0 63 | , find_resource_type/1 64 | , register_resource_types/1 65 | , unregister_resource_types_of/1 66 | ]). 67 | 68 | %% for debug purposes 69 | -export([dump/0]). 70 | 71 | %% gen_server Callbacks 72 | -export([ init/1 73 | , handle_call/3 74 | , handle_cast/2 75 | , handle_info/2 76 | , terminate/2 77 | , code_change/3 78 | ]). 79 | 80 | %% Mnesia bootstrap 81 | -export([mnesia/1]). 82 | 83 | -boot_mnesia({mnesia, [boot]}). 84 | -copy_mnesia({mnesia, [copy]}). 85 | 86 | -define(REGISTRY, ?MODULE). 87 | 88 | %% Statistics 89 | -define(STATS, 90 | [ {?RULE_TAB, 'rules.count', 'rules.max'} 91 | , {?ACTION_TAB, 'actions.count', 'actions.max'} 92 | , {?RES_TAB, 'resources.count', 'resources.max'} 93 | ]). 94 | 95 | %%------------------------------------------------------------------------------ 96 | %% Mnesia bootstrap 97 | %%------------------------------------------------------------------------------ 98 | 99 | %% @doc Create or replicate tables. 100 | -spec(mnesia(boot | copy) -> ok). 101 | mnesia(boot) -> 102 | %% Optimize storage 103 | StoreProps = [{ets, [{read_concurrency, true}]}], 104 | %% Rule table 105 | ok = ekka_mnesia:create_table(?RULE_TAB, [ 106 | {disc_copies, [node()]}, 107 | {record_name, rule}, 108 | {index, [#rule.for]}, 109 | {attributes, record_info(fields, rule)}, 110 | {storage_properties, StoreProps}]), 111 | %% Rule action table 112 | ok = ekka_mnesia:create_table(?ACTION_TAB, [ 113 | {ram_copies, [node()]}, 114 | {record_name, action}, 115 | {index, [#action.for, #action.app]}, 116 | {attributes, record_info(fields, action)}, 117 | {storage_properties, StoreProps}]), 118 | %% Resource table 119 | ok = ekka_mnesia:create_table(?RES_TAB, [ 120 | {disc_copies, [node()]}, 121 | {record_name, resource}, 122 | {index, [#resource.type]}, 123 | {attributes, record_info(fields, resource)}, 124 | {storage_properties, StoreProps}]), 125 | %% Resource type table 126 | ok = ekka_mnesia:create_table(?RES_TYPE_TAB, [ 127 | {ram_copies, [node()]}, 128 | {record_name, resource_type}, 129 | {index, [#resource_type.provider]}, 130 | {attributes, record_info(fields, resource_type)}, 131 | {storage_properties, StoreProps}]); 132 | 133 | mnesia(copy) -> 134 | %% Copy rule table 135 | ok = ekka_mnesia:copy_table(?RULE_TAB), 136 | %% Copy rule action table 137 | ok = ekka_mnesia:copy_table(?ACTION_TAB), 138 | %% Copy resource table 139 | ok = ekka_mnesia:copy_table(?RES_TAB), 140 | %% Copy resource type table 141 | ok = ekka_mnesia:copy_table(?RES_TYPE_TAB). 142 | 143 | dump() -> 144 | io:format("Rules: ~p~n" 145 | "ActionInstParams: ~p~n" 146 | "Resources: ~p~n" 147 | "ResourceParams: ~p~n", 148 | [ets:tab2list(?RULE_TAB), 149 | ets:tab2list(?ACTION_INST_PARAMS_TAB), 150 | ets:tab2list(?RES_TAB), 151 | ets:tab2list(?RES_PARAMS_TAB)]). 152 | 153 | %%------------------------------------------------------------------------------ 154 | %% Start the registry 155 | %%------------------------------------------------------------------------------ 156 | 157 | -spec(start_link() -> {ok, pid()} | ignore | {error, Reason :: term()}). 158 | start_link() -> 159 | gen_server:start_link({local, ?REGISTRY}, ?MODULE, [], []). 160 | 161 | %%------------------------------------------------------------------------------ 162 | %% Rule Management 163 | %%------------------------------------------------------------------------------ 164 | 165 | -spec(get_rules() -> list(emqx_rule_engine:rule())). 166 | get_rules() -> 167 | get_all_records(?RULE_TAB). 168 | 169 | -spec(get_rules_for(Topic :: binary()) -> list(emqx_rule_engine:rule())). 170 | get_rules_for(Topic) -> 171 | [Rule || Rule = #rule{for = For} <- get_rules(), 172 | Topic =:= For orelse emqx_rule_utils:can_topic_match_oneof(Topic, For)]. 173 | 174 | -spec(get_rule(Id :: rule_id()) -> {ok, emqx_rule_engine:rule()} | not_found). 175 | get_rule(Id) -> 176 | case mnesia:dirty_read(?RULE_TAB, Id) of 177 | [Rule] -> {ok, Rule}; 178 | [] -> not_found 179 | end. 180 | 181 | -spec(add_rule(emqx_rule_engine:rule()) -> ok). 182 | add_rule(Rule) when is_record(Rule, rule) -> 183 | trans(fun insert_rule/1, [Rule]). 184 | 185 | -spec(add_rules(list(emqx_rule_engine:rule())) -> ok). 186 | add_rules(Rules) -> 187 | trans(fun lists:foreach/2, [fun insert_rule/1, Rules]). 188 | 189 | -spec(remove_rule(emqx_rule_engine:rule() | rule_id()) -> ok). 190 | remove_rule(RuleOrId) -> 191 | trans(fun delete_rule/1, [RuleOrId]). 192 | 193 | -spec(remove_rules(list(emqx_rule_engine:rule()) | list(rule_id())) -> ok). 194 | remove_rules(Rules) -> 195 | trans(fun lists:foreach/2, [fun delete_rule/1, Rules]). 196 | 197 | %% @private 198 | insert_rule(Rule = #rule{}) -> 199 | mnesia:write(?RULE_TAB, Rule, write). 200 | 201 | %% @private 202 | delete_rule(RuleId) when is_binary(RuleId) -> 203 | case get_rule(RuleId) of 204 | {ok, Rule} -> delete_rule(Rule); 205 | not_found -> ok 206 | end; 207 | delete_rule(Rule = #rule{}) when is_record(Rule, rule) -> 208 | mnesia:delete_object(?RULE_TAB, Rule, write). 209 | 210 | %%------------------------------------------------------------------------------ 211 | %% Action Management 212 | %%------------------------------------------------------------------------------ 213 | 214 | %% @doc Get all actions. 215 | -spec(get_actions() -> list(emqx_rule_engine:action())). 216 | get_actions() -> 217 | get_all_records(?ACTION_TAB). 218 | 219 | %% @doc Find an action by name. 220 | -spec(find_action(Name :: action_name()) -> {ok, emqx_rule_engine:action()} | not_found). 221 | find_action(Name) -> 222 | case mnesia:dirty_read(?ACTION_TAB, Name) of 223 | [Action] -> {ok, Action}; 224 | [] -> not_found 225 | end. 226 | 227 | %% @doc Add an action. 228 | -spec(add_action(emqx_rule_engine:action()) -> ok). 229 | add_action(Action) when is_record(Action, action) -> 230 | trans(fun insert_action/1, [Action]). 231 | 232 | %% @doc Add actions. 233 | -spec(add_actions(list(emqx_rule_engine:action())) -> ok). 234 | add_actions(Actions) when is_list(Actions) -> 235 | trans(fun lists:foreach/2, [fun insert_action/1, Actions]). 236 | 237 | %% @doc Remove an action. 238 | -spec(remove_action(emqx_rule_engine:action() | atom()) -> ok). 239 | remove_action(Action) when is_record(Action, action) -> 240 | trans(fun delete_action/1, [Action]); 241 | 242 | remove_action(Name) -> 243 | trans(fun mnesia:delete/1, [{?ACTION_TAB, Name}]). 244 | 245 | %% @doc Remove actions. 246 | -spec(remove_actions(list(emqx_rule_engine:action())) -> ok). 247 | remove_actions(Actions) -> 248 | trans(fun lists:foreach/2, [fun delete_action/1, Actions]). 249 | 250 | %% @doc Remove actions of the App. 251 | -spec(remove_actions_of(App :: atom()) -> ok). 252 | remove_actions_of(App) -> 253 | trans(fun() -> 254 | lists:foreach(fun delete_action/1, mnesia:index_read(?ACTION_TAB, App, #action.app)) 255 | end). 256 | 257 | %% @private 258 | insert_action(Action) -> 259 | mnesia:write(?ACTION_TAB, Action, write). 260 | 261 | %% @private 262 | delete_action(Action) when is_record(Action, action) -> 263 | mnesia:delete_object(?ACTION_TAB, Action, write); 264 | delete_action(Name) when is_atom(Name) -> 265 | mnesia:delete(?ACTION_TAB, Name, write). 266 | 267 | %% @doc Add an action instance params. 268 | -spec(add_action_instance_params(emqx_rule_engine:action_instance_params()) -> ok). 269 | add_action_instance_params(ActionInstParams) when is_record(ActionInstParams, action_instance_params) -> 270 | ets:insert(?ACTION_INST_PARAMS_TAB, ActionInstParams), 271 | ok. 272 | 273 | -spec(get_action_instance_params(action_instance_id()) -> {ok, emqx_rule_engine:action_instance_params()} | not_found). 274 | get_action_instance_params(ActionInstId) -> 275 | case ets:lookup(?ACTION_INST_PARAMS_TAB, ActionInstId) of 276 | [ActionInstParams] -> {ok, ActionInstParams}; 277 | [] -> not_found 278 | end. 279 | 280 | %% @doc Delete an action instance params. 281 | -spec(remove_action_instance_params(action_instance_id()) -> ok). 282 | remove_action_instance_params(ActionInstId) -> 283 | ets:delete(?ACTION_INST_PARAMS_TAB, ActionInstId), 284 | ok. 285 | 286 | %%------------------------------------------------------------------------------ 287 | %% Resource Management 288 | %%------------------------------------------------------------------------------ 289 | 290 | -spec(get_resources() -> list(emqx_rule_engine:resource())). 291 | get_resources() -> 292 | get_all_records(?RES_TAB). 293 | 294 | -spec(add_resource(emqx_rule_engine:resource()) -> ok). 295 | add_resource(Resource) when is_record(Resource, resource) -> 296 | trans(fun insert_resource/1, [Resource]). 297 | 298 | -spec(add_resource_params(emqx_rule_engine:resource_params()) -> ok). 299 | add_resource_params(ResParams) when is_record(ResParams, resource_params) -> 300 | ets:insert(?RES_PARAMS_TAB, ResParams), 301 | ok. 302 | 303 | -spec(find_resource(Id :: resource_id()) -> {ok, emqx_rule_engine:resource()} | not_found). 304 | find_resource(Id) -> 305 | case mnesia:dirty_read(?RES_TAB, Id) of 306 | [Res] -> {ok, Res}; 307 | [] -> not_found 308 | end. 309 | 310 | -spec(find_resource_params(Id :: resource_id()) 311 | -> {ok, emqx_rule_engine:resource_params()} | not_found). 312 | find_resource_params(Id) -> 313 | case ets:lookup(?RES_PARAMS_TAB, Id) of 314 | [ResParams] -> {ok, ResParams}; 315 | [] -> not_found 316 | end. 317 | 318 | -spec(remove_resource(emqx_rule_engine:resource() | emqx_rule_engine:resource_id()) -> ok). 319 | remove_resource(Resource) when is_record(Resource, resource) -> 320 | trans(fun delete_resource/1, [Resource#resource.id]); 321 | 322 | remove_resource(ResId) when is_binary(ResId) -> 323 | trans(fun delete_resource/1, [ResId]). 324 | 325 | -spec(remove_resource_params(emqx_rule_engine:resource_id()) -> ok). 326 | remove_resource_params(ResId) -> 327 | ets:delete(?RES_PARAMS_TAB, ResId), 328 | ok. 329 | 330 | %% @private 331 | delete_resource(ResId) -> 332 | [[ResId =:= ResId1 andalso throw({dependency_exists, {rule, Id}}) 333 | || #action_instance{args = #{<<"$resource">> := ResId1}} <- Actions] 334 | || #rule{id = Id, actions = Actions} <- get_rules()], 335 | mnesia:delete(?RES_TAB, ResId, write). 336 | 337 | %% @private 338 | insert_resource(Resource) -> 339 | mnesia:write(?RES_TAB, Resource, write). 340 | 341 | %%------------------------------------------------------------------------------ 342 | %% Resource Type Management 343 | %%------------------------------------------------------------------------------ 344 | 345 | -spec(get_resource_types() -> list(emqx_rule_engine:resource_type())). 346 | get_resource_types() -> 347 | get_all_records(?RES_TYPE_TAB). 348 | 349 | -spec(find_resource_type(Name :: resource_type_name()) -> {ok, emqx_rule_engine:resource_type()} | not_found). 350 | find_resource_type(Name) -> 351 | case mnesia:dirty_read(?RES_TYPE_TAB, Name) of 352 | [ResType] -> {ok, ResType}; 353 | [] -> not_found 354 | end. 355 | 356 | -spec(get_resources_by_type(Type :: resource_type_name()) -> list(emqx_rule_engine:resource())). 357 | get_resources_by_type(Type) -> 358 | mnesia:dirty_index_read(?RES_TAB, Type, #resource.type). 359 | 360 | -spec(register_resource_types(list(emqx_rule_engine:resource_type())) -> ok). 361 | register_resource_types(Types) -> 362 | trans(fun lists:foreach/2, [fun insert_resource_type/1, Types]). 363 | 364 | %% @doc Unregister resource types of the App. 365 | -spec(unregister_resource_types_of(App :: atom()) -> ok). 366 | unregister_resource_types_of(App) -> 367 | trans(fun() -> 368 | lists:foreach(fun delete_resource_type/1, mnesia:index_read(?RES_TYPE_TAB, App, #resource_type.provider)) 369 | end). 370 | 371 | %% @private 372 | insert_resource_type(Type) -> 373 | mnesia:write(?RES_TYPE_TAB, Type, write). 374 | 375 | %% @private 376 | delete_resource_type(Type) -> 377 | mnesia:delete_object(?RES_TYPE_TAB, Type, write). 378 | 379 | %%------------------------------------------------------------------------------ 380 | %% gen_server callbacks 381 | %%------------------------------------------------------------------------------ 382 | 383 | init([]) -> 384 | %% Enable stats timer 385 | ok = emqx_stats:update_interval(rule_registery_stats, fun update_stats/0), 386 | {ok, #{}}. 387 | 388 | handle_call(Req, _From, State) -> 389 | ?LOG(error, "[RuleRegistry]: unexpected call - ~p", [Req]), 390 | {reply, ignored, State}. 391 | 392 | handle_cast(Msg, State) -> 393 | ?LOG(error, "[RuleRegistry]: unexpected cast ~p", [Msg]), 394 | {noreply, State}. 395 | 396 | handle_info(Info, State) -> 397 | ?LOG(error, "[RuleRegistry]: unexpected info ~p", [Info]), 398 | {noreply, State}. 399 | 400 | terminate(_Reason, _State) -> 401 | emqx_stats:cancel_update(rule_registery_stats). 402 | 403 | code_change(_OldVsn, State, _Extra) -> 404 | {ok, State}. 405 | 406 | %%------------------------------------------------------------------------------ 407 | %% Private functions 408 | %%------------------------------------------------------------------------------ 409 | 410 | update_stats() -> 411 | lists:foreach( 412 | fun({Tab, Stat, MaxStat}) -> 413 | Size = mnesia:table_info(Tab, size), 414 | emqx_stats:setstat(Stat, MaxStat, Size) 415 | end, ?STATS). 416 | 417 | get_all_records(Tab) -> 418 | %mnesia:dirty_match_object(Tab, mnesia:table_info(Tab, wild_pattern)). 419 | ets:tab2list(Tab). 420 | 421 | trans(Fun) -> trans(Fun, []). 422 | trans(Fun, Args) -> 423 | case mnesia:transaction(Fun, Args) of 424 | {atomic, ok} -> ok; 425 | {aborted, Reason} -> error(Reason) 426 | end. 427 | -------------------------------------------------------------------------------- /src/emqx_rule_runtime.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_runtime). 18 | 19 | -include("rule_engine.hrl"). 20 | -include_lib("emqx/include/emqx.hrl"). 21 | -include_lib("emqx/include/logger.hrl"). 22 | 23 | -export([ apply_rule/2 24 | , apply_rules/2 25 | , clear_rule_payload/0 26 | ]). 27 | 28 | -import(emqx_rule_maps, 29 | [ nested_get/2 30 | , range_gen/2 31 | , range_get/3 32 | ]). 33 | 34 | -type(input() :: map()). 35 | -type(alias() :: atom()). 36 | -type(collection() :: {alias(), [term()]}). 37 | 38 | -define(ephemeral_alias(TYPE, NAME), 39 | iolist_to_binary(io_lib:format("_v_~s_~p_~p", [TYPE, NAME, erlang:system_time()]))). 40 | 41 | -define(ActionMaxRetry, 1). 42 | 43 | %%------------------------------------------------------------------------------ 44 | %% Apply rules 45 | %%------------------------------------------------------------------------------ 46 | -spec(apply_rules(list(emqx_rule_engine:rule()), input()) -> ok). 47 | apply_rules([], _Input) -> 48 | ok; 49 | apply_rules([#rule{enabled = false}|More], Input) -> 50 | apply_rules(More, Input); 51 | apply_rules([Rule = #rule{id = RuleID}|More], Input) -> 52 | try apply_rule(Rule, Input) 53 | catch 54 | %% ignore the errors if select or match failed 55 | _:{select_and_transform_error, Error} -> 56 | ?LOG(warning, "SELECT clause exception for ~s failed: ~p", 57 | [RuleID, Error]); 58 | _:{match_conditions_error, Error} -> 59 | ?LOG(warning, "WHERE clause exception for ~s failed: ~p", 60 | [RuleID, Error]); 61 | _:{select_and_collect_error, Error} -> 62 | ?LOG(warning, "FOREACH clause exception for ~s failed: ~p", 63 | [RuleID, Error]); 64 | _:{match_incase_error, Error} -> 65 | ?LOG(warning, "INCASE clause exception for ~s failed: ~p", 66 | [RuleID, Error]); 67 | _:Error:StkTrace -> 68 | ?LOG(error, "Apply rule ~s failed: ~p. Stacktrace:~n~p", 69 | [RuleID, Error, StkTrace]) 70 | end, 71 | apply_rules(More, Input). 72 | 73 | apply_rule(Rule = #rule{id = RuleID}, Input) -> 74 | clear_rule_payload(), 75 | do_apply_rule(Rule, add_metadata(Input, #{rule_id => RuleID})). 76 | 77 | do_apply_rule(#rule{id = RuleId, 78 | is_foreach = true, 79 | fields = Fields, 80 | doeach = DoEach, 81 | incase = InCase, 82 | conditions = Conditions, 83 | on_action_failed = OnFailed, 84 | actions = Actions}, Input) -> 85 | {Selected, Collection} = ?RAISE(select_and_collect(Fields, Input), 86 | {select_and_collect_error, {_REASON_,_ST_}}), 87 | ColumnsAndSelected = maps:merge(Input, Selected), 88 | case ?RAISE(match_conditions(Conditions, ColumnsAndSelected), 89 | {match_conditions_error, {_REASON_,_ST_}}) of 90 | true -> 91 | ok = emqx_rule_metrics:inc(RuleId, 'rules.matched'), 92 | Collection2 = filter_collection(Input, InCase, DoEach, Collection), 93 | {ok, [take_actions(Actions, Coll, Input, OnFailed) || Coll <- Collection2]}; 94 | false -> 95 | {error, nomatch} 96 | end; 97 | 98 | do_apply_rule(#rule{id = RuleId, 99 | is_foreach = false, 100 | fields = Fields, 101 | conditions = Conditions, 102 | on_action_failed = OnFailed, 103 | actions = Actions}, Input) -> 104 | Selected = ?RAISE(select_and_transform(Fields, Input), 105 | {select_and_transform_error, {_REASON_,_ST_}}), 106 | case ?RAISE(match_conditions(Conditions, maps:merge(Input, Selected)), 107 | {match_conditions_error, {_REASON_,_ST_}}) of 108 | true -> 109 | ok = emqx_rule_metrics:inc(RuleId, 'rules.matched'), 110 | {ok, take_actions(Actions, Selected, Input, OnFailed)}; 111 | false -> 112 | {error, nomatch} 113 | end. 114 | 115 | clear_rule_payload() -> 116 | erlang:erase(rule_payload). 117 | 118 | %% SELECT Clause 119 | select_and_transform(Fields, Input) -> 120 | select_and_transform(Fields, Input, #{}). 121 | 122 | select_and_transform([], _Input, Output) -> 123 | Output; 124 | select_and_transform(['*'|More], Input, Output) -> 125 | select_and_transform(More, Input, maps:merge(Output, Input)); 126 | select_and_transform([{as, Field, Alias}|More], Input, Output) -> 127 | Val = eval(Field, Input), 128 | select_and_transform(More, 129 | nested_put(Alias, Val, Input), 130 | nested_put(Alias, Val, Output)); 131 | select_and_transform([Field|More], Input, Output) -> 132 | Val = eval(Field, Input), 133 | Key = alias(Field), 134 | select_and_transform(More, 135 | nested_put(Key, Val, Input), 136 | nested_put(Key, Val, Output)). 137 | 138 | %% FOREACH Clause 139 | -spec select_and_collect(list(), input()) -> {input(), collection()}. 140 | select_and_collect(Fields, Input) -> 141 | select_and_collect(Fields, Input, {#{}, {'item', []}}). 142 | 143 | select_and_collect([{as, Field, {_, A} = Alias}], Input, {Output, _}) -> 144 | Val = eval(Field, Input), 145 | {nested_put(Alias, Val, Output), {A, ensure_list(Val)}}; 146 | select_and_collect([{as, Field, Alias}|More], Input, {Output, LastKV}) -> 147 | Val = eval(Field, Input), 148 | select_and_collect(More, 149 | nested_put(Alias, Val, Input), 150 | {nested_put(Alias, Val, Output), LastKV}); 151 | select_and_collect([Field], Input, {Output, _}) -> 152 | Val = eval(Field, Input), 153 | Key = alias(Field), 154 | {nested_put(Key, Val, Output), {'item', ensure_list(Val)}}; 155 | select_and_collect([Field|More], Input, {Output, LastKV}) -> 156 | Val = eval(Field, Input), 157 | Key = alias(Field), 158 | select_and_collect(More, 159 | nested_put(Key, Val, Input), 160 | {nested_put(Key, Val, Output), LastKV}). 161 | 162 | %% Filter each item got from FOREACH 163 | filter_collection(Input, InCase, DoEach, {CollKey, CollVal}) -> 164 | lists:filtermap( 165 | fun(Item) -> 166 | InputAndItem = maps:merge(Input, #{CollKey => Item}), 167 | case ?RAISE(match_conditions(InCase, InputAndItem), 168 | {match_incase_error, {_REASON_,_ST_}}) of 169 | true when DoEach == [] -> {true, InputAndItem}; 170 | true -> 171 | {true, ?RAISE(select_and_transform(DoEach, InputAndItem), 172 | {doeach_error, {_REASON_,_ST_}})}; 173 | false -> false 174 | end 175 | end, CollVal). 176 | 177 | %% Conditional Clauses such as WHERE, WHEN. 178 | match_conditions({'and', L, R}, Data) -> 179 | match_conditions(L, Data) andalso match_conditions(R, Data); 180 | match_conditions({'or', L, R}, Data) -> 181 | match_conditions(L, Data) orelse match_conditions(R, Data); 182 | match_conditions({'not', Var}, Data) -> 183 | case eval(Var, Data) of 184 | Bool when is_boolean(Bool) -> 185 | not Bool; 186 | _other -> false 187 | end; 188 | match_conditions({in, Var, {list, Vals}}, Data) -> 189 | lists:member(eval(Var, Data), [eval(V, Data) || V <- Vals]); 190 | match_conditions({'fun', {_, Name}, Args}, Data) -> 191 | apply_func(Name, [eval(Arg, Data) || Arg <- Args], Data); 192 | match_conditions({Op, L, R}, Data) when ?is_comp(Op) -> 193 | compare(Op, eval(L, Data), eval(R, Data)); 194 | %%match_conditions({'like', Var, Pattern}, Data) -> 195 | %% match_like(eval(Var, Data), Pattern); 196 | match_conditions({}, _Data) -> 197 | true. 198 | 199 | %% comparing numbers against strings 200 | compare(Op, L, R) when is_number(L), is_binary(R) -> 201 | do_compare(Op, L, number(R)); 202 | compare(Op, L, R) when is_binary(L), is_number(R) -> 203 | do_compare(Op, number(L), R); 204 | compare(Op, L, R) when is_atom(L), is_binary(R) -> 205 | do_compare(Op, atom_to_binary(L, utf8), R); 206 | compare(Op, L, R) when is_binary(L), is_atom(R) -> 207 | do_compare(Op, L, atom_to_binary(R, utf8)); 208 | compare(Op, L, R) -> 209 | do_compare(Op, L, R). 210 | 211 | do_compare('=', L, R) -> L == R; 212 | do_compare('>', L, R) -> L > R; 213 | do_compare('<', L, R) -> L < R; 214 | do_compare('<=', L, R) -> L =< R; 215 | do_compare('>=', L, R) -> L >= R; 216 | do_compare('<>', L, R) -> L /= R; 217 | do_compare('!=', L, R) -> L /= R; 218 | do_compare('=~', T, F) -> emqx_topic:match(T, F). 219 | 220 | number(Bin) -> 221 | try binary_to_integer(Bin) 222 | catch error:badarg -> binary_to_float(Bin) 223 | end. 224 | 225 | %% Step3 -> Take actions 226 | take_actions(Actions, Selected, Envs, OnFailed) -> 227 | [take_action(ActInst, Selected, Envs, OnFailed, ?ActionMaxRetry) 228 | || ActInst <- Actions]. 229 | 230 | take_action(#action_instance{id = Id, fallbacks = Fallbacks} = ActInst, 231 | Selected, Envs, OnFailed, RetryN) when RetryN >= 0 -> 232 | try 233 | {ok, #action_instance_params{apply = Apply}} 234 | = emqx_rule_registry:get_action_instance_params(Id), 235 | Result = Apply(Selected, Envs), 236 | emqx_rule_metrics:inc(Id, 'actions.success'), 237 | Result 238 | catch 239 | error:{badfun, Func}:_Stack -> 240 | ?LOG(warning, "Action ~p maybe outdated, refresh it and try again." 241 | "Func: ~p", [Id, Func]), 242 | _ = emqx_rule_engine:refresh_actions([ActInst]), 243 | take_action(ActInst, Selected, Envs, OnFailed, RetryN-1); 244 | Error:Reason:Stack -> 245 | handle_action_failure(OnFailed, Id, Fallbacks, Selected, Envs, {Error, Reason, Stack}) 246 | end; 247 | 248 | take_action(#action_instance{id = Id, fallbacks = Fallbacks}, Selected, Envs, OnFailed, _RetryN) -> 249 | handle_action_failure(OnFailed, Id, Fallbacks, Selected, Envs, {max_try_reached, ?ActionMaxRetry}). 250 | 251 | handle_action_failure(continue, Id, Fallbacks, Selected, Envs, Reason) -> 252 | emqx_rule_metrics:inc(Id, 'actions.failure'), 253 | ?LOG(error, "Take action ~p failed, continue next action, reason: ~0p", [Id, Reason]), 254 | take_actions(Fallbacks, Selected, Envs, continue), 255 | failed; 256 | handle_action_failure(stop, Id, Fallbacks, Selected, Envs, Reason) -> 257 | emqx_rule_metrics:inc(Id, 'actions.failure'), 258 | ?LOG(error, "Take action ~p failed, skip all actions, reason: ~0p", [Id, Reason]), 259 | take_actions(Fallbacks, Selected, Envs, continue), 260 | error({take_action_failed, {Id, Reason}}). 261 | 262 | eval({path, [{key, <<"payload">>} | Path]}, #{payload := Payload}) -> 263 | nested_get({path, Path}, may_decode_payload(Payload)); 264 | eval({path, [{key, <<"payload">>} | Path]}, #{<<"payload">> := Payload}) -> 265 | nested_get({path, Path}, may_decode_payload(Payload)); 266 | eval({path, _} = Path, Input) -> 267 | nested_get(Path, Input); 268 | eval({range, {Begin, End}}, _Input) -> 269 | range_gen(Begin, End); 270 | eval({get_range, {Begin, End}, Data}, Input) -> 271 | range_get(Begin, End, eval(Data, Input)); 272 | eval({var, _} = Var, Input) -> 273 | nested_get(Var, Input); 274 | eval({const, Val}, _Input) -> 275 | Val; 276 | %% unary add 277 | eval({'+', L}, Input) -> 278 | eval(L, Input); 279 | %% unary subtract 280 | eval({'-', L}, Input) -> 281 | -(eval(L, Input)); 282 | eval({Op, L, R}, Input) when ?is_arith(Op) -> 283 | apply_func(Op, [eval(L, Input), eval(R, Input)], Input); 284 | eval({Op, L, R}, Input) when ?is_comp(Op) -> 285 | compare(Op, eval(L, Input), eval(R, Input)); 286 | eval({list, List}, Input) -> 287 | [eval(L, Input) || L <- List]; 288 | eval({'case', <<>>, CaseClauses, ElseClauses}, Input) -> 289 | eval_case_clauses(CaseClauses, ElseClauses, Input); 290 | eval({'case', CaseOn, CaseClauses, ElseClauses}, Input) -> 291 | eval_switch_clauses(CaseOn, CaseClauses, ElseClauses, Input); 292 | eval({'fun', {_, Name}, Args}, Input) -> 293 | apply_func(Name, [eval(Arg, Input) || Arg <- Args], Input). 294 | 295 | handle_alias({path, [{key, <<"payload">>} | _]}, #{payload := Payload} = Input) -> 296 | Input#{payload => may_decode_payload(Payload)}; 297 | handle_alias({path, [{key, <<"payload">>} | _]}, #{<<"payload">> := Payload} = Input) -> 298 | Input#{<<"payload">> => may_decode_payload(Payload)}; 299 | handle_alias(_, Input) -> 300 | Input. 301 | 302 | alias({var, Var}) -> 303 | {var, Var}; 304 | alias({const, Val}) when is_binary(Val) -> 305 | {var, Val}; 306 | alias({list, L}) -> 307 | {var, ?ephemeral_alias(list, length(L))}; 308 | alias({range, R}) -> 309 | {var, ?ephemeral_alias(range, R)}; 310 | alias({get_range, _, {var, Key}}) -> 311 | {var, Key}; 312 | alias({get_range, _, {path, Path}}) -> 313 | {path, Path}; 314 | alias({path, Path}) -> 315 | {path, Path}; 316 | alias({const, Val}) -> 317 | {var, ?ephemeral_alias(const, Val)}; 318 | alias({Op, _L, _R}) when ?is_arith(Op); ?is_comp(Op) -> 319 | {var, ?ephemeral_alias(op, Op)}; 320 | alias({'case', On, _, _}) -> 321 | {var, ?ephemeral_alias('case', On)}; 322 | alias({'fun', Name, _}) -> 323 | {var, ?ephemeral_alias('fun', Name)}; 324 | alias(_) -> 325 | ?ephemeral_alias(unknown, unknown). 326 | 327 | eval_case_clauses([], ElseClauses, Input) -> 328 | case ElseClauses of 329 | {} -> undefined; 330 | _ -> eval(ElseClauses, Input) 331 | end; 332 | eval_case_clauses([{Cond, Clause} | CaseClauses], ElseClauses, Input) -> 333 | case match_conditions(Cond, Input) of 334 | true -> 335 | eval(Clause, Input); 336 | _ -> 337 | eval_case_clauses(CaseClauses, ElseClauses, Input) 338 | end. 339 | 340 | eval_switch_clauses(_CaseOn, [], ElseClauses, Input) -> 341 | case ElseClauses of 342 | {} -> undefined; 343 | _ -> eval(ElseClauses, Input) 344 | end; 345 | eval_switch_clauses(CaseOn, [{Cond, Clause} | CaseClauses], ElseClauses, Input) -> 346 | ConResult = eval(Cond, Input), 347 | case eval(CaseOn, Input) of 348 | ConResult -> 349 | eval(Clause, Input); 350 | _ -> 351 | eval_switch_clauses(CaseOn, CaseClauses, ElseClauses, Input) 352 | end. 353 | 354 | apply_func(Name, Args, Input) when is_atom(Name) -> 355 | do_apply_func(Name, Args, Input); 356 | apply_func(Name, Args, Input) when is_binary(Name) -> 357 | FunName = 358 | try binary_to_existing_atom(Name, utf8) 359 | catch error:badarg -> error({sql_function_not_supported, Name}) 360 | end, 361 | do_apply_func(FunName, Args, Input). 362 | 363 | do_apply_func(Name, Args, Input) -> 364 | case erlang:apply(emqx_rule_funcs, Name, Args) of 365 | Func when is_function(Func) -> 366 | erlang:apply(Func, [Input]); 367 | Result -> Result 368 | end. 369 | 370 | add_metadata(Input, Metadata) when is_map(Input), is_map(Metadata) -> 371 | NewMetadata = maps:merge(maps:get(metadata, Input, #{}), Metadata), 372 | Input#{metadata => NewMetadata}. 373 | 374 | %%------------------------------------------------------------------------------ 375 | %% Internal Functions 376 | %%------------------------------------------------------------------------------ 377 | may_decode_payload(Payload) when is_binary(Payload) -> 378 | case get_cached_payload() of 379 | undefined -> safe_decode_and_cache(Payload); 380 | DecodedP -> DecodedP 381 | end; 382 | may_decode_payload(Payload) -> 383 | Payload. 384 | 385 | get_cached_payload() -> 386 | erlang:get(rule_payload). 387 | 388 | cache_payload(DecodedP) -> 389 | erlang:put(rule_payload, DecodedP), 390 | DecodedP. 391 | 392 | safe_decode_and_cache(MaybeJson) -> 393 | try cache_payload(emqx_json:decode(MaybeJson, [return_maps])) 394 | catch _:_ -> #{} 395 | end. 396 | 397 | ensure_list(List) when is_list(List) -> List; 398 | ensure_list(_NotList) -> []. 399 | 400 | nested_put(Alias, Val, Input0) -> 401 | Input = handle_alias(Alias, Input0), 402 | emqx_rule_maps:nested_put(Alias, Val, Input). 403 | -------------------------------------------------------------------------------- /src/emqx_rule_sqlparser.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_sqlparser). 18 | 19 | -include("rule_engine.hrl"). 20 | -include("rule_events.hrl"). 21 | 22 | -export([parse_select/1]). 23 | 24 | -export([ select_fields/1 25 | , select_is_foreach/1 26 | , select_doeach/1 27 | , select_incase/1 28 | , select_from/1 29 | , select_where/1 30 | ]). 31 | 32 | -import(proplists, [ get_value/2 33 | , get_value/3 34 | ]). 35 | 36 | -record(select, {fields, from, where, is_foreach, doeach, incase}). 37 | 38 | -opaque(select() :: #select{}). 39 | 40 | -type(const() :: {const, number()|binary()}). 41 | 42 | -type(variable() :: binary() | list(binary())). 43 | 44 | -type(alias() :: binary() | list(binary())). 45 | 46 | -type(field() :: const() | variable() 47 | | {as, field(), alias()} 48 | | {'fun', atom(), list(field())}). 49 | 50 | -export_type([select/0]). 51 | 52 | %% Parse one select statement. 53 | -spec(parse_select(string() | binary()) 54 | -> {ok, select()} | {parse_error, term()} | {lex_error, term()}). 55 | parse_select(Sql) -> 56 | try case rulesql:parsetree(Sql) of 57 | {ok, {select, Clauses}} -> 58 | {ok, #select{ 59 | is_foreach = false, 60 | fields = get_value(fields, Clauses), 61 | doeach = [], 62 | incase = {}, 63 | from = get_value(from, Clauses), 64 | where = get_value(where, Clauses) 65 | }}; 66 | {ok, {foreach, Clauses}} -> 67 | {ok, #select{ 68 | is_foreach = true, 69 | fields = get_value(fields, Clauses), 70 | doeach = get_value(do, Clauses, []), 71 | incase = get_value(incase, Clauses, {}), 72 | from = get_value(from, Clauses), 73 | where = get_value(where, Clauses) 74 | }}; 75 | Error -> Error 76 | end 77 | catch 78 | _Error:Reason:StackTrace -> 79 | {parse_error, Reason, StackTrace} 80 | end. 81 | 82 | -spec(select_fields(select()) -> list(field())). 83 | select_fields(#select{fields = Fields}) -> 84 | Fields. 85 | 86 | -spec(select_is_foreach(select()) -> boolean()). 87 | select_is_foreach(#select{is_foreach = IsForeach}) -> 88 | IsForeach. 89 | 90 | -spec(select_doeach(select()) -> list(field())). 91 | select_doeach(#select{doeach = DoEach}) -> 92 | DoEach. 93 | 94 | -spec(select_incase(select()) -> list(field())). 95 | select_incase(#select{incase = InCase}) -> 96 | InCase. 97 | 98 | -spec(select_from(select()) -> list(binary())). 99 | select_from(#select{from = From}) -> 100 | From. 101 | 102 | -spec(select_where(select()) -> tuple()). 103 | select_where(#select{where = Where}) -> 104 | Where. 105 | 106 | -------------------------------------------------------------------------------- /src/emqx_rule_sqltester.erl: -------------------------------------------------------------------------------- 1 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 2 | %% 3 | %% Licensed under the Apache License, Version 2.0 (the "License"); 4 | %% you may not use this file except in compliance with the License. 5 | %% You may obtain a copy of the License at 6 | %% 7 | %% http://www.apache.org/licenses/LICENSE-2.0 8 | %% 9 | %% Unless required by applicable law or agreed to in writing, software 10 | %% distributed under the License is distributed on an "AS IS" BASIS, 11 | %% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | %% See the License for the specific language governing permissions and 13 | %% limitations under the License. 14 | 15 | -module(emqx_rule_sqltester). 16 | 17 | -include("rule_engine.hrl"). 18 | -include("rule_events.hrl"). 19 | -include_lib("emqx/include/logger.hrl"). 20 | 21 | -export([ test/1 22 | ]). 23 | 24 | -spec(test(#{}) -> {ok, Result::map()} | no_return()). 25 | test(#{<<"rawsql">> := Sql, <<"ctx">> := Context}) -> 26 | case emqx_rule_sqlparser:parse_select(Sql) of 27 | {ok, Select} -> 28 | InTopic = maps:get(<<"topic">>, Context, <<>>), 29 | EventTopics = emqx_rule_sqlparser:select_from(Select), 30 | case lists:all(fun is_publish_topic/1, EventTopics) of 31 | true -> 32 | %% test if the topic matches the topic filters in the rule 33 | case emqx_rule_utils:can_topic_match_oneof(InTopic, EventTopics) of 34 | true -> test_rule(Sql, Select, Context, EventTopics); 35 | false -> {error, nomatch} 36 | end; 37 | false -> 38 | %% the rule is for both publish and events, test it directly 39 | test_rule(Sql, Select, Context, EventTopics) 40 | end; 41 | Error -> error(Error) 42 | end. 43 | 44 | test_rule(Sql, Select, Context, EventTopics) -> 45 | RuleId = iolist_to_binary(["test_rule", emqx_rule_id:gen()]), 46 | ActInstId = iolist_to_binary(["test_action", emqx_rule_id:gen()]), 47 | Rule = #rule{ 48 | id = RuleId, 49 | rawsql = Sql, 50 | for = EventTopics, 51 | is_foreach = emqx_rule_sqlparser:select_is_foreach(Select), 52 | fields = emqx_rule_sqlparser:select_fields(Select), 53 | doeach = emqx_rule_sqlparser:select_doeach(Select), 54 | incase = emqx_rule_sqlparser:select_incase(Select), 55 | conditions = emqx_rule_sqlparser:select_where(Select), 56 | actions = [#action_instance{ 57 | id = ActInstId, 58 | name = test_rule_sql}] 59 | }, 60 | FullContext = fill_default_values(hd(EventTopics), emqx_rule_maps:atom_key_map(Context)), 61 | try 62 | ok = emqx_rule_registry:add_action_instance_params( 63 | #action_instance_params{id = ActInstId, 64 | params = #{}, 65 | apply = sql_test_action()}), 66 | emqx_rule_runtime:apply_rule(Rule, FullContext) 67 | of 68 | {ok, Data} -> {ok, flatten(Data)}; 69 | {error, nomatch} -> {error, nomatch} 70 | after 71 | ok = emqx_rule_registry:remove_action_instance_params(ActInstId) 72 | end. 73 | 74 | is_publish_topic(<<"$events/", _/binary>>) -> false; 75 | is_publish_topic(_Topic) -> true. 76 | 77 | flatten([]) -> []; 78 | flatten([D1]) -> D1; 79 | flatten([D1 | L]) when is_list(D1) -> 80 | D1 ++ flatten(L). 81 | 82 | sql_test_action() -> 83 | fun(Data, _Envs) -> 84 | ?LOG(info, "Testing Rule SQL OK"), Data 85 | end. 86 | 87 | fill_default_values(Event, Context) -> 88 | maps:merge(?EG_ENVS(Event), Context). 89 | -------------------------------------------------------------------------------- /src/emqx_rule_utils.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_utils). 18 | 19 | %% preprocess and process tempalte string with place holders 20 | -export([ preproc_tmpl/1 21 | , proc_tmpl/2 22 | , preproc_sql/1 23 | , preproc_sql/2]). 24 | 25 | %% type converting 26 | -export([ str/1 27 | , bin/1 28 | , bool/1 29 | , int/1 30 | , float/1 31 | , map/1 32 | , utf8_bin/1 33 | , utf8_str/1 34 | , number_to_binary/1 35 | , atom_key/1 36 | , unsafe_atom_key/1 37 | ]). 38 | 39 | %% connectivity check 40 | -export([ http_connectivity/1 41 | , http_connectivity/2 42 | , tcp_connectivity/2 43 | , tcp_connectivity/3 44 | ]). 45 | 46 | -export([ now_ms/0 47 | , can_topic_match_oneof/2 48 | ]). 49 | 50 | -compile({no_auto_import, 51 | [ float/1 52 | ]}). 53 | 54 | -define(EX_PLACE_HOLDER, "(\\$\\{[a-zA-Z0-9\\._]+\\})"). 55 | 56 | -type(uri_string() :: iodata()). 57 | 58 | -type(tmpl_token() :: list({var, fun()} | {str, binary()})). 59 | 60 | -type(prepare_statement() :: binary()). 61 | 62 | -type(prepare_params() :: fun((binary()) -> list())). 63 | 64 | %% preprocess template string with place holders 65 | -spec(preproc_tmpl(binary()) -> tmpl_token()). 66 | preproc_tmpl(Str) -> 67 | Tokens = re:split(Str, ?EX_PLACE_HOLDER, [{return,binary},group,trim]), 68 | preproc_tmpl(Tokens, []). 69 | 70 | preproc_tmpl([], Acc) -> 71 | lists:reverse(Acc); 72 | preproc_tmpl([[Str, Phld]| Tokens], Acc) -> 73 | GetVarFun = fun(Data) -> get_phld_var(Phld, Data) end, 74 | preproc_tmpl(Tokens, 75 | put_head(var, GetVarFun, 76 | put_head(str, Str, Acc))); 77 | preproc_tmpl([[Str]| Tokens], Acc) -> 78 | preproc_tmpl(Tokens, put_head(str, Str, Acc)). 79 | 80 | put_head(_Type, <<>>, List) -> List; 81 | put_head(Type, Term, List) -> 82 | [{Type, Term} | List]. 83 | 84 | -spec(proc_tmpl(tmpl_token(), binary()) -> binary()). 85 | proc_tmpl(Tokens, Data) -> 86 | list_to_binary( 87 | lists:map( 88 | fun ({str, Str}) -> Str; 89 | ({var, GetVal}) -> bin(GetVal(Data)) 90 | end, Tokens)). 91 | 92 | %% preprocess SQL with place holders 93 | -spec(preproc_sql(Sql::binary()) -> {prepare_statement(), prepare_params()}). 94 | preproc_sql(Sql) -> 95 | preproc_sql(Sql, '?'). 96 | 97 | -spec(preproc_sql(Sql::binary(), ReplaceWith :: '?' | '$n') -> {prepare_statement(), prepare_params()}). 98 | preproc_sql(Sql, ReplaceWith) -> 99 | case re:run(Sql, ?EX_PLACE_HOLDER, [{capture, all_but_first, binary}, global]) of 100 | {match, PlaceHolders} -> 101 | {repalce_with(Sql, ReplaceWith), 102 | fun(Data) -> 103 | [sql_data(get_phld_var(Phld, Data)) 104 | || Phld <- [hd(PH) || PH <- PlaceHolders]] 105 | end}; 106 | nomatch -> 107 | {Sql, fun(_) -> [] end} 108 | end. 109 | 110 | get_phld_var(Phld, Data) -> 111 | emqx_rule_maps:nested_get(parse_nested(unwrap(Phld)), Data). 112 | 113 | repalce_with(Tmpl, '?') -> 114 | re:replace(Tmpl, ?EX_PLACE_HOLDER, "?", [{return, binary}, global]); 115 | repalce_with(Tmpl, '$n') -> 116 | Parts = re:split(Tmpl, ?EX_PLACE_HOLDER, [{return, binary}, trim, group]), 117 | {Res, _} = 118 | lists:foldl( 119 | fun([Tkn, _Phld], {Acc, Seq}) -> 120 | Seq1 = erlang:integer_to_binary(Seq), 121 | {<>, Seq + 1}; 122 | ([Tkn], {Acc, Seq}) -> 123 | {<>, Seq} 124 | end, {<<>>, 1}, Parts), 125 | Res. 126 | 127 | unsafe_atom_key(Key) when is_atom(Key) -> 128 | Key; 129 | unsafe_atom_key(Key) when is_binary(Key) -> 130 | binary_to_atom(Key, utf8); 131 | unsafe_atom_key(Keys = [_Key | _]) -> 132 | [unsafe_atom_key(SubKey) || SubKey <- Keys]; 133 | unsafe_atom_key(Key) -> 134 | error({invalid_key, Key}). 135 | 136 | atom_key(Key) when is_atom(Key) -> 137 | Key; 138 | atom_key(Key) when is_binary(Key) -> 139 | try binary_to_existing_atom(Key, utf8) 140 | catch error:badarg -> error({invalid_key, Key}) 141 | end; 142 | atom_key(Keys = [_Key | _]) -> %% nested keys 143 | [atom_key(SubKey) || SubKey <- Keys]; 144 | atom_key(Key) -> 145 | error({invalid_key, Key}). 146 | 147 | -spec(http_connectivity(uri_string()) -> ok | {error, Reason :: term()}). 148 | http_connectivity(Url) -> 149 | http_connectivity(Url, 3000). 150 | 151 | -spec(http_connectivity(uri_string(), integer()) -> ok | {error, Reason :: term()}). 152 | http_connectivity(Url, Timeout) -> 153 | case uri_string:parse(uri_string:normalize(Url)) of 154 | {error, Reason, _} -> 155 | {error, Reason}; 156 | #{host := Host, port := Port} -> 157 | tcp_connectivity(str(Host), Port, Timeout); 158 | #{host := Host, scheme := Scheme} -> 159 | tcp_connectivity(str(Host), default_port(Scheme), Timeout); 160 | _ -> 161 | {error, {invalid_url, Url}} 162 | end. 163 | 164 | -spec tcp_connectivity(Host :: inet:socket_address() | inet:hostname(), 165 | Port :: inet:port_number()) 166 | -> ok | {error, Reason :: term()}. 167 | tcp_connectivity(Host, Port) -> 168 | tcp_connectivity(Host, Port, 3000). 169 | 170 | -spec(tcp_connectivity(Host :: inet:socket_address() | inet:hostname(), 171 | Port :: inet:port_number(), 172 | Timeout :: integer()) 173 | -> ok | {error, Reason :: term()}). 174 | tcp_connectivity(Host, Port, Timeout) -> 175 | case gen_tcp:connect(Host, Port, [], Timeout) of 176 | {ok, Sock} -> gen_tcp:close(Sock), ok; 177 | {error, Reason} -> {error, Reason} 178 | end. 179 | 180 | default_port("http") -> 80; 181 | default_port("https") -> 443; 182 | default_port(<<"http">>) -> 80; 183 | default_port(<<"https">>) -> 443; 184 | default_port(Scheme) -> throw({bad_scheme, Scheme}). 185 | 186 | 187 | unwrap(<<"${", Val/binary>>) -> 188 | binary:part(Val, {0, byte_size(Val)-1}). 189 | 190 | sql_data(List) when is_list(List) -> List; 191 | sql_data(Bin) when is_binary(Bin) -> Bin; 192 | sql_data(Num) when is_number(Num) -> Num; 193 | sql_data(Bool) when is_boolean(Bool) -> Bool; 194 | sql_data(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); 195 | sql_data(Map) when is_map(Map) -> emqx_json:encode(Map). 196 | 197 | str(Bin) when is_binary(Bin) -> binary_to_list(Bin); 198 | str(Num) when is_number(Num) -> number_to_list(Num); 199 | str(Atom) when is_atom(Atom) -> atom_to_list(Atom); 200 | str(Map) when is_map(Map) -> binary_to_list(emqx_json:encode(Map)); 201 | str(List) when is_list(List) -> 202 | case io_lib:printable_list(List) of 203 | true -> List; 204 | false -> binary_to_list(emqx_json:encode(List)) 205 | end; 206 | str(Data) -> error({invalid_str, Data}). 207 | 208 | utf8_bin(Str) when is_binary(Str); is_list(Str) -> 209 | unicode:characters_to_binary(Str); 210 | utf8_bin(Str) -> 211 | unicode:characters_to_binary(bin(Str)). 212 | 213 | utf8_str(Str) when is_binary(Str); is_list(Str) -> 214 | unicode:characters_to_list(Str); 215 | utf8_str(Str) -> 216 | unicode:characters_to_list(str(Str)). 217 | 218 | bin(Bin) when is_binary(Bin) -> Bin; 219 | bin(Num) when is_number(Num) -> number_to_binary(Num); 220 | bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); 221 | bin(Map) when is_map(Map) -> emqx_json:encode(Map); 222 | bin(List) when is_list(List) -> 223 | case io_lib:printable_list(List) of 224 | true -> list_to_binary(List); 225 | false -> emqx_json:encode(List) 226 | end; 227 | bin(Data) -> error({invalid_bin, Data}). 228 | 229 | int(List) when is_list(List) -> 230 | try list_to_integer(List) 231 | catch error:badarg -> 232 | int(list_to_float(List)) 233 | end; 234 | int(Bin) when is_binary(Bin) -> 235 | try binary_to_integer(Bin) 236 | catch error:badarg -> 237 | int(binary_to_float(Bin)) 238 | end; 239 | int(Int) when is_integer(Int) -> Int; 240 | int(Float) when is_float(Float) -> erlang:floor(Float); 241 | int(true) -> 1; 242 | int(false) -> 0; 243 | int(Data) -> error({invalid_number, Data}). 244 | 245 | float(List) when is_list(List) -> 246 | try list_to_float(List) 247 | catch error:badarg -> 248 | float(list_to_integer(List)) 249 | end; 250 | float(Bin) when is_binary(Bin) -> 251 | try binary_to_float(Bin) 252 | catch error:badarg -> 253 | float(binary_to_integer(Bin)) 254 | end; 255 | float(Num) when is_number(Num) -> erlang:float(Num); 256 | float(Data) -> error({invalid_number, Data}). 257 | 258 | map(Bin) when is_binary(Bin) -> 259 | case emqx_json:decode(Bin, [return_maps]) of 260 | Map = #{} -> Map; 261 | _ -> error({invalid_map, Bin}) 262 | end; 263 | map(List) when is_list(List) -> maps:from_list(List); 264 | map(Map) when is_map(Map) -> Map; 265 | map(Data) -> error({invalid_map, Data}). 266 | 267 | 268 | bool(Bool) when Bool == true; 269 | Bool == <<"true">>; 270 | Bool == 1 -> true; 271 | bool(Bool) when Bool == false; 272 | Bool == <<"false">>; 273 | Bool == 0 -> false; 274 | bool(Bool) -> error({invalid_boolean, Bool}). 275 | 276 | number_to_binary(Int) when is_integer(Int) -> 277 | integer_to_binary(Int); 278 | number_to_binary(Float) when is_float(Float) -> 279 | float_to_binary(Float, [{decimals, 10}, compact]). 280 | 281 | number_to_list(Int) when is_integer(Int) -> 282 | integer_to_list(Int); 283 | number_to_list(Float) when is_float(Float) -> 284 | float_to_list(Float, [{decimals, 10}, compact]). 285 | 286 | parse_nested(Attr) -> 287 | case string:split(Attr, <<".">>, all) of 288 | [Attr] -> {var, Attr}; 289 | Nested -> {path, [{key, P} || P <- Nested]} 290 | end. 291 | 292 | now_ms() -> 293 | erlang:system_time(millisecond). 294 | 295 | can_topic_match_oneof(Topic, Filters) -> 296 | MatchedFilters = [Fltr || Fltr <- Filters, emqx_topic:match(Topic, Fltr)], 297 | length(MatchedFilters) > 0. 298 | -------------------------------------------------------------------------------- /src/emqx_rule_validator.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_validator). 18 | 19 | -include("rule_engine.hrl"). 20 | 21 | -export([ validate_params/2 22 | , validate_spec/1 23 | ]). 24 | 25 | -type(params_spec() :: #{atom() => term()}). 26 | -type(params() :: #{binary() => term()}). 27 | 28 | -define(DATA_TYPES, [string, number, float, boolean, object, array, file]). 29 | 30 | %%------------------------------------------------------------------------------ 31 | %% APIs 32 | %%------------------------------------------------------------------------------ 33 | 34 | %% Validate the params according the spec and return a new spec. 35 | %% Note that this function will throw out exceptions in case of 36 | %% validation failure. 37 | -spec(validate_params(params(), params_spec()) -> params()). 38 | validate_params(Params, ParamsSepc) -> 39 | maps:map(fun(Name, Spec) -> 40 | do_validate_param(Name, Spec, Params) 41 | end, ParamsSepc), 42 | ok. 43 | 44 | -spec(validate_spec(params_spec()) -> ok). 45 | validate_spec(ParamsSepc) -> 46 | maps:map(fun do_validate_spec/2, ParamsSepc), 47 | ok. 48 | 49 | %%------------------------------------------------------------------------------ 50 | %% Internal Functions 51 | %%------------------------------------------------------------------------------ 52 | 53 | do_validate_param(Name, Spec = #{required := true}, Params) -> 54 | find_field(Name, Params, 55 | fun (not_found) -> error({required_field_missing, Name}); 56 | (Val) -> do_validate_param(Val, Spec) 57 | end); 58 | do_validate_param(Name, Spec, Params) -> 59 | find_field(Name, Params, 60 | fun (not_found) -> ok; %% optional field 'Name' 61 | (Val) -> do_validate_param(Val, Spec) 62 | end). 63 | 64 | do_validate_param(Val, Spec = #{type := Type}) -> 65 | case maps:find(enum, Spec) of 66 | {ok, Enum} -> validate_enum(Val, Enum); 67 | error -> ok 68 | end, 69 | validate_type(Val, Type, Spec). 70 | 71 | validate_type(Val, file, _Spec) -> 72 | ok = validate_file(Val); 73 | validate_type(Val, string, Spec) -> 74 | ok = validate_string(Val, reg_exp(maps:get(format, Spec, any))); 75 | validate_type(Val, number, Spec) -> 76 | ok = validate_number(Val, maps:get(range, Spec, any)); 77 | validate_type(Val, boolean, _Spec) -> 78 | ok = validate_boolean(Val); 79 | validate_type(Val, array, Spec) -> 80 | [do_validate_param(V, maps:get(items, Spec)) || V <- Val], 81 | ok; 82 | validate_type(Val, object, Spec) -> 83 | ok = validate_object(Val, maps:get(schema, Spec, any)). 84 | 85 | validate_enum(Val, Enum) -> 86 | case lists:member(Val, Enum) of 87 | true -> ok; 88 | false -> error({invalid_data_type, {enum, {Val, Enum}}}) 89 | end. 90 | 91 | validate_string(Val, RegExp) -> 92 | try re:run(Val, RegExp) of 93 | nomatch -> error({invalid_data_type, {string, Val}}); 94 | _Match -> ok 95 | catch 96 | _:_ -> error({invalid_data_type, {string, Val}}) 97 | end. 98 | 99 | validate_number(Val, any) when is_integer(Val); is_float(Val) -> 100 | ok; 101 | validate_number(Val, _Range = [Min, Max]) 102 | when (is_integer(Val) orelse is_float(Val)), 103 | (Val >= Min andalso Val =< Max) -> 104 | ok; 105 | validate_number(Val, Range) -> 106 | error({invalid_data_type, {number, {Val, Range}}}). 107 | 108 | validate_object(Val, Schema) -> 109 | validate_params(Val, Schema). 110 | 111 | validate_boolean(true) -> ok; 112 | validate_boolean(false) -> ok; 113 | validate_boolean(Val) -> error({invalid_data_type, {boolean, Val}}). 114 | 115 | validate_file(Val) when is_binary(Val) -> ok; 116 | validate_file(Val) -> error({invalid_data_type, {file, Val}}). 117 | 118 | reg_exp(url) -> "^https?://\\w+(\.\\w+)*(:[0-9]+)?"; 119 | reg_exp(topic) -> "^/?(\\w|\\#|\\+)+(/?(\\w|\\#|\\+))*/?$"; 120 | reg_exp(resource_type) -> "[a-zA-Z0-9_:-]"; 121 | reg_exp(any) -> ".*"; 122 | reg_exp(RegExp) -> RegExp. 123 | 124 | do_validate_spec(Name, Spec = #{type := object}) -> 125 | find_field(schema, Spec, 126 | fun (not_found) -> error({required_field_missing, {schema, {in, Name}}}); 127 | (Schema) -> validate_spec(Schema) 128 | end); 129 | do_validate_spec(Name, Spec = #{type := array}) -> 130 | find_field(items, Spec, 131 | fun (not_found) -> error({required_field_missing, {items, {in, Name}}}); 132 | (Items) -> do_validate_spec(Name, Items) 133 | end); 134 | do_validate_spec(Name, Spec = #{type := Type}) -> 135 | ok = supported_data_type(Type, ?DATA_TYPES), 136 | ok = validate_default_value(Name, Spec), 137 | ok. 138 | 139 | supported_data_type(Type, Supported) -> 140 | case lists:member(Type, Supported) of 141 | false -> error({unsupported_data_types, Type}); 142 | true -> ok 143 | end. 144 | 145 | validate_default_value(Name, Spec) -> 146 | case maps:get(required, Spec, false) of 147 | true -> ok; 148 | false -> 149 | find_field(default, Spec, 150 | fun (not_found) -> error({required_field_missing, {default, Name}}); 151 | (_Default) -> ok 152 | end) 153 | end. 154 | 155 | find_field(Field, Spec, Func) -> 156 | do_find_field([Field, bin(Field)], Spec, Func). 157 | 158 | do_find_field([], _Spec, Func) -> 159 | Func(not_found); 160 | do_find_field([F | Fields], Spec, Func) -> 161 | case maps:find(F, Spec) of 162 | {ok, Value} -> Func(Value); 163 | error -> 164 | do_find_field(Fields, Spec, Func) 165 | end. 166 | 167 | bin(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); 168 | bin(Str) when is_list(Str) -> atom_to_list(Str); 169 | bin(Bin) when is_binary(Bin) -> Bin. 170 | -------------------------------------------------------------------------------- /test/emqx_rule_events_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(emqx_rule_events_SUITE). 2 | 3 | -compile(export_all). 4 | -compile(nowarn_export_all). 5 | 6 | -include_lib("eunit/include/eunit.hrl"). 7 | 8 | all() -> emqx_ct:all(?MODULE). 9 | 10 | t_mod_hook_fun(_) -> 11 | Funcs = emqx_rule_events:module_info(exports), 12 | [?assert(lists:keymember(emqx_rule_events:hook_fun(Event), 1, Funcs)) || 13 | Event <- ['client.connected', 14 | 'client.disconnected', 15 | 'session.subscribed', 16 | 'session.unsubscribed', 17 | 'message.acked', 18 | 'message.dropped', 19 | 'message.delivered' 20 | ]]. 21 | 22 | t_printable_maps(_) -> 23 | Headers = #{peerhost => {127,0,0,1}, 24 | peername => {{127,0,0,1}, 9980}, 25 | sockname => {{127,0,0,1}, 1883} 26 | }, 27 | ?assertMatch( 28 | #{peerhost := <<"127.0.0.1">>, 29 | peername := <<"127.0.0.1:9980">>, 30 | sockname := <<"127.0.0.1:1883">> 31 | }, emqx_rule_events:printable_maps(Headers)). 32 | -------------------------------------------------------------------------------- /test/emqx_rule_id_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_id_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | 24 | all() -> emqx_ct:all(?MODULE). 25 | 26 | t_gen(_) -> 27 | ?assertEqual(10, length(emqx_rule_id:gen(10))), 28 | ?assertEqual(20, length(emqx_rule_id:gen(20))). 29 | -------------------------------------------------------------------------------- /test/emqx_rule_maps_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_maps_SUITE). 18 | 19 | -include_lib("eunit/include/eunit.hrl"). 20 | -include_lib("common_test/include/ct.hrl"). 21 | 22 | -compile(export_all). 23 | -compile(nowarn_export_all). 24 | 25 | -import(emqx_rule_maps, 26 | [ nested_get/2 27 | , nested_get/3 28 | , nested_put/3 29 | , atom_key_map/1 30 | ]). 31 | 32 | -define(path(Path), {path, 33 | [case K of 34 | {ic, Key} -> {index, {const, Key}}; 35 | {iv, Key} -> {index, {var, Key}}; 36 | {i, Path1} -> {index, Path1}; 37 | _ -> {key, K} 38 | end || K <- Path]}). 39 | 40 | -define(PROPTEST(Prop), true = proper:quickcheck(Prop)). 41 | 42 | t_nested_put_map(_) -> 43 | ?assertEqual(#{a => 1}, nested_put(?path([a]), 1, #{})), 44 | ?assertEqual(#{a => a}, nested_put(?path([a]), a, #{})), 45 | ?assertEqual(#{a => 1}, nested_put(?path([a]), 1, not_map)), 46 | ?assertMatch(#{payload := #{<<"msg">> := <<"v">>}}, 47 | nested_put(?path([<<"payload">>, <<"msg">>]), <<"v">>, 48 | #{payload => <<"{\n \"msg\": \"hello\"\n}">>})), 49 | ?assertEqual(#{a => #{b => b}}, nested_put(?path([a,b]), b, #{})), 50 | ?assertEqual(#{a => #{b => #{c => c}}}, nested_put(?path([a,b,c]), c, #{})), 51 | ?assertEqual(#{<<"k">> => v1}, nested_put(?path([k]), v1, #{<<"k">> => v0})), 52 | ?assertEqual(#{k => v1}, nested_put(?path([k]), v1, #{k => v0})), 53 | ?assertEqual(#{<<"k">> => v1, a => b}, nested_put(?path([k]), v1, #{<<"k">> => v0, a => b})), 54 | ?assertEqual(#{<<"k">> => v1}, nested_put(?path([k]), v1, #{<<"k">> => v0})), 55 | ?assertEqual(#{k => v1}, nested_put(?path([k]), v1, #{k => v0})), 56 | ?assertEqual(#{k => v1, a => b}, nested_put(?path([k]), v1, #{k => v0, a => b})), 57 | ?assertEqual(#{<<"k">> => v1, a => b}, nested_put(?path([k]), v1, #{<<"k">> => v0, a => b})), 58 | ?assertEqual(#{<<"k">> => #{<<"t">> => v1}}, nested_put(?path([k,t]), v1, #{<<"k">> => #{<<"t">> => v0}})), 59 | ?assertEqual(#{<<"k">> => #{t => v1}}, nested_put(?path([k,t]), v1, #{<<"k">> => #{t => v0}})), 60 | ?assertEqual(#{k => #{<<"t">> => #{a => v1}}}, nested_put(?path([k,t,a]), v1, #{k => #{<<"t">> => v0}})), 61 | ?assertEqual(#{k => #{<<"t">> => #{<<"a">> => v1}}}, nested_put(?path([k,t,<<"a">>]), v1, #{k => #{<<"t">> => v0}})). 62 | 63 | t_nested_put_index(_) -> 64 | ?assertEqual([1,a,3], nested_put(?path([{ic,2}]), a, [1,2,3])), 65 | ?assertEqual([1,2,3], nested_put(?path([{ic,0}]), a, [1,2,3])), 66 | ?assertEqual([1,2,3], nested_put(?path([{ic,4}]), a, [1,2,3])), 67 | ?assertEqual([1,[a],3], nested_put(?path([{ic,2}, {ic,1}]), a, [1,[2],3])), 68 | ?assertEqual([1,[[a]],3], nested_put(?path([{ic,2}, {ic,1}, {ic,1}]), a, [1,[[2]],3])), 69 | ?assertEqual([1,[[2]],3], nested_put(?path([{ic,2}, {ic,1}, {ic,2}]), a, [1,[[2]],3])), 70 | ?assertEqual([1,[a],1], nested_put(?path([{ic,2}, {i,?path([{ic,3}])}]), a, [1,[2],1])), 71 | %% nested_put to the first or tail of a list: 72 | ?assertEqual([a], nested_put(?path([{ic,head}]), a, not_list)), 73 | ?assertEqual([a], nested_put(?path([{ic,head}]), a, [])), 74 | ?assertEqual([a,1,2,3], nested_put(?path([{ic,head}]), a, [1,2,3])), 75 | ?assertEqual([a], nested_put(?path([{ic,tail}]), a, not_list)), 76 | ?assertEqual([a], nested_put(?path([{ic,tail}]), a, [])), 77 | ?assertEqual([1,2,3,a], nested_put(?path([{ic,tail}]), a, [1,2,3])). 78 | 79 | t_nested_put_negative_index(_) -> 80 | ?assertEqual([1,2,a], nested_put(?path([{ic,-1}]), a, [1,2,3])), 81 | ?assertEqual([1,a,3], nested_put(?path([{ic,-2}]), a, [1,2,3])), 82 | ?assertEqual([a,2,3], nested_put(?path([{ic,-3}]), a, [1,2,3])), 83 | ?assertEqual([1,2,3], nested_put(?path([{ic,-4}]), a, [1,2,3])). 84 | 85 | t_nested_put_mix_map_index(_) -> 86 | ?assertEqual(#{a => [a]}, nested_put(?path([a, {ic,2}]), a, #{})), 87 | ?assertEqual(#{a => [#{b => 0}]}, nested_put(?path([a, {ic,2}, b]), 0, #{})), 88 | ?assertEqual(#{a => [1,a,3]}, nested_put(?path([a, {ic,2}]), a, #{a => [1,2,3]})), 89 | ?assertEqual([1,#{a => c},3], nested_put(?path([{ic,2}, a]), c, [1,#{a => b},3])), 90 | ?assertEqual([1,#{a => [c]},3], nested_put(?path([{ic,2}, a, {ic, 1}]), c, [1,#{a => [b]},3])), 91 | ?assertEqual(#{a => [1,a,3], b => 2}, nested_put(?path([a, {iv,b}]), a, #{a => [1,2,3], b => 2})), 92 | ?assertEqual(#{a => [1,2,3], b => 2}, nested_put(?path([a, {iv,c}]), a, #{a => [1,2,3], b => 2})), 93 | ?assertEqual(#{a => [#{c => a},1,2,3]}, nested_put(?path([a, {ic,head}, c]), a, #{a => [1,2,3]})). 94 | 95 | t_nested_get_map(_) -> 96 | ?assertEqual(undefined, nested_get(?path([a]), not_map)), 97 | ?assertEqual(<<"hello">>, nested_get(?path([msg]), <<"{\n \"msg\": \"hello\"\n}">>)), 98 | ?assertEqual(<<"hello">>, nested_get(?path([<<"msg">>]), <<"{\n \"msg\": \"hello\"\n}">>)), 99 | ?assertEqual(<<"hello">>, nested_get(?path([<<"payload">>, <<"msg">>]), #{payload => <<"{\n \"msg\": \"hello\"\n}">>})), 100 | ?assertEqual(#{a => 1}, nested_get(?path([]), #{a => 1})), 101 | ?assertEqual(#{b => c}, nested_get(?path([a]), #{a => #{b => c}})), 102 | ?assertEqual(undefined, nested_get(?path([a,b,c]), not_map)), 103 | ?assertEqual(undefined, nested_get(?path([a,b,c]), #{})), 104 | ?assertEqual(undefined, nested_get(?path([a,b,c]), #{a => #{}})), 105 | ?assertEqual(undefined, nested_get(?path([a,b,c]), #{a => #{b => #{}}})), 106 | ?assertEqual(v1, nested_get(?path([p,x]), #{p => #{x => v1}})), 107 | ?assertEqual(v1, nested_get(?path([<<"p">>,<<"x">>]), #{p => #{x => v1}})), 108 | ?assertEqual(c, nested_get(?path([a,b,c]), #{a => #{b => #{c => c}}})). 109 | 110 | t_nested_get_index(_) -> 111 | %% single index get 112 | ?assertEqual(1, nested_get(?path([{ic,1}]), [1,2,3])), 113 | ?assertEqual(2, nested_get(?path([{ic,2}]), [1,2,3])), 114 | ?assertEqual(3, nested_get(?path([{ic,3}]), [1,2,3])), 115 | ?assertEqual(undefined, nested_get(?path([{ic,0}]), [1,2,3])), 116 | ?assertEqual("not_found", nested_get(?path([{ic,0}]), [1,2,3], "not_found")), 117 | ?assertEqual(undefined, nested_get(?path([{ic,4}]), [1,2,3])), 118 | ?assertEqual("not_found", nested_get(?path([{ic,4}]), [1,2,3], "not_found")), 119 | %% multiple index get 120 | ?assertEqual(c, nested_get(?path([{ic,2}, {ic,3}]), [1,[a,b,c],3])), 121 | ?assertEqual("I", nested_get(?path([{ic,2}, {ic,3}, {ic,1}]), [1,[a,b,["I","II","III"]],3])), 122 | ?assertEqual(undefined, nested_get(?path([{ic,2}, {ic,1}, {ic,1}]), [1,[a,b,["I","II","III"]],3])), 123 | ?assertEqual(default, nested_get(?path([{ic,2}, {ic,1}, {ic,1}]), [1,[a,b,["I","II","III"]],3], default)). 124 | 125 | t_nested_get_negative_index(_) -> 126 | ?assertEqual(3, nested_get(?path([{ic,-1}]), [1,2,3])), 127 | ?assertEqual(2, nested_get(?path([{ic,-2}]), [1,2,3])), 128 | ?assertEqual(1, nested_get(?path([{ic,-3}]), [1,2,3])), 129 | ?assertEqual(undefined, nested_get(?path([{ic,-4}]), [1,2,3])). 130 | 131 | t_nested_get_mix_map_index(_) -> 132 | %% index const 133 | ?assertEqual(1, nested_get(?path([a, {ic,1}]), #{a => [1,2,3]})), 134 | ?assertEqual(2, nested_get(?path([{ic,2}, a]), [1,#{a => 2},3])), 135 | ?assertEqual(undefined, nested_get(?path([a, {ic,0}]), #{a => [1,2,3]})), 136 | ?assertEqual("not_found", nested_get(?path([a, {ic,0}]), #{a => [1,2,3]}, "not_found")), 137 | ?assertEqual("not_found", nested_get(?path([b, {ic,1}]), #{a => [1,2,3]}, "not_found")), 138 | ?assertEqual(undefined, nested_get(?path([{ic,4}, a]), [1,2,3,4])), 139 | ?assertEqual("not_found", nested_get(?path([{ic,4}, a]), [1,2,3,4], "not_found")), 140 | ?assertEqual(c, nested_get(?path([a, {ic,2}, {ic,3}]), #{a => [1,[a,b,c],3]})), 141 | ?assertEqual("I", nested_get(?path([{ic,2}, c, {ic,1}]), [1,#{a => a, b => b, c => ["I","II","III"]},3])), 142 | ?assertEqual("I", nested_get(?path([{ic,2}, c, d]), [1,#{a => a, b => b, c => #{d => "I"}},3])), 143 | ?assertEqual(undefined, nested_get(?path([{ic,2}, c, e]), [1,#{a => a, b => b, c => #{d => "I"}},3])), 144 | ?assertEqual(default, nested_get(?path([{ic,2}, c, e]), [1,#{a => a, b => b, c => #{d => "I"}},3], default)), 145 | %% index var 146 | ?assertEqual(1, nested_get(?path([a, {iv,<<"b">>}]), #{a => [1,2,3], b => 1})), 147 | ?assertEqual(1, nested_get(?path([a, {iv,b}]), #{a => [1,2,3], b => 1})), 148 | ?assertEqual(undefined, nested_get(?path([a, {iv,c}]), #{a => [1,2,3], b => 1})), 149 | ?assertEqual(undefined, nested_get(?path([a, {iv,b}]), #{a => [1,2,3], b => 4})), 150 | ?assertEqual("I", nested_get(?path([{i,?path([{ic, 3}])}, c, d]), 151 | [1,#{a => a, b => b, c => #{d => "I"}},2], default)), 152 | ?assertEqual(3, nested_get(?path([a, {i,?path([b,{ic,1},c])}]), 153 | #{a => [1,2,3], b => [#{c => 3}]})), 154 | ?assertEqual(3, nested_get(?path([a, {i,?path([b,{ic,1},c])}]), 155 | #{a => [1,2,3], b => [#{c => 3}]}, default)), 156 | ?assertEqual(default, nested_get(?path([a, {i,?path([b,{ic,1},c])}]), 157 | #{a => [1,2,3], b => [#{c => 4}]}, default)), 158 | ?assertEqual(default, nested_get(?path([a, {i,?path([b,{ic,2},c])}]), 159 | #{a => [1,2,3], b => [#{c => 3}]}, default)). 160 | 161 | t_atom_key_map(_) -> 162 | ?assertEqual(#{a => 1}, atom_key_map(#{<<"a">> => 1})), 163 | ?assertEqual(#{a => 1, b => #{a => 2}}, 164 | atom_key_map(#{<<"a">> => 1, <<"b">> => #{<<"a">> => 2}})), 165 | ?assertEqual([#{a => 1}, #{b => #{a => 2}}], 166 | atom_key_map([#{<<"a">> => 1}, #{<<"b">> => #{<<"a">> => 2}}])), 167 | ?assertEqual(#{a => 1, b => [#{a => 2}, #{c => 2}]}, 168 | atom_key_map(#{<<"a">> => 1, <<"b">> => [#{<<"a">> => 2}, #{<<"c">> => 2}]})). 169 | 170 | all() -> 171 | IsTestCase = fun("t_" ++ _) -> true; (_) -> false end, 172 | [F || {F, _A} <- module_info(exports), IsTestCase(atom_to_list(F))]. 173 | 174 | suite() -> 175 | [{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}]. 176 | 177 | -------------------------------------------------------------------------------- /test/emqx_rule_metrics_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_metrics_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | -include_lib("common_test/include/ct.hrl"). 24 | 25 | all() -> 26 | [ {group, metrics} 27 | , {group, speed} ]. 28 | 29 | suite() -> 30 | [{ct_hooks, [cth_surefire]}, {timetrap, {seconds, 30}}]. 31 | 32 | groups() -> 33 | [{metrics, [sequence], 34 | [ t_action 35 | , t_rule 36 | , t_clear 37 | ]}, 38 | {speed, [sequence], 39 | [ rule_speed 40 | ]} 41 | ]. 42 | 43 | init_per_suite(Config) -> 44 | emqx_ct_helpers:start_apps([emqx]), 45 | {ok, _} = emqx_rule_metrics:start_link(), 46 | Config. 47 | 48 | end_per_suite(_Config) -> 49 | catch emqx_rule_metrics:stop(), 50 | emqx_ct_helpers:stop_apps([emqx]), 51 | ok. 52 | 53 | init_per_testcase(_, Config) -> 54 | catch emqx_rule_metrics:stop(), 55 | {ok, _} = emqx_rule_metrics:start_link(), 56 | [emqx_metrics:set(M, 0) || M <- emqx_rule_metrics:overall_metrics()], 57 | Config. 58 | 59 | end_per_testcase(_, _Config) -> 60 | ok. 61 | 62 | t_action(_) -> 63 | ?assertEqual(0, emqx_rule_metrics:get(<<"action:1">>, 'actions.success')), 64 | ?assertEqual(0, emqx_rule_metrics:get(<<"action:1">>, 'actions.failure')), 65 | ?assertEqual(0, emqx_rule_metrics:get(<<"action:2">>, 'actions.success')), 66 | ok = emqx_rule_metrics:inc(<<"action:1">>, 'actions.success'), 67 | ok = emqx_rule_metrics:inc(<<"action:1">>, 'actions.failure'), 68 | ok = emqx_rule_metrics:inc(<<"action:2">>, 'actions.success'), 69 | ok = emqx_rule_metrics:inc(<<"action:2">>, 'actions.success'), 70 | ?assertEqual(1, emqx_rule_metrics:get(<<"action:1">>, 'actions.success')), 71 | ?assertEqual(1, emqx_rule_metrics:get(<<"action:1">>, 'actions.failure')), 72 | ?assertEqual(2, emqx_rule_metrics:get(<<"action:2">>, 'actions.success')), 73 | ?assertEqual(0, emqx_rule_metrics:get(<<"action:3">>, 'actions.success')), 74 | ?assertEqual(3, emqx_rule_metrics:get_overall('actions.success')), 75 | ?assertEqual(1, emqx_rule_metrics:get_overall('actions.failure')). 76 | 77 | t_rule(_) -> 78 | ok = emqx_rule_metrics:inc(<<"rule:1">>, 'rules.matched'), 79 | ok = emqx_rule_metrics:inc(<<"rule:2">>, 'rules.matched'), 80 | ok = emqx_rule_metrics:inc(<<"rule:2">>, 'rules.matched'), 81 | ?assertEqual(1, emqx_rule_metrics:get(<<"rule:1">>, 'rules.matched')), 82 | ?assertEqual(2, emqx_rule_metrics:get(<<"rule:2">>, 'rules.matched')), 83 | ?assertEqual(0, emqx_rule_metrics:get(<<"rule:3">>, 'rules.matched')), 84 | ?assertEqual(3, emqx_rule_metrics:get_overall('rules.matched')). 85 | 86 | t_clear(_) -> 87 | ok = emqx_rule_metrics:inc(<<"action:1">>, 'actions.success'), 88 | ?assertEqual(1, emqx_rule_metrics:get(<<"action:1">>, 'actions.success')), 89 | ok = emqx_rule_metrics:clear(<<"action:1">>), 90 | ?assertEqual(0, emqx_rule_metrics:get(<<"action:1">>, 'actions.success')). 91 | 92 | rule_speed(_) -> 93 | ok = emqx_rule_metrics:inc(<<"rule:1">>, 'rules.matched'), 94 | ok = emqx_rule_metrics:inc(<<"rule:1">>, 'rules.matched'), 95 | ok = emqx_rule_metrics:inc(<<"rule:2">>, 'rules.matched'), 96 | ?assertEqual(2, emqx_rule_metrics:get(<<"rule:1">>, 'rules.matched')), 97 | ct:sleep(1000), 98 | ?LET(#{max := Max, current := Current}, emqx_rule_metrics:get_rule_speed(<<"rule:1">>), 99 | {?assert(Max =< 2), 100 | ?assert(Current =< 2)}), 101 | ct:pal("===== Speed: ~p~n", [emqx_rule_metrics:get_overall_rule_speed()]), 102 | ?LET(#{max := Max, current := Current}, emqx_rule_metrics:get_overall_rule_speed(), 103 | {?assert(Max =< 3), 104 | ?assert(Current =< 3)}), 105 | ct:sleep(2100), 106 | ?LET(#{max := Max, current := Current, last5m := Last5Min}, emqx_rule_metrics:get_rule_speed(<<"rule:1">>), 107 | {?assert(Max =< 2), 108 | ?assert(Current == 0), 109 | ?assert(Last5Min =< 0.67)}), 110 | ?LET(#{max := Max, current := Current, last5m := Last5Min}, emqx_rule_metrics:get_overall_rule_speed(), 111 | {?assert(Max =< 3), 112 | ?assert(Current == 0), 113 | ?assert(Last5Min =< 1)}), 114 | ct:sleep(3000), 115 | ?LET(#{max := Max, current := Current, last5m := Last5Min}, emqx_rule_metrics:get_overall_rule_speed(), 116 | {?assert(Max =< 3), 117 | ?assert(Current == 0), 118 | ?assert(Last5Min == 0)}). 119 | 120 | % t_create(_) -> 121 | % error('TODO'). 122 | 123 | % t_get(_) -> 124 | % error('TODO'). 125 | 126 | % t_get_overall(_) -> 127 | % error('TODO'). 128 | 129 | % t_get_rule_speed(_) -> 130 | % error('TODO'). 131 | 132 | % t_get_overall_rule_speed(_) -> 133 | % error('TODO'). 134 | 135 | % t_get_rule_metrics(_) -> 136 | % error('TODO'). 137 | 138 | % t_get_action_metrics(_) -> 139 | % error('TODO'). 140 | 141 | % t_inc(_) -> 142 | % error('TODO'). 143 | 144 | % t_overall_metrics(_) -> 145 | % error('TODO'). 146 | 147 | -------------------------------------------------------------------------------- /test/emqx_rule_registry_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_registry_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | 24 | all() -> emqx_ct:all(?MODULE). 25 | 26 | init_per_testcase(_TestCase, Config) -> 27 | Config. 28 | 29 | end_per_testcase(_TestCase, Config) -> 30 | Config. 31 | 32 | % t_mnesia(_) -> 33 | % error('TODO'). 34 | 35 | % t_dump(_) -> 36 | % error('TODO'). 37 | 38 | % t_start_link(_) -> 39 | % error('TODO'). 40 | 41 | % t_get_rules_for(_) -> 42 | % error('TODO'). 43 | 44 | % t_add_rules(_) -> 45 | % error('TODO'). 46 | 47 | % t_remove_rules(_) -> 48 | % error('TODO'). 49 | 50 | % t_add_action(_) -> 51 | % error('TODO'). 52 | 53 | % t_remove_action(_) -> 54 | % error('TODO'). 55 | 56 | % t_remove_actions(_) -> 57 | % error('TODO'). 58 | 59 | % t_init(_) -> 60 | % error('TODO'). 61 | 62 | % t_handle_call(_) -> 63 | % error('TODO'). 64 | 65 | % t_handle_cast(_) -> 66 | % error('TODO'). 67 | 68 | % t_handle_info(_) -> 69 | % error('TODO'). 70 | 71 | % t_terminate(_) -> 72 | % error('TODO'). 73 | 74 | % t_code_change(_) -> 75 | % error('TODO'). 76 | 77 | % t_get_resource_types(_) -> 78 | % error('TODO'). 79 | 80 | % t_get_resources_by_type(_) -> 81 | % error('TODO'). 82 | 83 | % t_get_actions_for(_) -> 84 | % error('TODO'). 85 | 86 | % t_get_actions(_) -> 87 | % error('TODO'). 88 | 89 | % t_get_action_instance_params(_) -> 90 | % error('TODO'). 91 | 92 | % t_remove_action_instance_params(_) -> 93 | % error('TODO'). 94 | 95 | % t_remove_resource_params(_) -> 96 | % error('TODO'). 97 | 98 | % t_add_action_instance_params(_) -> 99 | % error('TODO'). 100 | 101 | % t_add_resource_params(_) -> 102 | % error('TODO'). 103 | 104 | % t_find_action(_) -> 105 | % error('TODO'). 106 | 107 | % t_get_rules(_) -> 108 | % error('TODO'). 109 | 110 | % t_get_resources(_) -> 111 | % error('TODO'). 112 | 113 | % t_remove_resource(_) -> 114 | % error('TODO'). 115 | 116 | % t_find_resource_params(_) -> 117 | % error('TODO'). 118 | 119 | % t_add_resource(_) -> 120 | % error('TODO'). 121 | 122 | % t_find_resource_type(_) -> 123 | % error('TODO'). 124 | 125 | % t_remove_rule(_) -> 126 | % error('TODO'). 127 | 128 | % t_add_rule(_) -> 129 | % error('TODO'). 130 | 131 | % t_register_resource_types(_) -> 132 | % error('TODO'). 133 | 134 | % t_add_actions(_) -> 135 | % error('TODO'). 136 | 137 | % t_unregister_resource_types_of(_) -> 138 | % error('TODO'). 139 | 140 | % t_remove_actions_of(_) -> 141 | % error('TODO'). 142 | 143 | % t_get_rule(_) -> 144 | % error('TODO'). 145 | 146 | % t_find_resource(_) -> 147 | % error('TODO'). 148 | 149 | -------------------------------------------------------------------------------- /test/emqx_rule_utils_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_utils_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | 24 | -define(PORT, 9876). 25 | 26 | all() -> emqx_ct:all(?MODULE). 27 | 28 | t_preproc_sql(_) -> 29 | Selected = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}}, 30 | {PrepareStatement, GetPrepareParams} = emqx_rule_utils:preproc_sql(<<"a:${a},b:${b},c:${c},d:${d}">>, '?'), 31 | ?assertEqual(<<"a:?,b:?,c:?,d:?">>, PrepareStatement), 32 | ?assertEqual([<<"1">>,1,1.0,<<"{\"d1\":\"hi\"}">>], 33 | GetPrepareParams(Selected)). 34 | 35 | t_http_connectivity(_) -> 36 | {ok, Socket} = gen_tcp:listen(?PORT, []), 37 | ok = emqx_rule_utils:http_connectivity("http://127.0.0.1:"++emqx_rule_utils:str(?PORT), 1000), 38 | gen_tcp:close(Socket), 39 | {error, _} = emqx_rule_utils:http_connectivity("http://127.0.0.1:"++emqx_rule_utils:str(?PORT), 1000). 40 | 41 | t_tcp_connectivity(_) -> 42 | {ok, Socket} = gen_tcp:listen(?PORT, []), 43 | ok = emqx_rule_utils:tcp_connectivity("127.0.0.1", ?PORT, 1000), 44 | gen_tcp:close(Socket), 45 | {error, _} = emqx_rule_utils:tcp_connectivity("127.0.0.1", ?PORT, 1000). 46 | 47 | t_str(_) -> 48 | ?assertEqual("abc", emqx_rule_utils:str("abc")), 49 | ?assertEqual("abc", emqx_rule_utils:str(abc)), 50 | ?assertEqual("{\"a\":1}", emqx_rule_utils:str(#{a => 1})), 51 | ?assertEqual("1", emqx_rule_utils:str(1)), 52 | ?assertEqual("2.0", emqx_rule_utils:str(2.0)), 53 | ?assertEqual("true", emqx_rule_utils:str(true)), 54 | ?assertError(_, emqx_rule_utils:str({a, v})). 55 | 56 | t_bin(_) -> 57 | ?assertEqual(<<"abc">>, emqx_rule_utils:bin("abc")), 58 | ?assertEqual(<<"abc">>, emqx_rule_utils:bin(abc)), 59 | ?assertEqual(<<"{\"a\":1}">>, emqx_rule_utils:bin(#{a => 1})), 60 | ?assertEqual(<<"[{\"a\":1}]">>, emqx_rule_utils:bin([#{a => 1}])), 61 | ?assertEqual(<<"1">>, emqx_rule_utils:bin(1)), 62 | ?assertEqual(<<"2.0">>, emqx_rule_utils:bin(2.0)), 63 | ?assertEqual(<<"true">>, emqx_rule_utils:bin(true)), 64 | ?assertError(_, emqx_rule_utils:bin({a, v})). 65 | 66 | t_atom_key(_) -> 67 | _ = erlang, _ = port, 68 | ?assertEqual([erlang], emqx_rule_utils:atom_key([<<"erlang">>])), 69 | ?assertEqual([erlang, port], emqx_rule_utils:atom_key([<<"erlang">>, port])), 70 | ?assertEqual([erlang, port], emqx_rule_utils:atom_key([<<"erlang">>, <<"port">>])), 71 | ?assertEqual(erlang, emqx_rule_utils:atom_key(<<"erlang">>)), 72 | ?assertError({invalid_key, {a, v}}, emqx_rule_utils:atom_key({a, v})), 73 | _ = xyz876gv123, 74 | ?assertEqual([xyz876gv123, port], emqx_rule_utils:atom_key([<<"xyz876gv123">>, port])). 75 | 76 | t_unsafe_atom_key(_) -> 77 | ?assertEqual([xyz876gv], emqx_rule_utils:unsafe_atom_key([<<"xyz876gv">>])), 78 | ?assertEqual([xyz876gv33, port], emqx_rule_utils:unsafe_atom_key([<<"xyz876gv33">>, port])), 79 | ?assertEqual([xyz876gv331, port1221], emqx_rule_utils:unsafe_atom_key([<<"xyz876gv331">>, <<"port1221">>])), 80 | ?assertEqual(xyz876gv3312, emqx_rule_utils:unsafe_atom_key(<<"xyz876gv3312">>)). 81 | 82 | t_proc_tmpl(_) -> 83 | Selected = #{a => <<"1">>, b => 1, c => 1.0, d => #{d1 => <<"hi">>}}, 84 | Tks = emqx_rule_utils:preproc_tmpl(<<"a:${a},b:${b},c:${c},d:${d}">>), 85 | ?assertEqual(<<"a:1,b:1,c:1.0,d:{\"d1\":\"hi\"}">>, 86 | emqx_rule_utils:proc_tmpl(Tks, Selected)). 87 | -------------------------------------------------------------------------------- /test/emqx_rule_validator_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%-------------------------------------------------------------------- 2 | %% Copyright (c) 2020 EMQ Technologies Co., Ltd. All Rights Reserved. 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 | -module(emqx_rule_validator_SUITE). 18 | 19 | -compile(export_all). 20 | -compile(nowarn_export_all). 21 | 22 | -include_lib("eunit/include/eunit.hrl"). 23 | 24 | all() -> emqx_ct:all(?MODULE). 25 | 26 | % t_validate_params(_) -> 27 | % error('TODO'). 28 | 29 | % t_validate_spec(_) -> 30 | % error('TODO'). 31 | 32 | -------------------------------------------------------------------------------- /test/prop_rule_maps.erl: -------------------------------------------------------------------------------- 1 | -module(prop_rule_maps). 2 | 3 | -include_lib("proper/include/proper.hrl"). 4 | 5 | prop_get_put_single_key() -> 6 | ?FORALL({Key, Val}, {term(), term()}, 7 | begin 8 | Val =:= emqx_rule_maps:nested_get({var, Key}, 9 | emqx_rule_maps:nested_put({var, Key}, Val, #{})) 10 | end). 11 | --------------------------------------------------------------------------------