├── .gitignore ├── LICENSE ├── README.md ├── example ├── hot.lua ├── logs │ └── .gitkeep ├── nginx.conf ├── resty_inspect_hooks1.lua └── resty_inspect_hooks2.lua └── lib └── resty └── inspect ├── dbg.lua └── init.lua /.gitignore: -------------------------------------------------------------------------------- 1 | example/** 2 | !example/logs/ 3 | !example/logs/.gitkeep 4 | !example/nginx.conf 5 | !example/hot.lua 6 | !example/resty_inspect_hooks1.lua 7 | !example/resty_inspect_hooks2.lua 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2022 ShenZhen ZhiLiu Technology Co., Ltd 2 | Copyright (c) 2016-2022 Kong Inc. 3 | 4 | Apache License 5 | Version 2.0, January 2004 6 | http://www.apache.org/licenses/ 7 | 8 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 9 | 10 | 1. Definitions. 11 | 12 | "License" shall mean the terms and conditions for use, reproduction, 13 | and distribution as defined by Sections 1 through 9 of this document. 14 | 15 | "Licensor" shall mean the copyright owner or entity authorized by 16 | the copyright owner that is granting the License. 17 | 18 | "Legal Entity" shall mean the union of the acting entity and all 19 | other entities that control, are controlled by, or are under common 20 | control with that entity. For the purposes of this definition, 21 | "control" means (i) the power, direct or indirect, to cause the 22 | direction or management of such entity, whether by contract or 23 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 24 | outstanding shares, or (iii) beneficial ownership of such entity. 25 | 26 | "You" (or "Your") shall mean an individual or Legal Entity 27 | exercising permissions granted by this License. 28 | 29 | "Source" form shall mean the preferred form for making modifications, 30 | including but not limited to software source code, documentation 31 | source, and configuration files. 32 | 33 | "Object" form shall mean any form resulting from mechanical 34 | transformation or translation of a Source form, including but 35 | not limited to compiled object code, generated documentation, 36 | and conversions to other media types. 37 | 38 | "Work" shall mean the work of authorship, whether in Source or 39 | Object form, made available under the License, as indicated by a 40 | copyright notice that is included in or attached to the work 41 | (an example is provided in the Appendix below). 42 | 43 | "Derivative Works" shall mean any work, whether in Source or Object 44 | form, that is based on (or derived from) the Work and for which the 45 | editorial revisions, annotations, elaborations, or other modifications 46 | represent, as a whole, an original work of authorship. For the purposes 47 | of this License, Derivative Works shall not include works that remain 48 | separable from, or merely link (or bind by name) to the interfaces of, 49 | the Work and Derivative Works thereof. 50 | 51 | "Contribution" shall mean any work of authorship, including 52 | the original version of the Work and any modifications or additions 53 | to that Work or Derivative Works thereof, that is intentionally 54 | submitted to Licensor for inclusion in the Work by the copyright owner 55 | or by an individual or Legal Entity authorized to submit on behalf of 56 | the copyright owner. For the purposes of this definition, "submitted" 57 | means any form of electronic, verbal, or written communication sent 58 | to the Licensor or its representatives, including but not limited to 59 | communication on electronic mailing lists, source code control systems, 60 | and issue tracking systems that are managed by, or on behalf of, the 61 | Licensor for the purpose of discussing and improving the Work, but 62 | excluding communication that is conspicuously marked or otherwise 63 | designated in writing by the copyright owner as "Not a Contribution." 64 | 65 | "Contributor" shall mean Licensor and any individual or Legal Entity 66 | on behalf of whom a Contribution has been received by Licensor and 67 | subsequently incorporated within the Work. 68 | 69 | 2. Grant of Copyright License. Subject to the terms and conditions of 70 | this License, each Contributor hereby grants to You a perpetual, 71 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 72 | copyright license to reproduce, prepare Derivative Works of, 73 | publicly display, publicly perform, sublicense, and distribute the 74 | Work and such Derivative Works in Source or Object form. 75 | 76 | 3. Grant of Patent License. Subject to the terms and conditions of 77 | this License, each Contributor hereby grants to You a perpetual, 78 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 79 | (except as stated in this section) patent license to make, have made, 80 | use, offer to sell, sell, import, and otherwise transfer the Work, 81 | where such license applies only to those patent claims licensable 82 | by such Contributor that are necessarily infringed by their 83 | Contribution(s) alone or by combination of their Contribution(s) 84 | with the Work to which such Contribution(s) was submitted. If You 85 | institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work 87 | or a Contribution incorporated within the Work constitutes direct 88 | or contributory patent infringement, then any patent licenses 89 | granted to You under this License for that Work shall terminate 90 | as of the date such litigation is filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the 93 | Work or Derivative Works thereof in any medium, with or without 94 | modifications, and in Source or Object form, provided that You 95 | meet the following conditions: 96 | 97 | (a) You must give any other recipients of the Work or 98 | Derivative Works a copy of this License; and 99 | 100 | (b) You must cause any modified files to carry prominent notices 101 | stating that You changed the files; and 102 | 103 | (c) You must retain, in the Source form of any Derivative Works 104 | that You distribute, all copyright, patent, trademark, and 105 | attribution notices from the Source form of the Work, 106 | excluding those notices that do not pertain to any part of 107 | the Derivative Works; and 108 | 109 | (d) If the Work includes a "NOTICE" text file as part of its 110 | distribution, then any Derivative Works that You distribute must 111 | include a readable copy of the attribution notices contained 112 | within such NOTICE file, excluding those notices that do not 113 | pertain to any part of the Derivative Works, in at least one 114 | of the following places: within a NOTICE text file distributed 115 | as part of the Derivative Works; within the Source form or 116 | documentation, if provided along with the Derivative Works; or, 117 | within a display generated by the Derivative Works, if and 118 | wherever such third-party notices normally appear. The contents 119 | of the NOTICE file are for informational purposes only and 120 | do not modify the License. You may add Your own attribution 121 | notices within Derivative Works that You distribute, alongside 122 | or as an addendum to the NOTICE text from the Work, provided 123 | that such additional attribution notices cannot be construed 124 | as modifying the License. 125 | 126 | You may add Your own copyright statement to Your modifications and 127 | may provide additional or different license terms and conditions 128 | for use, reproduction, or distribution of Your modifications, or 129 | for any such Derivative Works as a whole, provided Your use, 130 | reproduction, and distribution of the Work otherwise complies with 131 | the conditions stated in this License. 132 | 133 | 5. Submission of Contributions. Unless You explicitly state otherwise, 134 | any Contribution intentionally submitted for inclusion in the Work 135 | by You to the Licensor shall be under the terms and conditions of 136 | this License, without any additional terms or conditions. 137 | Notwithstanding the above, nothing herein shall supersede or modify 138 | the terms of any separate license agreement you may have executed 139 | with Licensor regarding such Contributions. 140 | 141 | 6. Trademarks. This License does not grant permission to use the trade 142 | names, trademarks, service marks, or product names of the Licensor, 143 | except as required for reasonable and customary use in describing the 144 | origin of the Work and reproducing the content of the NOTICE file. 145 | 146 | 7. Disclaimer of Warranty. Unless required by applicable law or 147 | agreed to in writing, Licensor provides the Work (and each 148 | Contributor provides its Contributions) on an "AS IS" BASIS, 149 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 150 | implied, including, without limitation, any warranties or conditions 151 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 152 | PARTICULAR PURPOSE. You are solely responsible for determining the 153 | appropriateness of using or redistributing the Work and assume any 154 | risks associated with Your exercise of permissions under this License. 155 | 156 | 8. Limitation of Liability. In no event and under no legal theory, 157 | whether in tort (including negligence), contract, or otherwise, 158 | unless required by applicable law (such as deliberate and grossly 159 | negligent acts) or agreed to in writing, shall any Contributor be 160 | liable to You for damages, including any direct, indirect, special, 161 | incidental, or consequential damages of any character arising as a 162 | result of this License or out of the use or inability to use the 163 | Work (including but not limited to damages for loss of goodwill, 164 | work stoppage, computer failure or malfunction, or any and all 165 | other commercial damages or losses), even if such Contributor 166 | has been advised of the possibility of such damages. 167 | 168 | 9. Accepting Warranty or Additional Liability. While redistributing 169 | the Work or Derivative Works thereof, You may choose to offer, 170 | and charge a fee for, acceptance of support, warranty, indemnity, 171 | or other liability obligations and/or rights consistent with this 172 | License. However, in accepting such obligations, You may act only 173 | on Your own behalf and on Your sole responsibility, not on behalf 174 | of any other Contributor, and only if You agree to indemnify, 175 | defend, and hold each Contributor harmless for any liability 176 | incurred by, or claims asserted against, such Contributor by reason 177 | of your accepting any such warranty or additional liability. 178 | 179 | END OF TERMS AND CONDITIONS 180 | 181 | APPENDIX: How to apply the Apache License to your work. 182 | 183 | To apply the Apache License to your work, attach the following 184 | boilerplate notice, with the fields enclosed by brackets "[]" 185 | replaced with your own identifying information. (Don't include 186 | the brackets!) The text should be enclosed in the appropriate 187 | comment syntax for the file format. We also recommend that a 188 | file or class name and description of purpose be included on the 189 | same "printed page" as the copyright notice for easier 190 | identification within third-party archives. 191 | 192 | Copyright [yyyy] [name of copyright owner] 193 | 194 | Licensed under the Apache License, Version 2.0 (the "License"); 195 | you may not use this file except in compliance with the License. 196 | You may obtain a copy of the License at 197 | 198 | http://www.apache.org/licenses/LICENSE-2.0 199 | 200 | Unless required by applicable law or agreed to in writing, software 201 | distributed under the License is distributed on an "AS IS" BASIS, 202 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 203 | See the License for the specific language governing permissions and 204 | limitations under the License. 205 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lua-resty-inspect 2 | 3 | It's useful to set arbitrary breakpoint in any lua file to inspect the context information, 4 | e.g. print local variables if some condition satisfied. 5 | 6 | In this way, you don't need to modify the source code of your project, and just get diagnose information 7 | on demand, i.e. dynamic logging. 8 | 9 | **Why not modify the source code to debug?** 10 | 11 | * The production environment doesn't allow to modify the code, and you should not to. 12 | * bad code changes would pollute the business source code. 13 | * You need to reload nginx to make code changes take effect, which ruins the online business. 14 | * It's difficult to change code in container. 15 | * It's easy to forget rolling back the changes after debug. And it's hard to roll back completely. 16 | 17 | This library supports setting breakpoints within both interpretd function and jit compiled function. 18 | The breakpoint could be at any position within the function. The function could be global/local/module/ananymous. 19 | 20 | It works for luajit2.1 or lua5.1. 21 | 22 | This library has been used to implement inspect plugin of APISIX: 23 | 24 | https://apisix.apache.org/zh/docs/apisix/plugins/inspect/ 25 | 26 | ## Use cases 27 | 28 | * locate the issue source 29 | * print some masked logs 30 | * learn from huge source code via debugging 31 | 32 | ## Features 33 | 34 | * non-intrusive, bad code in the breakpint doesn't affect the business source code 35 | * set breakpoint at any position 36 | * dynamic breakpoint 37 | * Customized breakpoint handler 38 | * you could define one-shot breakpoint 39 | * work for jit compiled function 40 | * if function reference specified, then performance impact is only bound to that function (JIT compiled code will not trigger debug hook, so they would run fast even if hook is enabled) 41 | * if all breakpoints deleted, jit could recover 42 | 43 | ## Operation Graph 44 | 45 | ![1666251376616](https://user-images.githubusercontent.com/4401042/196885851-d883a7fb-0ab5-45b1-987f-7b6d810748f5.png) 46 | 47 | ## API 48 | 49 | ### require("resty.inspect.dbg").set_hook(file, line, func, filter_func) 50 | 51 | The breakpoint is specified by `file` (full qualified or short file name) and the `line` number. 52 | 53 | The `func` specified the scope (which function or global) of jit cache to flush: 54 | * If the breakpoint is related to a module function or 55 | global function, you should set it that function reference, then only the jit cache of that function would 56 | be flushed, and it would not affect other caches to avoid slowing down other parts of the program. 57 | * If the breakpointis related to local function or anonymous function, 58 | then you have to set it to `nil` (because no way to get function reference), which would flush the whole jit cache of lua vm. 59 | 60 | You attach a `filter_func` function of the breakpoint, the function takes the `info` as argument and returns 61 | true of false to determine whether the breakpoint would be removed. You could setup one-shot breakpoint 62 | at ease. 63 | 64 | The `info` is a hash table which contains below keys: 65 | 66 | * `finfo`: return value of `debug.getinfo(level, "nSlf")` 67 | * `uv`: upvalues hash table 68 | * `vals`: local variables hash table 69 | 70 | ### require("resty.inspect.dbg").unset_hook(file, line) 71 | 72 | remove the specific breakpoint. 73 | 74 | ### require("resty.inspect").init(delay, file) 75 | 76 | Setup a timer (in `delay` interval, in seconds) to monitor specific `file` to setup the needed breakpoints. You could modify that file 77 | to configure breakpoints on-the-fly. Delete that file would unset all breakpoints. It's recommanded to use soft link file trick. 78 | 79 | ### require("resty.inspect").destroy() 80 | 81 | Destroy the monitor timer. 82 | 83 | ## Synopsis 84 | 85 | ```lua 86 | -- example_hooks.lua 87 | local dbg = require "resty.inspect.dbg" 88 | 89 | -- print stack backtrace and variable `conf_key` 90 | dbg.set_hook("limit-req.lua", 88, require("apisix.plugins.limit-req").access, 91 | function(info) 92 | ngx.log(ngx.INFO, debug.traceback("foo traceback", 3)) 93 | ngx.log(ngx.INFO, dbg.getname(info.finfo)) 94 | ngx.log(ngx.INFO, "conf_key=", info.vals.conf_key) 95 | return true 96 | end) 97 | 98 | -- send info to remote server if var `i` is 222 99 | dbg.set_hook("t/lib/demo.lua", 31, require("t.lib.demo").hot2, function(info) 100 | if info.vals.i == 222 then 101 | ngx.timer.at(0, function(_, body) 102 | local httpc = require("resty.http").new() 103 | httpc:request_uri("http://127.0.0.1:9080/upstream1", { 104 | method = "POST", 105 | body = body, 106 | }) 107 | end, ngx.var.request_uri .. "," .. info.vals.i) 108 | return true 109 | end 110 | return false 111 | end) 112 | 113 | -- get worker id only if `sync_data` is one of the function callers 114 | dbg.set_hook("apisix/upstream.lua", 506, nil, function(info) 115 | if debug.traceback("demo traceback", 3):find("sync_data") then 116 | ngx.log(ngx.WARN, "ngx.worker.id=", ngx.worker.id()) 117 | return true 118 | end 119 | return false 120 | end) 121 | 122 | -- check when new route configuration get updated by data plane 123 | local cjson = require("cjson") 124 | dbg.set_hook("apisix/core/config_etcd.lua", 393, nil, function(info) 125 | local filter_res = "/routes" 126 | if info.vals.self.key:sub(-#filter_res) == filter_res and not info.vals.err then 127 | ngx.log(ngx.WARN, "etcd watch /routes response: ", cjson.encode(info.vals.dir_res)) 128 | return true 129 | end 130 | return false 131 | end) 132 | ``` 133 | 134 | **The breakpoint handler could be arbitrary lua code, so you could do anything there, not 135 | just printing logs.** 136 | 137 | **But, note that async code, e.g. cosocket, should not run in the breakpoint directly, 138 | because the execution of the breakpint handler function is synchronous and blocking, 139 | and you have no chance to return to the nginx event loop until completion.** 140 | 141 | **Instead, you could use a timer to postpone the async code, just like what I did above.** 142 | 143 | ## Caveats 144 | 145 | To setup breakpoint within jit compiled function, it needs to flush the jit cache first. 146 | 147 | Depending on the scope of jit cache flush, if you only flush the specific function cache, then it only 148 | slows down that function execution, otherwise, it would slow down the whole jit cache of lua vm. 149 | 150 | When the breakpoints are enabled, the lua vm could not trace new hot paths and compile them. 151 | 152 | But when the breakpoints disappear, the lua vm would recover jit tracing. 153 | 154 | Conservatively, this library is mainly useful for functional debug without stress. 155 | 156 | ## Example 157 | 158 | ### 1. example/nginx.conf 159 | 160 | ```nginx 161 | error_log /dev/stderr info; 162 | worker_processes auto; 163 | 164 | events {} 165 | 166 | http { 167 | lua_package_path '/opt/lua-resty-inspect/example/?.lua;/opt/lua-resty-inspect/lib/?.lua;/opt/lua-resty-inspect/lib/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/share/lua/5.1/?.lua;;'; 168 | lua_package_cpath '/usr/local/lib/lua/5.1/?.so;;'; 169 | 170 | init_worker_by_lua_block { 171 | local inspect = require "resty.inspect" 172 | inspect.init(1, "/opt/lua-resty-inspect/example/resty_inspect_hooks.lua") 173 | } 174 | 175 | server { 176 | listen 10000; 177 | 178 | location / { 179 | content_by_lua_block { 180 | local hot = require("hot") 181 | 182 | -- hook enabled, jit flush, slow 183 | hot.timeit(hot.foo, "foo") 184 | hot.run_bar() 185 | 186 | -- hook removed, tracing 187 | hot.timeit(hot.foo, "foo") 188 | hot.run_bar() 189 | 190 | -- jit, fast 191 | hot.timeit(hot.foo, "foo") 192 | hot.run_bar() 193 | 194 | ngx.say("ok") 195 | } 196 | } 197 | } 198 | } 199 | 200 | ``` 201 | 202 | ### 2. example/resty_inspect_hooks1.lua 203 | 204 | ```lua 205 | local dbg = require "resty.inspect.dbg" 206 | local cjson = require "cjson" 207 | local hot = require "hot" 208 | 209 | -- short file name 210 | -- foo is module function, so here we just flush jit cache of foo function 211 | dbg.set_hook("hot.lua", 9, hot.foo, function(info) 212 | if info.vals.i == 100 then 213 | ngx.log(ngx.INFO, debug.traceback("foo traceback", 3)) 214 | ngx.log(ngx.INFO, dbg.getname(info.finfo)) 215 | ngx.log(ngx.INFO, cjson.encode(info.vals)) 216 | ngx.log(ngx.INFO, cjson.encode(info.uv)) 217 | return true 218 | end 219 | return false 220 | end) 221 | 222 | ``` 223 | 224 | ### 3. example/resty_inspect_hooks2.lua 225 | 226 | ```lua 227 | local dbg = require "resty.inspect.dbg" 228 | local cjson = require "cjson" 229 | local hot = require "hot" 230 | 231 | -- short file name 232 | -- foo is module function, so here we just flush jit cache of foo function 233 | dbg.set_hook("hot.lua", 9, hot.foo, function(info) 234 | if info.vals.i == 100 then 235 | ngx.log(ngx.INFO, debug.traceback("foo traceback", 3)) 236 | ngx.log(ngx.INFO, dbg.getname(info.finfo)) 237 | ngx.log(ngx.INFO, cjson.encode(info.vals)) 238 | ngx.log(ngx.INFO, cjson.encode(info.uv)) 239 | return true 240 | end 241 | return false 242 | end) 243 | 244 | -- more specific file name 245 | -- bar is local function, so it have to flush the whole jit cache 246 | dbg.set_hook("example/hot.lua", 17, nil, function(info) 247 | if info.vals.i == 99 then 248 | ngx.log(ngx.INFO, debug.traceback("bar traceback", 3)) 249 | ngx.log(ngx.INFO, dbg.getname(info.finfo)) 250 | return true 251 | end 252 | return false 253 | end) 254 | 255 | ``` 256 | 257 | ### 4. test 258 | 259 | **Setup the test env:** 260 | 261 | ```bash 262 | luarocks install cjson socket lfs 263 | cd /opt 264 | git clone https://github.com/kingluo/lua-resty-inspect 265 | cd /opt/lua-resty-inspect 266 | /usr/local/openresty-debug/nginx/sbin/nginx -p /opt/lua-resty-inspect/example/ -c nginx.conf -g "daemon off;" 267 | ``` 268 | 269 | **Nginx log output explanation:** 270 | 271 | ```bash 272 | ### no breakpoints initially, because the breakpoints file resty_inspect_hooks.lua not exist 273 | 274 | ### setup foo breakpoint 275 | 276 | ln -sf /opt/lua-resty-inspect/example/resty_inspect_hooks1.lua /opt/lua-resty-inspect/example/resty_inspect_hooks.lua 277 | 278 | 2022/08/28 21:44:12 [info] 2688226#2688226: *31 [lua] init.lua:29: setup_hooks(): set hooks: err=nil, hooks=["hot.lua#9"], context: ngx.timer 279 | 280 | ### trigger the hot.lua execution 281 | 282 | curl localhost:10000/get 283 | 284 | ### breakpoint on foo() function triggered 285 | ### print related information around the breakpoint 286 | 287 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] resty_inspect_hooks.lua:9: foo traceback 288 | stack traceback: 289 | /opt/lua-resty-inspect/lib/resty/inspect/dbg.lua:50: in function '__index' 290 | /opt/lua-resty-inspect/example/hot.lua:9: in function 'func' 291 | /opt/lua-resty-inspect/example/hot.lua:24: in function 'timeit' 292 | content_by_lua(nginx.conf:35):5: in main chunk, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 293 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] resty_inspect_hooks.lua:10: /opt/lua-resty-inspect/example/hot.lua:9 (func), client: 127.0.0.1, server: , request: 294 | "GET /get HTTP/1.1", host: "localhost:10000" 295 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] resty_inspect_hooks.lua:11: {"i":100,"j":1}, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 296 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] resty_inspect_hooks.lua:12: {"s":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaall"}, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 297 | 298 | 299 | ### the breakpoint only affect the jit cache of foo, so you could see that the foo execution is slow, 300 | ### but the bar function is still fast 301 | 302 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] hot.lua:26: timeit(): timeit foo: 81.11 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "l 303 | ocalhost:10000" 304 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] hot.lua:26: timeit(): timeit bar: 0.12 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "lo 305 | calhost:10000" 306 | 307 | 308 | ### the breakpoint is setup to be one-shot, so you could see that the latter foo would redo the jit tracing, and becomes fast again 309 | 310 | 311 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] hot.lua:26: timeit(): timeit foo: 0.56 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 312 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] hot.lua:26: timeit(): timeit bar: 0.04 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 313 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] hot.lua:26: timeit(): timeit foo: 0.05 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 314 | 2022/08/28 21:44:28 [info] 2688226#2688226: *47 [lua] hot.lua:26: timeit(): timeit bar: 0.04 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 315 | 2022/08/28 21:44:28 [info] 2688226#2688226: *48 [lua] init.lua:57: alive hooks: {}, context: ngx.timer 316 | 2022/08/28 21:44:33 [info] 2688226#2688226: *53 [lua] init.lua:57: alive hooks: {}, context: ngx.timer 317 | 2022/08/28 21:44:38 [info] 2688226#2688226: *58 [lua] init.lua:57: alive hooks: {}, context: ngx.timer 318 | 2022/08/28 21:44:43 [info] 2688226#2688226: *63 [lua] init.lua:57: alive hooks: {}, context: ngx.timer 319 | 2022/08/28 21:44:48 [info] 2688226#2688226: *68 [lua] init.lua:57: alive hooks: {}, context: ngx.timer 320 | 321 | 322 | ## 323 | ## setup two breakpoints again 324 | ## 325 | 326 | ln -sf /opt/lua-resty-inspect/example/resty_inspect_hooks2.lua /opt/lua-resty-inspect/example/resty_inspect_hooks.lua 327 | 328 | ## 329 | ## breakpoints enabled 330 | ## 331 | 332 | 2022/08/28 21:44:49 [info] 2688226#2688226: *69 [lua] init.lua:29: setup_hooks(): set hooks: err=nil, hooks=["hot.lua#9","example\/hot.lua#17"], context: ngx.timer 333 | 334 | ## 335 | ## The bar breakpoint clears the whole jit cache, so you could see that it slows down everything 336 | ## 337 | 338 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] resty_inspect_hooks.lua:9: foo traceback 339 | stack traceback: 340 | /opt/lua-resty-inspect/lib/resty/inspect/dbg.lua:50: in function '__index' 341 | /opt/lua-resty-inspect/example/hot.lua:9: in function 'func' 342 | /opt/lua-resty-inspect/example/hot.lua:24: in function 'timeit' 343 | content_by_lua(nginx.conf:35):5: in main chunk, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 344 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] resty_inspect_hooks.lua:10: /opt/lua-resty-inspect/example/hot.lua:9 (func), client: 127.0.0.1, server: , request: 345 | "GET /get HTTP/1.1", host: "localhost:10000" 346 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] resty_inspect_hooks.lua:11: {"i":100,"j":1}, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "loc 347 | alhost:10000" 348 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] resty_inspect_hooks.lua:12: {"s":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaall"}, client: 12 349 | 7.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 350 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] hot.lua:26: timeit(): timeit foo: 68.49 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "l 351 | ocalhost:10000" 352 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] resty_inspect_hooks.lua:22: bar traceback 353 | stack traceback: 354 | /opt/lua-resty-inspect/lib/resty/inspect/dbg.lua:50: in function '__index' 355 | /opt/lua-resty-inspect/example/hot.lua:17: in function 'func' 356 | /opt/lua-resty-inspect/example/hot.lua:24: in function 'timeit' 357 | /opt/lua-resty-inspect/example/hot.lua:30: in function 'run_bar' 358 | content_by_lua(nginx.conf:35):6: in main chunk, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "localhost:10000" 359 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] resty_inspect_hooks.lua:23: /opt/lua-resty-inspect/example/hot.lua:17 (func), client: 127.0.0.1, server: , request 360 | : "GET /get HTTP/1.1", host: "localhost:10000" 361 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] hot.lua:26: timeit(): timeit bar: 63.60 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "l 362 | ocalhost:10000" 363 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] hot.lua:26: timeit(): timeit foo: 0.64 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "lo 364 | calhost:10000" 365 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] hot.lua:26: timeit(): timeit bar: 0.11 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "lo 366 | calhost:10000" 367 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] hot.lua:26: timeit(): timeit foo: 0.07 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "lo 368 | calhost:10000" 369 | 2022/08/28 21:45:03 [info] 2688226#2688226: *84 [lua] hot.lua:26: timeit(): timeit bar: 0.03 msecs, client: 127.0.0.1, server: , request: "GET /get HTTP/1.1", host: "lo 370 | calhost:10000" 371 | 2022/08/28 21:45:08 [info] 2688226#2688226: *89 [lua] init.lua:57: alive hooks: {}, context: ngx.timer 372 | 373 | ## 374 | ## remove hooks 375 | ## 376 | 377 | rm -f /opt/lua-resty-inspect/example/resty_inspect_hooks.lua 378 | 379 | ``` 380 | -------------------------------------------------------------------------------- /example/hot.lua: -------------------------------------------------------------------------------- 1 | local sock = require "socket" 2 | local s = string.rep("a", 64) .. "ll" 3 | 4 | local _M = {} 5 | 6 | function _M.foo() 7 | for i=1,100 do 8 | for j=1,100 do 9 | string.find(s, "ll", 1, true) 10 | end 11 | end 12 | end 13 | 14 | local function bar() 15 | for i=1,100 do 16 | for j=1,100 do 17 | string.find(s, "ll", 1, true) 18 | end 19 | end 20 | end 21 | 22 | function _M.timeit(func, name, ...) 23 | local t1 = sock.gettime() * 1000 24 | func(...) 25 | local t2 = sock.gettime() * 1000 26 | ngx.log(ngx.INFO, string.format("timeit %s: %.2f msecs", name, t2-t1)) 27 | end 28 | 29 | function _M.run_bar() 30 | _M.timeit(bar, "bar") 31 | end 32 | 33 | return _M 34 | -------------------------------------------------------------------------------- /example/logs/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kingluo/lua-resty-inspect/be2f1cdbb852ee18061799f6073f521f3a10ad82/example/logs/.gitkeep -------------------------------------------------------------------------------- /example/nginx.conf: -------------------------------------------------------------------------------- 1 | error_log /dev/stderr info; 2 | worker_processes auto; 3 | 4 | events {} 5 | 6 | http { 7 | lua_package_path '/opt/lua-resty-inspect/example/?.lua;/opt/lua-resty-inspect/lib/?.lua;/opt/lua-resty-inspect/lib/?/init.lua;/usr/local/lib/lua/5.1/?.lua;/usr/local/share/lua/5.1/?.lua;;'; 8 | lua_package_cpath '/usr/local/lib/lua/5.1/?.so;;'; 9 | 10 | init_worker_by_lua_block { 11 | local inspect = require "resty.inspect" 12 | inspect.init(1, "/opt/lua-resty-inspect/example/resty_inspect_hooks.lua") 13 | } 14 | 15 | server { 16 | listen 10000; 17 | 18 | location / { 19 | content_by_lua_block { 20 | local hot = require("hot") 21 | 22 | -- hook enabled, jit flush, slow 23 | hot.timeit(hot.foo, "foo") 24 | hot.run_bar() 25 | 26 | -- hook removed, tracing 27 | hot.timeit(hot.foo, "foo") 28 | hot.run_bar() 29 | 30 | -- jit, fast 31 | hot.timeit(hot.foo, "foo") 32 | hot.run_bar() 33 | 34 | ngx.say("ok") 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /example/resty_inspect_hooks1.lua: -------------------------------------------------------------------------------- 1 | local dbg = require "resty.inspect.dbg" 2 | local cjson = require "cjson" 3 | local hot = require "hot" 4 | 5 | -- short file name 6 | -- foo is module function, so here we just flush jit cache of foo function 7 | dbg.set_hook("hot.lua", 9, hot.foo, function(info) 8 | if info.vals.i == 100 then 9 | ngx.log(ngx.INFO, debug.traceback("foo traceback", 3)) 10 | ngx.log(ngx.INFO, dbg.getname(info.finfo)) 11 | ngx.log(ngx.INFO, cjson.encode(info.vals)) 12 | ngx.log(ngx.INFO, cjson.encode(info.uv)) 13 | return true 14 | end 15 | return false 16 | end) 17 | -------------------------------------------------------------------------------- /example/resty_inspect_hooks2.lua: -------------------------------------------------------------------------------- 1 | local dbg = require "resty.inspect.dbg" 2 | local cjson = require "cjson" 3 | local hot = require "hot" 4 | 5 | -- short file name 6 | -- foo is module function, so here we just flush jit cache of foo function 7 | dbg.set_hook("hot.lua", 9, hot.foo, function(info) 8 | if info.vals.i == 100 then 9 | ngx.log(ngx.INFO, debug.traceback("foo traceback", 3)) 10 | ngx.log(ngx.INFO, dbg.getname(info.finfo)) 11 | ngx.log(ngx.INFO, cjson.encode(info.vals)) 12 | ngx.log(ngx.INFO, cjson.encode(info.uv)) 13 | return true 14 | end 15 | return false 16 | end) 17 | 18 | -- more specific file name 19 | -- bar is local function, so it have to flush the whole jit cache 20 | dbg.set_hook("example/hot.lua", 17, nil, function(info) 21 | if info.vals.i == 99 then 22 | ngx.log(ngx.INFO, debug.traceback("bar traceback", 3)) 23 | ngx.log(ngx.INFO, dbg.getname(info.finfo)) 24 | return true 25 | end 26 | return false 27 | end) 28 | -------------------------------------------------------------------------------- /lib/resty/inspect/dbg.lua: -------------------------------------------------------------------------------- 1 | local _M = {} 2 | 3 | local hooks = {} 4 | 5 | function _M.getname(n) 6 | if n.what == "C" then 7 | return n.name 8 | end 9 | local lc = string.format("%s:%d", n.short_src, n.currentline) 10 | if n.what ~= "main" and n.namewhat ~= "" then 11 | return string.format("%s (%s)", lc, n.name) 12 | else 13 | return lc 14 | end 15 | end 16 | 17 | local function hook(_, arg) 18 | local level = 2 19 | local finfo = debug.getinfo(level, "nSlf") 20 | local key = finfo.source .. "#" .. arg 21 | 22 | local hooks2 = {} 23 | for _, hook in ipairs(hooks) do 24 | if key:sub(-#hook.key) == hook.key then 25 | local filter_func = hook.filter_func 26 | local info = {finfo = finfo, uv = {}, vals = {}} 27 | 28 | -- upvalues 29 | local i = 1 30 | while true do 31 | local name, value = debug.getupvalue(finfo.func, i) 32 | if name == nil then break end 33 | if string.sub(name, 1, 1) ~= "(" then 34 | info.uv[name] = value 35 | end 36 | i = i + 1 37 | end 38 | 39 | -- local values 40 | local i = 1 41 | while true do 42 | local name, value = debug.getlocal(level, i) 43 | if not name then break end 44 | if string.sub(name, 1, 1) ~= "(" then 45 | info.vals[name] = value 46 | end 47 | i = i + 1 48 | end 49 | 50 | local r1, r2_or_err = pcall(filter_func, info) 51 | if not r1 then 52 | ngx.log(ngx.ERR, r2_or_err) 53 | end 54 | 55 | -- if filter_func returns false, keep the hook 56 | if r1 and r2_or_err == false then 57 | table.insert(hooks2, hook) 58 | end 59 | else 60 | -- key not match, keep the hook 61 | table.insert(hooks2, hook) 62 | end 63 | end 64 | 65 | -- disable debug mode if all hooks done 66 | if #hooks2 ~= #hooks then 67 | hooks = hooks2 68 | if #hooks == 0 then 69 | debug.sethook() 70 | if jit then 71 | jit.on() 72 | end 73 | end 74 | end 75 | end 76 | 77 | function _M.set_hook(file, line, func, filter_func) 78 | if file == nil then 79 | file = "=stdin" 80 | end 81 | 82 | local key = file .. "#" .. line 83 | table.insert(hooks, {key = key, filter_func = filter_func}) 84 | 85 | if jit then 86 | jit.flush(func) 87 | jit.off() 88 | end 89 | 90 | debug.sethook(hook, "l") 91 | end 92 | 93 | function _M.unset_hook(file, line) 94 | if file == nil then 95 | file = "=stdin" 96 | end 97 | 98 | local hooks2 = {} 99 | 100 | local key = file .. "#" .. line 101 | for i, hook in ipairs(hooks) do 102 | if hook.key ~= key then 103 | table.insert(hooks2, hook) 104 | end 105 | end 106 | 107 | if #hooks2 ~= #hooks then 108 | hooks = hooks2 109 | if #hooks == 0 then 110 | debug.sethook() 111 | if jit then 112 | jit.on() 113 | end 114 | end 115 | end 116 | end 117 | 118 | function _M.unset_all() 119 | if #hooks > 0 then 120 | hooks = {} 121 | debug.sethook() 122 | if jit then 123 | jit.on() 124 | end 125 | end 126 | end 127 | 128 | function _M.hooks() 129 | return hooks 130 | end 131 | 132 | return _M 133 | -------------------------------------------------------------------------------- /lib/resty/inspect/init.lua: -------------------------------------------------------------------------------- 1 | local dbg = require 'resty.inspect.dbg' 2 | local lfs = require 'lfs' 3 | local cjson = require "cjson" 4 | 5 | local _M = {} 6 | 7 | local last_modified = 0 8 | 9 | local stop = false 10 | 11 | local running = false 12 | 13 | local last_report_time = 0 14 | 15 | local REPORT_INTERVAL = 30 -- secs 16 | 17 | local function is_file_exists(file) 18 | local f = io.open(file, "r") 19 | if f then io.close(f) return true else return false end 20 | end 21 | 22 | local function setup_hooks(file) 23 | if is_file_exists(file) then 24 | dbg.unset_all() 25 | local chunk = loadfile(file) 26 | local ok, err = pcall(chunk) 27 | local hooks = {} 28 | for _, hook in ipairs(dbg.hooks()) do 29 | table.insert(hooks, hook.key) 30 | end 31 | ngx.log(ngx.INFO, "set hooks: err=", err, ", hooks=", cjson.encode(hooks)) 32 | end 33 | end 34 | 35 | local function reload_hooks(premature, delay, file) 36 | if premature or stop then 37 | stop = false 38 | running = false 39 | return 40 | end 41 | 42 | local time, err = lfs.attributes(file, 'modification') 43 | if err then 44 | if last_modified ~= 0 then 45 | ngx.log(ngx.INFO, err, ", disable all hooks") 46 | dbg.unset_all() 47 | last_modified = 0 48 | end 49 | elseif time ~= last_modified then 50 | setup_hooks(file) 51 | last_modified = time 52 | else 53 | local ts = os.time() 54 | if ts - last_report_time >= REPORT_INTERVAL then 55 | local hooks = {} 56 | for _, hook in ipairs(dbg.hooks()) do 57 | table.insert(hooks, hook.key) 58 | end 59 | ngx.log(ngx.INFO, "alive hooks: ", cjson.encode(hooks)) 60 | last_report_time = ts 61 | end 62 | end 63 | 64 | local ok, err = ngx.timer.at(delay, reload_hooks, delay, file) 65 | if not ok then 66 | ngx.log(ngx.ERR, "failed to create the timer: ", err) 67 | running = false 68 | end 69 | end 70 | 71 | function _M.init(delay, file) 72 | if not running then 73 | file = file or "/var/run/resty_inspect_hooks.lua" 74 | delay = delay or 3 75 | 76 | setup_hooks(file) 77 | 78 | local ok, err = ngx.timer.at(delay, reload_hooks, delay, file) 79 | if not ok then 80 | ngx.log(ngx.ERR, "failed to create the timer: ", err) 81 | return 82 | end 83 | running = true 84 | end 85 | end 86 | 87 | function _M.destroy() 88 | stop = true 89 | end 90 | 91 | return _M 92 | --------------------------------------------------------------------------------