├── README.md ├── dist.ini ├── docs ├── query-page.png └── stats_simple_demo.conf ├── lib └── resty │ ├── mongol │ ├── bson.lua │ ├── colmt.lua │ ├── cursor.lua │ ├── dbmt.lua │ ├── get.lua │ ├── globalplus.lua │ ├── gridfs.lua │ ├── gridfs_file.lua │ ├── init.lua │ ├── ll.lua │ ├── misc.lua │ ├── object_id.lua │ └── orderedtable.lua │ └── stats │ ├── cache.lua │ ├── coll_util.lua │ ├── init.lua │ ├── json.lua │ ├── mongo_dao.lua │ ├── orderedtable.lua │ └── util.lua ├── t ├── nginx.conf ├── test.sh └── wrk_test.lua └── view ├── filter.lua ├── main.lua ├── mongo.lua ├── resty └── template.lua ├── stats.html └── stats_key.html /README.md: -------------------------------------------------------------------------------- 1 | Name 2 | ==== 3 | 4 | lua-resty-stats - is a statistical module for nginx base on ngx_lua, Statistical key and values are configurable, can use the nginx core's variables and this module's variables. The statistical result store in mongodb. 5 | 6 | Table of Contents 7 | ================= 8 | 9 | - [Name](#name) 10 | - [Table of Contents](#table-of-contents) 11 | - [Synopsis](#synopsis) 12 | - [Variables](#variables) 13 | - [Methods](#methods) 14 | - [add_def_stats](#add_def_stats) 15 | - [add_stats_config](#add_stats_config) 16 | - [init](#init) 17 | - [log](#log) 18 | - [Simple Query And API](#simple-query-and-api) 19 | - [Simple Demo](#simple-demo) 20 | - [Authors](#authors) 21 | - [Copyright and License](#copyright-and-license) 22 | 23 | Synopsis 24 | ======== 25 | ```nginx 26 | #set ngx_lua's environment variable: 27 | lua_package_path '/path/to/lua-resty-stats/lib/?.lua;/path/to/lua-resty-stats/lib/?/init.lua;/path/to/lua-resty-stats/view/?.lua;;'; 28 | # init the lua-resty-stats 29 | init_worker_by_lua ' 30 | local stats = require("resty.stats") 31 | -- add the default stats that named "stats_host" 32 | stats.add_def_stats() 33 | -- the general stats"s config 34 | local update = {["$inc"]= {count=1, ["hour_cnt.$hour"]=1, ["status.$status"]=1, 35 | ["req_time.all"]="$request_time", ["req_time.$hour"]="$request_time"}} 36 | 37 | -- stats by uri 38 | stats.add_stats_config("stats_uri", 39 | {selector={date="$date",key="$uri"}, update=update, 40 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 41 | 42 | -- stats by arg 43 | stats.add_stats_config("stats_arg", 44 | {selector={date="$date",key="$arg_client_type"}, update=update, 45 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 46 | 47 | -- stats by uri and args 48 | stats.add_stats_config("stats_uri_arg", 49 | {selector={date="$date",key="$uri?$arg_from"}, update=update, 50 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 51 | 52 | -- stats by http request header 53 | stats.add_stats_config("stats_header_in", 54 | {selector={date="$date",key="city:$http_city"}, update=update, 55 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 56 | 57 | -- stats by http response header 58 | stats.add_stats_config("stats_header_out", 59 | {selector={date="$date",key="cache:$sent_http_cache"}, update=update, 60 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 61 | 62 | local mongo_cfg = {host="192.168.1.201", port=27017, dbname="ngx_stats"} 63 | local flush_interval = 2 -- second 64 | local retry_interval = 0.2 -- second 65 | -- init stats and start flush timer. 66 | stats.init(mongo_cfg, flush_interval, retry_interval) 67 | '; 68 | server { 69 | listen 80; 70 | server_name localhost; 71 | 72 | location /byuri { 73 | echo "byuri: $uri"; 74 | log_by_lua ' 75 | local stats = require("resty.stats") 76 | stats.log("stats_uri") 77 | stats.log("stats_host") 78 | '; 79 | } 80 | 81 | location /byarg { 82 | echo_sleep 0.005; 83 | echo "login $args"; 84 | log_by_lua ' 85 | local stats = require("resty.stats") 86 | stats.log("stats_arg") 87 | '; 88 | } 89 | 90 | location /byarg/404 { 91 | request_stats statby_arg "clitype:$arg_client_type"; 92 | return 404; 93 | log_by_lua ' 94 | local stats = require("resty.stats") 95 | stats.log("stats_arg") 96 | '; 97 | } 98 | 99 | location /byuriarg { 100 | echo "$uri?$args"; 101 | log_by_lua ' 102 | local stats = require("resty.stats") 103 | stats.log("stats_uri_arg") 104 | '; 105 | } 106 | 107 | location /byhttpheaderin { 108 | echo "city: $http_city"; 109 | log_by_lua ' 110 | local stats = require("resty.stats") 111 | stats.log("stats_header_in") 112 | '; 113 | } 114 | 115 | location /byhttpheaderout/ { 116 | proxy_pass http://127.0.0.1:82; 117 | log_by_lua ' 118 | local stats = require("resty.stats") 119 | stats.log("stats_header_out") 120 | '; 121 | } 122 | } 123 | 124 | server { 125 | listen 82; 126 | server_name localhost; 127 | location /byhttpheaderout/hit { 128 | add_header cache hit; 129 | echo "cache: hit"; 130 | } 131 | location /byhttpheaderout/miss { 132 | add_header cache miss; 133 | echo "cache: miss"; 134 | } 135 | } 136 | 137 | server { 138 | listen 2000; 139 | server_name localhost; 140 | 141 | location /stats { 142 | set $template_root /path/to/lua-resty-stats/view; 143 | content_by_lua_file '/path/to/lua-resty-stats/view/main.lua'; 144 | } 145 | } 146 | ``` 147 | 148 | Variables 149 | ======= 150 | * nginx_core module supports variable: http://nginx.org/en/docs/http/ngx_http_core_module.html#variables 151 | * This module variables 152 | * date: current date in the format: 1970-09-28 153 | * time: current time in the format: 12:00:00 154 | * year: current year 155 | * month: current month 156 | * day: current date 157 | * hour: current hour 158 | * minute: current minute 159 | * second: current second 160 | 161 | Methods 162 | ======= 163 | To load this library, 164 | 165 | you need to specify this library's path in ngx_lua's lua_package_path directive. For example: 166 | ```nginx 167 | http { 168 | lua_package_path '/path/to/lua-resty-stats/lib/?.lua;/path/to/lua-resty-stats/lib/?/init.lua;/path/to/lua-resty-stats/view/?.lua;;'; 169 | } 170 | ``` 171 | 172 | you use require to load the library into a local Lua variable: 173 | ```lua 174 | local stats = require("resty.stats") 175 | ``` 176 | 177 | 178 | [Back to TOC](#table-of-contents) 179 | add_def_stats 180 | --- 181 | `syntax: stats.add_def_stats()` 182 | 183 | add the predefined stats configs that contains: 184 | ```lua 185 | stats_name: stats_host 186 | stats_config: 187 | { 188 | selector={date='$date',key='$host'}, 189 | update={['$inc']= {count=1, ['hour_cnt.$hour']=1, ['status.$status']=1, 190 | ['req_time.all']="$request_time", ['req_time.$hour']="$request_time"}}, 191 | indexes={ 192 | {keys={'date', 'key'}, options={unique=true}}, 193 | {keys={'key'}, options={}} 194 | }, 195 | } 196 | } 197 | ``` 198 | After this method is called, when you used stats.log(stats_name) method, you can use these predefined statistics. 199 | 200 | add_stats_config 201 | --- 202 | `syntax: stats.add_stats_config(stats_name, stats_config)` 203 | 204 | Add a custom statistical configuration item that contains stats_name and stats config. 205 | * `stats_name` is the name of the statistics, and also is the name of the mongodb's table. 206 | The name will be used when calling the `stats.log(stats_name)` method. 207 | * `stats_config` is used to define the values of statistics. 208 | `stats_config` is a table that contains some fileds: 209 | * `selector` a mongodb query statement. like: `{date="$date",key="$host"}` 210 | * `update` a mongodb update statement. like: `{["$inc"]= {count=1, ["hour_cnt.$hour"]=1, ["status.$status"]=1, 211 | ["req_time.all"]="$request_time", ["req_time.$hour"]="$request_time"}}` 212 | * `indexes` a table that contains all fields of the index. 213 | 214 | The `selector` and `update` configuration can use [variables](#variables).
215 | Note that "$inc" is not a nginx variable, it's a mongodb's operator. 216 | 217 | init 218 | --- 219 | `syntax: stats.init(mongo_cfg, flush_interval, retry_interval)` 220 | 221 | Initialization statistical library. 222 | * `mongo_cfg` The mongodb configuration, contains fields: 223 | * `host` mongodb's host 224 | * `port` mongodb's port 225 | * `dbname` mongodb's database name. 226 | * `flush_interval` flush data to the mongodb time interval, the time unit is seconds. 227 | * `retry_interval` the retry time interval on flush error,the time unit is seconds. 228 | 229 | 230 | log 231 | --- 232 | `syntax: stats.log(stats_name)` 233 | 234 | Collect the specified(by stats_name) statistical information at the log phrase.
235 | * `stats_name` is one statistical name that add by `stats.add_stats_config`.
236 | if the `stats_name` is nil, log method will collect all the statistics that have been configured. 237 | 238 | [Back to TOC](#table-of-contents) 239 | 240 | Simple Query And API 241 | ======= 242 | lua-resty-stats with a simple query page and API interface, which can be used in the following steps: 243 | * add location configuration to nginx.conf 244 | 245 | ```nginx 246 | location /stats { 247 | set $template_root /path/to/lua-resty-stats/view; 248 | content_by_lua_file '/path/to/lua-resty-stats/view/main.lua'; 249 | } 250 | ``` 251 | 252 | * Access query page. eg. `http://192.168.1.xxx/stats`: 253 | 254 | ![docs/query-page.png](docs/query-page.png "The Simple Query") 255 | 256 | * Access API: 257 | 258 | ```curl 259 | # by date 260 | curl http://127.0.0.1:8020/stats/api?table=stats_uri&date=2020-02-20&limit=100 261 | # by date, today 262 | curl http://127.0.0.1:8020/stats/api?table=stats_uri&date=today&limit=10 263 | 264 | # by key(The date parameter is ignored.) 265 | curl http://127.0.0.1:8020/stats/api?table=stats_uri&key=/path/to/uri 266 | ``` 267 | 268 | * The API response will look something like this: 269 | 270 | ```json 271 | { 272 | "stats": [ 273 | { 274 | "hour_cnt": { 275 | "19": 24 276 | }, 277 | "count": 24, 278 | "status": { 279 | "200": 24 280 | }, 281 | "total": 24, 282 | "req_time": { 283 | "19": 13.262, 284 | "all": 13.262 285 | }, 286 | "percent": 100, 287 | "key": "/path/to/uri", 288 | "date": "2020-09-24" 289 | } 290 | ] 291 | } 292 | ``` 293 | 294 | *If you've configured some other fields in your update, this will be different* 295 | 296 | Simple Demo 297 | ======== 298 | [Simple Stats demo](docs/stats_simple_demo.conf "Simple Stats demo") 299 | 300 | You can include it in nginx.conf using the include directive. Such as: 301 | `include /path/to/simple_stats.conf;` 302 | 303 | Authors 304 | ======= 305 | 306 | jie123108 。 307 | 308 | [Back to TOC](#table-of-contents) 309 | 310 | Copyright and License 311 | ===================== 312 | 313 | This module is licensed under the BSD license. 314 | 315 | Copyright (C) 2020, by jie123108 316 | 317 | All rights reserved. 318 | 319 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 320 | 321 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 322 | 323 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 324 | 325 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 326 | 327 | [Back to TOC](#table-of-contents) 328 | 329 | -------------------------------------------------------------------------------- /dist.ini: -------------------------------------------------------------------------------- 1 | name=lua-resty-stats 2 | abstract=A statistical module for nginx base on ngx_lua, Statistical key and values are configurable, can use the nginx core's variables and this module's variables. The statistical result store in mongodb.(github.com/jie123108/lua-resty-stats) 3 | version=1.0.3 4 | author=jie123108@163.com 5 | is_original=yes 6 | license=2bsd 7 | lib_dir=lib 8 | doc_dir=lib 9 | repo_link=https://github.com/jie123108/lua-resty-stats 10 | main_module=lib/resty/stats/init.lua 11 | requires = openresty >= 1.9.3.1 -------------------------------------------------------------------------------- /docs/query-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jie123108/lua-resty-stats/b9ae6f2ff56155e4eeddd46b90575ba69f65e621/docs/query-page.png -------------------------------------------------------------------------------- /docs/stats_simple_demo.conf: -------------------------------------------------------------------------------- 1 | 2 | lua_package_path '/path/to/lua-resty-stats/lib/?.lua;/path/to/lua-resty-stats/lib/?/init.lua;/path/to/lua-resty-stats/view/?.lua;;'; 3 | # stats config 4 | init_worker_by_lua_block { 5 | require("resty.core") 6 | local stats = require("resty.stats") 7 | 8 | -- add the default stats that named "stats_host" 9 | stats.add_def_stats() 10 | -- the general stats"s config 11 | local update = {["$inc"]= {count=1, ["hour_cnt.$hour"]=1, ["status.$status"]=1, 12 | ["req_time.all"]="$request_time", ["req_time.$hour"]="$request_time"}} 13 | 14 | -- stats by uri 15 | stats.add_stats_config("stats_uri", 16 | {selector={date="$date",key="$host:$uri"}, update=update,index_keys={"date", "key"}}) 17 | 18 | local mongo_cfg = {host="127.0.0.1", port=27017, dbname="ngx_stats"} 19 | local flush_interval = 5 -- second 20 | local retry_interval = 0.2 -- second 21 | -- init stats and start flush timer. 22 | stats.init(mongo_cfg, flush_interval, retry_interval) 23 | } 24 | 25 | log_by_lua_block { 26 | local stats = require("resty.stats") 27 | stats.log("stats_uri") 28 | stats.log("stats_host") 29 | } 30 | server { 31 | listen 2000; 32 | server_name localhost; 33 | 34 | location /stats { 35 | set $template_root /path/to/lua-resty-stats/view; 36 | content_by_lua_file '/path/to/lua-resty-stats/view/main.lua'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/resty/mongol/bson.lua: -------------------------------------------------------------------------------- 1 | local mod_name = (...):match ( "^(.*)%..-$" ) 2 | 3 | local assert , error = assert , error 4 | local pairs = pairs 5 | local getmetatable = getmetatable 6 | local type = type 7 | local tonumber , tostring = tonumber , tostring 8 | local t_insert = table.insert 9 | local t_concat = table.concat 10 | local strformat = string.format 11 | local strmatch = string.match 12 | local strbyte = string.byte 13 | local floor = math.floor 14 | 15 | local ll = require ( mod_name .. ".ll" ) 16 | local le_uint_to_num = ll.le_uint_to_num 17 | local le_int_to_num = ll.le_int_to_num 18 | local num_to_le_uint = ll.num_to_le_uint 19 | local num_to_le_int = ll.num_to_le_int 20 | local from_double = ll.from_double 21 | local to_double = ll.to_double 22 | 23 | local getlib = require ( mod_name .. ".get" ) 24 | local read_terminated_string = getlib.read_terminated_string 25 | 26 | local obid = require ( mod_name .. ".object_id" ) 27 | local new_object_id = obid.new 28 | local object_id_mt = obid.metatable 29 | local binary_mt = {} 30 | local utc_date = {} 31 | 32 | 33 | local function read_document ( get , numerical ) 34 | local bytes = le_uint_to_num ( get ( 4 ) ) 35 | 36 | local ho , hk , hv = false , false , false 37 | local t = { } 38 | while true do 39 | local op = get ( 1 ) 40 | if op == "\0" then break end 41 | 42 | local e_name = read_terminated_string ( get ) 43 | local v 44 | if op == "\1" then -- Double 45 | v = from_double ( get ( 8 ) ) 46 | elseif op == "\2" then -- String 47 | local len = le_uint_to_num ( get ( 4 ) ) 48 | v = get ( len - 1 ) 49 | assert ( get ( 1 ) == "\0" ) 50 | elseif op == "\3" then -- Embedded document 51 | v = read_document ( get , false ) 52 | elseif op == "\4" then -- Array 53 | v = read_document ( get , true ) 54 | elseif op == "\5" then -- Binary 55 | local len = le_uint_to_num ( get ( 4 ) ) 56 | local subtype = get ( 1 ) 57 | v = get ( len ) 58 | elseif op == "\6" then -- undefined 59 | v = nil 60 | elseif op == "\7" then -- ObjectId 61 | v = new_object_id ( get ( 12 ) ) 62 | elseif op == "\8" then -- false 63 | local f = get ( 1 ) 64 | if f == "\0" then 65 | v = false 66 | elseif f == "\1" then 67 | v = true 68 | else 69 | error ( f:byte ( ) ) 70 | end 71 | elseif op == "\9" then -- UTC datetime milliseconds 72 | v = le_uint_to_num ( get ( 8 ) , 1 , 8 ) 73 | elseif op == "\10" then -- Null 74 | v = nil 75 | elseif op == "\16" then --int32 76 | v = le_int_to_num ( get ( 4 ) , 1 , 8 ) 77 | elseif op == "\17" then --int64 78 | v = le_int_to_num(get(8), 1, 8) 79 | elseif op == "\18" then --int64 80 | v = le_int_to_num(get(8), 1, 8) 81 | else 82 | error ( "Unknown BSON type: " .. strbyte ( op ) ) 83 | end 84 | 85 | if numerical then 86 | t [ tonumber ( e_name ) + 1] = v 87 | else 88 | t [ e_name ] = v 89 | end 90 | 91 | -- Check for special universal map 92 | if e_name == "_keys" then 93 | hk = v 94 | elseif e_name == "_vals" then 95 | hv = v 96 | else 97 | ho = true 98 | end 99 | end 100 | 101 | if not ho and hk and hv then 102 | t = { } 103 | for i=1,#hk do 104 | t [ hk [ i ] ] = hv [ i ] 105 | end 106 | end 107 | 108 | return t 109 | end 110 | 111 | local function get_utc_date(v) 112 | return setmetatable({v = v}, utc_date) 113 | end 114 | 115 | local function get_bin_data(v) 116 | return setmetatable({v = v, st = "\0"}, binary_mt) 117 | end 118 | 119 | local function from_bson ( get ) 120 | local t = read_document ( get , false ) 121 | return t 122 | end 123 | 124 | local to_bson 125 | local function pack ( k , v ) 126 | local ot = type ( v ) 127 | local mt = getmetatable ( v ) 128 | 129 | if ot == "number" then 130 | if floor(v) == v then 131 | if v >= -2^31 and v <= 2^31-1 then --int32 132 | return "\16" .. k .. "\0" .. num_to_le_int ( v ) 133 | else --int64 134 | return "\18" .. k .. "\0" .. num_to_le_int ( v, 8 ) 135 | end 136 | else 137 | return "\1" .. k .. "\0" .. to_double ( v ) 138 | end 139 | elseif ot == "nil" then 140 | return "\10" .. k .. "\0" 141 | elseif ot == "string" then 142 | return "\2" .. k .. "\0" .. num_to_le_uint ( #v + 1 ) .. v .. "\0" 143 | elseif ot == "boolean" then 144 | if v == false then 145 | return "\8" .. k .. "\0\0" 146 | else 147 | return "\8" .. k .. "\0\1" 148 | end 149 | elseif mt == object_id_mt then 150 | return "\7" .. k .. "\0" .. v.id 151 | elseif mt == utc_date then 152 | return "\9" .. k .. "\0" .. num_to_le_int(v.v, 8) 153 | elseif mt == binary_mt then 154 | return "\5" .. k .. "\0" .. num_to_le_uint(string.len(v.v)) .. 155 | v.st .. v.v 156 | elseif ot == "table" then 157 | local doc , array = to_bson(v) 158 | if array then 159 | return "\4" .. k .. "\0" .. doc 160 | else 161 | return "\3" .. k .. "\0" .. doc 162 | end 163 | elseif ot == "userdata" and tostring(v) == "userdata: NULL" then 164 | return "\10" .. k .. "\0" 165 | else 166 | error ( "Failure converting " .. ot ..": " .. tostring ( v ) ) 167 | end 168 | end 169 | 170 | function to_bson(ob) 171 | -- Find out if ob if an array; string->value map; or general table 172 | local onlyarray = true 173 | local seen_n , high_n = { } , 0 174 | local onlystring = true 175 | for k , v in pairs ( ob ) do 176 | local t_k = type ( k ) 177 | onlystring = onlystring and ( t_k == "string" ) 178 | if onlyarray then 179 | if t_k == "number" and k >= 0 then 180 | if k >= high_n then 181 | high_n = k 182 | end 183 | seen_n [ k ] = v 184 | else 185 | onlyarray = false 186 | end 187 | end 188 | if not onlyarray and not onlystring then break end 189 | end 190 | 191 | local retarray , m = false 192 | if onlyarray then 193 | local r = { } 194 | 195 | local low = 1 196 | --if seen_n [ 0 ] then low = 0 end 197 | for i=low , high_n do 198 | r [ i ] = pack ( i - 1 , seen_n [ i ] ) 199 | end 200 | 201 | m = t_concat ( r , "" , low , high_n ) 202 | retarray = true 203 | elseif onlystring then -- Do string first so the case of an empty table is done properly 204 | local r = { } 205 | for k , v in pairs ( ob ) do 206 | t_insert ( r , pack ( k , v ) ) 207 | end 208 | m = t_concat ( r ) 209 | else 210 | local ni = 1 211 | local keys , vals = { } , { } 212 | for k , v in pairs ( ob ) do 213 | keys [ ni ] = k 214 | vals [ ni ] = v 215 | ni = ni + 1 216 | end 217 | return to_bson ( { _keys = keys , _vals = vals } ) 218 | end 219 | 220 | return num_to_le_uint ( #m + 4 + 1 ) .. m .. "\0" , retarray 221 | end 222 | 223 | return { 224 | from_bson = from_bson ; 225 | to_bson = to_bson ; 226 | get_bin_data = get_bin_data; 227 | get_utc_date = get_utc_date; 228 | } 229 | -------------------------------------------------------------------------------- /lib/resty/mongol/colmt.lua: -------------------------------------------------------------------------------- 1 | local mod_name = (...):match ( "^(.*)%..-$" ) 2 | 3 | local misc = require ( mod_name .. ".misc" ) 4 | 5 | local assert , pcall = assert , pcall 6 | local ipairs , pairs = ipairs , pairs 7 | local t_insert , t_concat = table.insert , table.concat 8 | 9 | local attachpairs_start = misc.attachpairs_start 10 | 11 | local ll = require ( mod_name .. ".ll" ) 12 | local num_to_le_uint = ll.num_to_le_uint 13 | local num_to_le_int = ll.num_to_le_int 14 | local le_uint_to_num = ll.le_uint_to_num 15 | local le_bpeek = ll.le_bpeek 16 | 17 | local getlib = require ( mod_name .. ".get" ) 18 | local get_from_string = getlib.get_from_string 19 | 20 | local bson = require ( mod_name .. ".bson" ) 21 | local from_bson = bson.from_bson 22 | local to_bson = bson.to_bson 23 | 24 | local new_cursor = require ( mod_name .. ".cursor" ) 25 | 26 | local colmethods = { } 27 | local colmt = { __index = colmethods } 28 | 29 | 30 | local opcodes = { 31 | REPLY = 1 ; 32 | MSG = 1000 ; 33 | UPDATE = 2001 ; 34 | INSERT = 2002 ; 35 | QUERY = 2004 ; 36 | GET_MORE = 2005 ; 37 | DELETE = 2006 ; 38 | KILL_CURSORS = 2007 ; 39 | } 40 | 41 | local function bool2int(val) 42 | if type(val) == 'boolean' then 43 | return (val and 1 or 0) 44 | end 45 | return val 46 | end 47 | 48 | local function compose_msg ( requestID , reponseTo , opcode , message ) 49 | return num_to_le_uint ( #message + 16 ) .. requestID .. reponseTo .. opcode .. message 50 | end 51 | 52 | local function full_collection_name ( self , collection ) 53 | local db = assert ( self.db , "Not current in a database" ) 54 | return db .. "." .. collection .. "\0" 55 | end 56 | 57 | local id = 0 58 | local function docmd ( conn , opcode , message , reponseTo ) 59 | id = id + 1 60 | local req_id = id 61 | local requestID = num_to_le_uint ( req_id ) 62 | reponseTo = reponseTo or "\255\255\255\255" 63 | opcode = num_to_le_uint ( assert ( opcodes [ opcode ] ) ) 64 | 65 | local m = compose_msg ( requestID , reponseTo , opcode , message ) 66 | local sent = assert ( conn.sock:send ( m ) ) 67 | return req_id , sent 68 | end 69 | 70 | local function read_msg_header ( sock ) 71 | local header = assert ( sock:receive ( 16 ) ) 72 | 73 | local length = le_uint_to_num ( header , 1 , 4 ) 74 | local requestID = le_uint_to_num ( header , 5 , 8 ) 75 | local reponseTo = le_uint_to_num ( header , 9 , 12 ) 76 | local opcode = le_uint_to_num ( header , 13 , 16 ) 77 | 78 | return length , requestID , reponseTo , opcode 79 | end 80 | 81 | local function handle_reply ( conn , req_id , offset_i ) 82 | offset_i = offset_i or 0 83 | 84 | local r_len , r_req_id , r_res_id , opcode = read_msg_header ( conn.sock ) 85 | assert ( req_id == r_res_id ) 86 | assert ( opcode == opcodes.REPLY ) 87 | local data = assert ( conn.sock:receive ( r_len - 16 ) ) 88 | local get = get_from_string ( data ) 89 | 90 | local responseFlags = get ( 4 ) 91 | local cursorid = get ( 8 ) 92 | 93 | local t = { } 94 | t.startingFrom = le_uint_to_num ( get ( 4 ) ) 95 | t.numberReturned = le_uint_to_num ( get ( 4 ) ) 96 | t.CursorNotFound = le_bpeek ( responseFlags , 0 ) 97 | t.QueryFailure = le_bpeek ( responseFlags , 1 ) 98 | t.ShardConfigStale = le_bpeek ( responseFlags , 2 ) 99 | t.AwaitCapable = le_bpeek ( responseFlags , 3 ) 100 | 101 | local r = { } 102 | for i = 1 , t.numberReturned do 103 | r[i] = from_bson(get) 104 | end 105 | 106 | return cursorid, r, t 107 | end 108 | 109 | function colmethods:insert(docs, continue_on_error, safe) 110 | if #docs < 1 then 111 | return nil, "docs needed" 112 | end 113 | 114 | safe = bool2int(safe) or 0 115 | continue_on_error = bool2int(continue_on_error) or 0 116 | local flags = 2^0*continue_on_error 117 | 118 | local t = { } 119 | for i , v in ipairs(docs) do 120 | t[i] = to_bson(v) 121 | end 122 | 123 | local m = num_to_le_uint(flags)..full_collection_name(self, self.col) 124 | ..t_concat(t) 125 | local id, send = docmd(self.conn, "INSERT", m) 126 | if send == 0 then 127 | return nil, "send message failed" 128 | end 129 | 130 | if safe ~= 0 then 131 | local r, err = self.db_obj:cmd({getlasterror=1}) 132 | if not r then 133 | return nil, err 134 | end 135 | 136 | if r["err"] then 137 | return nil, r["err"] 138 | else 139 | return r["n"] 140 | end 141 | else 142 | return -1 end 143 | end 144 | 145 | function colmethods:update(selector, update, upsert, multiupdate, safe) 146 | safe = bool2int(safe) or 0 147 | upsert = bool2int(upsert) or 0 148 | multiupdate = bool2int(multiupdate) or 0 149 | local flags = 2^0*upsert + 2^1*multiupdate 150 | 151 | selector = to_bson(selector) 152 | update = to_bson(update) 153 | 154 | local m = "\0\0\0\0" .. full_collection_name(self, self.col) 155 | .. num_to_le_uint ( flags ) .. selector .. update 156 | local id, send = docmd(self.conn, "UPDATE", m) 157 | if send == 0 then 158 | return nil, "send message failed" 159 | end 160 | 161 | if safe ~= 0 then 162 | local r, err = self.db_obj:cmd({getlasterror=1}) 163 | if not r then 164 | return nil, err 165 | end 166 | 167 | if r["err"] then 168 | return nil, r["err"] 169 | else 170 | return r["n"] 171 | end 172 | else return -1 end 173 | end 174 | 175 | function colmethods:delete(selector, single_remove, safe) 176 | safe = bool2int(safe) or 0 177 | single_remove = bool2int(single_remove) or 0 178 | local flags = 2^0*single_remove 179 | 180 | selector = to_bson(selector) 181 | 182 | local m = "\0\0\0\0" .. full_collection_name(self, self.col) 183 | .. num_to_le_uint(flags) .. selector 184 | 185 | local id, sent = docmd(self.conn, "DELETE", m) 186 | if sent == 0 then 187 | return nil, "send message failed" 188 | end 189 | 190 | if safe ~= 0 then 191 | local r, err = self.db_obj:cmd({getlasterror=1}) 192 | if not r then 193 | return nil, err 194 | end 195 | 196 | if r["err"] then 197 | return nil, r["err"] 198 | else 199 | return r["n"] 200 | end 201 | else return -1 end 202 | end 203 | 204 | function colmethods:kill_cursors(cursorIDs) 205 | local n = #cursorIDs 206 | cursorIDs = t_concat(cursorIDs) 207 | 208 | local m = "\0\0\0\0" .. full_collection_name(self, self.col) 209 | .. num_to_le_uint(n) .. cursorIDs 210 | 211 | return docmd(self.conn, "KILL_CURSORS", m ) 212 | end 213 | 214 | function colmethods:query(query, returnfields, numberToSkip, numberToReturn, options) 215 | numberToSkip = numberToSkip or 0 216 | 217 | local flags = 0 218 | if options then 219 | flags = 2^1*( options.TailableCursor and 1 or 0 ) 220 | + 2^2*( options.SlaveOk and 1 or 0 ) 221 | + 2^3*( options.OplogReplay and 1 or 0 ) 222 | + 2^4*( options.NoCursorTimeout and 1 or 0 ) 223 | + 2^5*( options.AwaitData and 1 or 0 ) 224 | + 2^6*( options.Exhaust and 1 or 0 ) 225 | + 2^7*( options.Partial and 1 or 0 ) 226 | end 227 | 228 | query = to_bson(query) 229 | if returnfields then 230 | returnfields = to_bson(returnfields) 231 | else 232 | returnfields = "" 233 | end 234 | 235 | local m = num_to_le_uint(flags) .. full_collection_name(self, self.col) 236 | .. num_to_le_uint(numberToSkip) .. num_to_le_int(numberToReturn or -1 ) 237 | .. query .. returnfields 238 | 239 | local req_id = docmd(self.conn, "QUERY", m) 240 | return handle_reply(self.conn, req_id) 241 | end 242 | 243 | function colmethods:getmore(cursorID, numberToReturn) 244 | local m = "\0\0\0\0" .. full_collection_name(self, self.col) 245 | .. num_to_le_int(numberToReturn or 0) .. cursorID 246 | 247 | local req_id = docmd(self.conn, "GET_MORE" , m) 248 | return handle_reply(self.conn, req_id) 249 | end 250 | 251 | function colmethods:count(query) 252 | local r, err = self.db_obj:cmd(attachpairs_start({ 253 | count = self.col; 254 | query = query or { } ; 255 | } , "count" ) ) 256 | 257 | if not r then 258 | return nil, err 259 | end 260 | return r.n 261 | end 262 | 263 | function colmethods:drop() 264 | local r, err = self.db_obj:cmd({drop = self.col}) 265 | if not r then 266 | return nil, err 267 | end 268 | return 1 269 | end 270 | 271 | function colmethods:find(query, returnfields, num_each_query) 272 | num_each_query = num_each_query or 100 273 | return new_cursor(self, query, returnfields, num_each_query) 274 | end 275 | 276 | function colmethods:find_one(query, returnfields) 277 | local id, results, t = self:query(query, returnfields, 0, 1) 278 | if id == "\0\0\0\0\0\0\0\0" and results[1] then 279 | return results[1] 280 | end 281 | return nil 282 | end 283 | 284 | return colmt 285 | -------------------------------------------------------------------------------- /lib/resty/mongol/cursor.lua: -------------------------------------------------------------------------------- 1 | local t_insert = table.insert 2 | local t_remove = table.remove 3 | local t_concat = table.concat 4 | local strbyte = string.byte 5 | local strformat = string.format 6 | 7 | 8 | local cursor_methods = { } 9 | local cursor_mt = { __index = cursor_methods } 10 | 11 | local function new_cursor(col, query, returnfields, num_each_query) 12 | return setmetatable ( { 13 | col = col ; 14 | query = { ['$query'] = query} ; 15 | returnfields = returnfields ; 16 | 17 | id = false ; 18 | results = { } ; 19 | 20 | done = false ; 21 | i = 0; 22 | limit_n = 0; 23 | skip_n = 0; 24 | num_each = num_each_query or 100, 25 | } , cursor_mt ) 26 | end 27 | 28 | cursor_mt.__gc = function( self ) 29 | self.col:kill_cursors({ self.id }) 30 | end 31 | 32 | cursor_mt.__tostring = function ( ob ) 33 | local t = { } 34 | for i = 1 , 8 do 35 | t_insert(t, strformat("%02x", strbyte(ob.id, i, i))) 36 | end 37 | return "CursorId(" .. t_concat ( t ) .. ")" 38 | end 39 | 40 | function cursor_methods:limit(n) 41 | assert(n) 42 | self.limit_n = n 43 | end 44 | function cursor_methods:skip(n) 45 | assert(n) 46 | self.skip_n = n 47 | end 48 | 49 | --todo 50 | --function cursor_methods:skip(n) 51 | 52 | function cursor_methods:sort(fields) 53 | self.query["$orderby"] = fields 54 | return self 55 | end 56 | 57 | function cursor_methods:next() 58 | if self.limit_n > 0 and self.i >= self.limit_n then return nil end 59 | local idx = self.i - math.floor(self.i/self.num_each)*self.num_each 60 | local v = self.results [ idx + 1 ] 61 | if v ~= nil then 62 | self.i = self.i + 1 63 | self.results [ idx+1 ] = nil 64 | return self.i , v 65 | end 66 | 67 | if self.done then return nil end 68 | 69 | local t 70 | if not self.id then 71 | -- ngx.log(ngx.ERR, "--------- query -------- self.i:", self.i, " num_each:", self.num_each) 72 | self.id, self.results, t = self.col:query(self.query, 73 | self.returnfields, self.skip_n or 0, self.num_each) 74 | if self.id == "\0\0\0\0\0\0\0\0" then 75 | self.done = true 76 | end 77 | else 78 | -- ngx.log(ngx.ERR, "--------- getmore -------- self.num_each:", self.num_each, ", skip:", self.skip_n) 79 | self.id, self.results, t = self.col:getmore(self.id, self.num_each) 80 | 81 | if self.id == "\0\0\0\0\0\0\0\0" then 82 | self.done = true 83 | elseif t.CursorNotFound then 84 | self.id = false 85 | end 86 | end 87 | return self:next() 88 | end 89 | 90 | function cursor_methods:pairs( ) 91 | return self.next, self 92 | end 93 | 94 | return new_cursor 95 | -------------------------------------------------------------------------------- /lib/resty/mongol/dbmt.lua: -------------------------------------------------------------------------------- 1 | local mod_name = (...):match ( "^(.*)%..-$" ) 2 | 3 | local misc = require ( mod_name .. ".misc" ) 4 | local attachpairs_start = misc.attachpairs_start 5 | 6 | local setmetatable = setmetatable 7 | local pcall = pcall 8 | 9 | local colmt = require ( mod_name .. ".colmt" ) 10 | local gridfs = require ( mod_name .. ".gridfs" ) 11 | 12 | local dbmethods = { } 13 | local dbmt = { __index = dbmethods } 14 | 15 | function dbmethods:cmd(q) 16 | local collection = "$cmd" 17 | local col = self:get_col(collection) 18 | 19 | local c_id , r , t = col:query(q) 20 | 21 | if t.QueryFailure then 22 | return nil, "Query Failure" 23 | elseif not r[1] then 24 | return nil, "No results returned" 25 | elseif r[1].ok == 0 then -- Failure 26 | return nil , r[1].errmsg , r[1] , t 27 | else 28 | return r[1] 29 | end 30 | end 31 | 32 | function dbmethods:listcollections ( ) 33 | local col = self:get_col("system.namespaces") 34 | return col:find( { } ) 35 | end 36 | 37 | function dbmethods:dropDatabase ( ) 38 | local r, err = self:cmd({ dropDatabase = true }) 39 | if not r then 40 | return nil, err 41 | end 42 | return 1 43 | end 44 | 45 | local function pass_digest ( username , password ) 46 | return ngx.md5(username .. ":mongo:" .. password) 47 | end 48 | 49 | -- XOR two byte strings together 50 | local function xor_bytestr( a, b ) 51 | local res = "" 52 | for i=1,#a do 53 | res = res .. string.char(bit.bxor(string.byte(a,i,i), string.byte(b, i, i))) 54 | end 55 | return res 56 | end 57 | 58 | -- A simple implementation of PBKDF2_HMAC_SHA1 59 | local function pbkdf2_hmac_sha1( pbkdf2_key, iterations, salt, len ) 60 | local u1 = ngx.hmac_sha1(pbkdf2_key, salt .. string.char(0) .. string.char(0) .. string.char(0) .. string.char(1)) 61 | local ui = u1 62 | for i=1,iterations-1 do 63 | u1 = ngx.hmac_sha1(pbkdf2_key, u1) 64 | ui = xor_bytestr(ui, u1) 65 | end 66 | if #ui < len then 67 | for i=1,len-(#ui) do 68 | ui = string.char(0) .. ui 69 | end 70 | end 71 | return ui 72 | end 73 | 74 | function dbmethods:add_user ( username , password ) 75 | local digest = pass_digest ( username , password ) 76 | return self:update ( "system.users" , { user = username } , { ["$set"] = { pwd = password } } , true ) 77 | end 78 | 79 | function dbmethods:auth(username, password) 80 | local r, err = self:cmd({ getnonce = true }) 81 | if not r then 82 | return nil, err 83 | end 84 | 85 | local digest = ngx.md5( r.nonce .. username .. pass_digest ( username , password ) ) 86 | 87 | r, err = self:cmd(attachpairs_start({ 88 | authenticate = true ; 89 | user = username ; 90 | nonce = r.nonce ; 91 | key = digest ; 92 | } , "authenticate" ) ) 93 | if not r then 94 | return nil, err 95 | end 96 | return 1 97 | end 98 | 99 | function dbmethods:auth_scram_sha1(username, password) 100 | local user = string.gsub(string.gsub(username, '=', '=3D'), ',' , '=2C') 101 | local nonce = ngx.encode_base64(string.sub(tostring(math.random()), 3 , 14)) 102 | local first_bare = "n=" .. user .. ",r=" .. nonce 103 | local sasl_start_payload = ngx.encode_base64("n,," .. first_bare) 104 | 105 | r, err = self:cmd(attachpairs_start({ 106 | saslStart = 1 ; 107 | mechanism = "SCRAM-SHA-1" ; 108 | autoAuthorize = 1 ; 109 | payload = sasl_start_payload ; 110 | } , "saslStart" ) ) 111 | if not r then 112 | return nil, err 113 | end 114 | 115 | local conversationId = r['conversationId'] 116 | local server_first = r['payload'] 117 | local parsed_s = ngx.decode_base64(server_first) 118 | local parsed_t = {} 119 | for k, v in string.gmatch(parsed_s, "(%w+)=([^,]*)") do 120 | parsed_t[k] = v 121 | end 122 | local iterations = tonumber(parsed_t['i']) 123 | local salt = parsed_t['s'] 124 | local rnonce = parsed_t['r'] 125 | 126 | if not string.sub(rnonce, 1, 12) == nonce then 127 | return nil, 'Server returned an invalid nonce.' 128 | end 129 | local without_proof = "c=biws,r=" .. rnonce 130 | local pbkdf2_key = pass_digest ( username , password ) 131 | local salted_pass = pbkdf2_hmac_sha1(pbkdf2_key, iterations, ngx.decode_base64(salt), 20) 132 | local client_key = ngx.hmac_sha1(salted_pass, "Client Key") 133 | local stored_key = ngx.sha1_bin(client_key) 134 | local auth_msg = first_bare .. ',' .. parsed_s .. ',' .. without_proof 135 | local client_sig = ngx.hmac_sha1(stored_key, auth_msg) 136 | local client_key_xor_sig = xor_bytestr(client_key, client_sig) 137 | local client_proof = "p=" .. ngx.encode_base64(client_key_xor_sig) 138 | local client_final = ngx.encode_base64(without_proof .. ',' .. client_proof) 139 | local server_key = ngx.hmac_sha1(salted_pass, "Server Key") 140 | local server_sig = ngx.encode_base64(ngx.hmac_sha1(server_key, auth_msg)) 141 | 142 | r, err = self:cmd(attachpairs_start({ 143 | saslContinue = 1 ; 144 | conversationId = conversationId ; 145 | payload = client_final ; 146 | } , "saslContinue" ) ) 147 | if not r then 148 | return nil, err 149 | end 150 | parsed_s = ngx.decode_base64(r['payload']) 151 | parsed_t = {} 152 | for k, v in string.gmatch(parsed_s, "(%w+)=([^,]*)") do 153 | parsed_t[k] = v 154 | end 155 | if parsed_t['v'] ~= server_sig then 156 | return nil, "Server returned an invalid signature." 157 | end 158 | 159 | if not r['done'] then 160 | r, err = self:cmd(attachpairs_start({ 161 | saslContinue = 1 ; 162 | conversationId = conversationId ; 163 | payload = ngx.encode_base64("") ; 164 | } , "saslContinue" ) ) 165 | if not r then 166 | return nil, err 167 | end 168 | if not r['done'] then 169 | return nil, 'SASL conversation failed to complete.' 170 | end 171 | return 1 172 | end 173 | return 1 174 | end 175 | 176 | function dbmethods:get_col(collection) 177 | if not collection then 178 | return nil, "collection needed" 179 | end 180 | 181 | return setmetatable ( { 182 | conn = self.conn; 183 | db_obj = self; 184 | db = self.db ; 185 | col = collection; 186 | } , colmt ) 187 | end 188 | 189 | function dbmethods:get_gridfs(fs) 190 | if not fs then 191 | return nil, "fs name needed" 192 | end 193 | 194 | return setmetatable({ 195 | conn = self.conn; 196 | db_obj = self; 197 | db = self.db; 198 | file_col = self:get_col(fs..".files"); 199 | chunk_col = self:get_col(fs..".chunks"); 200 | } , gridfs) 201 | end 202 | 203 | return dbmt 204 | -------------------------------------------------------------------------------- /lib/resty/mongol/get.lua: -------------------------------------------------------------------------------- 1 | local strsub = string.sub 2 | local t_insert = table.insert 3 | local t_concat = table.concat 4 | 5 | local function get_from_string ( s , i ) 6 | i = i or 1 7 | return function ( n ) 8 | if not n then -- Rest of string 9 | n = #s - i + 1 10 | end 11 | i = i + n 12 | assert ( i-1 <= #s , "Unable to read enough characters" ) 13 | return strsub ( s , i-n , i-1 ) 14 | end , function ( new_i ) 15 | if new_i then i = new_i end 16 | return i 17 | end 18 | end 19 | 20 | local function string_to_array_of_chars ( s ) 21 | local t = { } 22 | for i = 1 , #s do 23 | t [ i ] = strsub ( s , i , i ) 24 | end 25 | return t 26 | end 27 | 28 | local function read_terminated_string ( get , terminators ) 29 | local terminators = string_to_array_of_chars ( terminators or "\0" ) 30 | local str = { } 31 | local found = 0 32 | while found < #terminators do 33 | local c = get ( 1 ) 34 | if c == terminators [ found + 1 ] then 35 | found = found + 1 36 | else 37 | found = 0 38 | end 39 | t_insert ( str , c ) 40 | end 41 | return t_concat ( str , "" , 1 , #str - #terminators ) 42 | end 43 | 44 | return { 45 | get_from_string = get_from_string ; 46 | read_terminated_string = read_terminated_string ; 47 | } 48 | -------------------------------------------------------------------------------- /lib/resty/mongol/globalplus.lua: -------------------------------------------------------------------------------- 1 | -- globalsplus.lua 2 | -- Like globals.lua in Lua 5.1.4 but records fields in global tables too. 3 | -- Probably works but not well tested. Could be extended even further. 4 | -- 5 | -- usage: luac -p -l example.lua | lua globalsplus.lua 6 | -- 7 | -- D.Manura, 2010-07, public domain 8 | 9 | local function parse(line) 10 | local idx,linenum,opname,arga,argb,extra = 11 | line:match('^%s+(%d+)%s+%[(%d+)%]%s+(%w+)%s+([-%d]+)%s+([-%d]+)%s*(.*)') 12 | if idx then 13 | idx = tonumber(idx) 14 | linenum = tonumber(linenum) 15 | arga = tonumber(arga) 16 | argb = tonumber(argb) 17 | end 18 | local argc, const 19 | if extra then 20 | local extra2 21 | argc, extra2 = extra:match('^([-%d]+)%s*(.*)') 22 | if argc then argc = tonumber(argc); extra = extra2 end 23 | end 24 | if extra then 25 | const = extra:match('^; (.+)') 26 | end 27 | return {idx=idx,linenum=linenum,opname=opname,arga=arga,argb=argb,argc=argc,const=const} 28 | end 29 | 30 | local function getglobals(fh) 31 | local globals = {} 32 | local last 33 | for line in fh:lines() do 34 | local data = parse(line) 35 | if data.opname == 'GETGLOBAL' then 36 | data.gname = data.const 37 | last = data 38 | table.insert(globals, {linenum=last.linenum, name=data.const, isset=false}) 39 | elseif data.opname == 'SETGLOBAL' then 40 | last = data 41 | table.insert(globals, {linenum=last.linenum, name=data.const, isset=true}) 42 | elseif (data.opname == 'GETTABLE' or data.opname == 'SETTABLE') and last and 43 | last.gname and last.idx == data.idx-1 and last.arga == data.arga and data.const 44 | then 45 | local const = data.const:match('^"(.*)"') 46 | if const then 47 | data.gname = last.gname .. '.' .. const 48 | last = data 49 | table.insert(globals, {linenum=last.linenum, name=data.gname, isset=data.opname=='SETTABLE'}) 50 | end 51 | else 52 | last = nil 53 | end 54 | end 55 | return globals 56 | end 57 | 58 | local function rindex(t, name) 59 | for part in name:gmatch('%w+') do 60 | t = t[part] 61 | if t == nil then return nil end 62 | end 63 | return t 64 | end 65 | 66 | local whitelist = _G 67 | 68 | local globals = getglobals(io.stdin) 69 | table.sort(globals, function(a,b) return a.linenum < b.linenum end) 70 | for i,v in ipairs(globals) do 71 | local found = rindex(whitelist, v.name) 72 | print(v.linenum, v.name, v.isset and 'set' or 'get', found and 'defined' or 'undefined') 73 | end 74 | -------------------------------------------------------------------------------- /lib/resty/mongol/gridfs.lua: -------------------------------------------------------------------------------- 1 | local mod_name = (...):match ( "^(.*)%..-$" ) 2 | 3 | local md5 = require "resty.md5" 4 | local str = require "resty.string" 5 | local bson = require ( mod_name .. ".bson" ) 6 | local object_id = require ( mod_name .. ".object_id" ) 7 | local gridfs_file= require ( mod_name .. ".gridfs_file" ) 8 | 9 | local gridfs_mt = { } 10 | local gridfs = { __index = gridfs_mt } 11 | local get_bin_data = bson.get_bin_data 12 | local get_utc_date = bson.get_utc_date 13 | 14 | function gridfs_mt:find_one(fields) 15 | local r = self.file_col:find_one(fields) 16 | if not r then return nil end 17 | 18 | return setmetatable({ 19 | file_col = self.file_col; 20 | chunk_col = self.chunk_col; 21 | chunk_size = r.chunkSize; 22 | files_id = r._id; 23 | file_size = r.length; 24 | file_md5 = r.md5; 25 | file_name = r.filename; 26 | }, gridfs_file) 27 | end 28 | 29 | function gridfs_mt:get(fh, fields) 30 | local f = self:find_one(fields) 31 | if not f then return nil, "file not found" end 32 | local r = fh:write(f:read()) 33 | return r 34 | end 35 | 36 | function gridfs_mt:remove(fields, continue_on_err, safe) 37 | local r, err 38 | local n = 0 39 | if fields == {} then 40 | r,err = self.chunk_col:delete({}, continue_on_err, safe) 41 | if not r then return nil, "remove chunks failed: "..err end 42 | r,err = self.file_col:delete({}, continue_on_err, safe) 43 | if not r then return nil, "remove files failed: "..err end 44 | return r 45 | end 46 | 47 | local cursor = self.file_col:find(fields, {_id=1}) 48 | for k,v in cursor:pairs() do 49 | r,err = self.chunk_col:delete({files_id=v._id}, continue_on_err, safe) 50 | if not r then return nil, "remove chunks failed: "..err end 51 | r,err = self.file_col:delete({_id=v._id}, continue_on_err, safe) 52 | if not r then return nil, "remove files failed: "..err end 53 | n = n + 1 54 | end 55 | return n 56 | end 57 | 58 | function gridfs_mt:new(meta) 59 | meta = meta or {} 60 | meta._id = meta._id or object_id.new() 61 | meta.chunkSize = meta.chunkSize or 256*1024 62 | meta.filename = meta.filename or meta._id:tostring() 63 | 64 | meta.md5 = 0 65 | meta.uploadDate = get_utc_date(ngx.time() * 1000) 66 | meta.length = 0 67 | local r, err = self.file_col:insert({meta}, nil, true) 68 | if not r then return nil, err end 69 | 70 | return setmetatable({ 71 | file_col = self.file_col; 72 | chunk_col = self.chunk_col; 73 | chunk_size = meta.chunkSize; 74 | files_id = meta._id; 75 | file_size = 0; 76 | file_md5 = 0; 77 | file_name = meta.filename; 78 | }, gridfs_file) 79 | end 80 | 81 | function gridfs_mt:insert(fh, meta, safe) 82 | meta = meta or {} 83 | meta.chunkSize = meta.chunkSize or 256*1024 84 | meta._id = meta._id or object_id.new() 85 | meta.filename = meta.filename or meta._id:tostring() 86 | 87 | local n = 0 88 | local length = 0 89 | local r, err 90 | local md5_obj = md5:new() 91 | while true do 92 | local bytes = fh:read(meta.chunkSize) 93 | if not bytes then break end 94 | 95 | md5_obj:update(bytes) 96 | r, err = self.chunk_col:insert({{ files_id = meta._id, 97 | n = n, 98 | data = get_bin_data(bytes), 99 | }}, nil, safe) 100 | if safe and not r then return nil, err end 101 | 102 | n = n + 1 103 | length = length + string.len(bytes) 104 | end 105 | local md5hex = str.to_hex(md5_obj:final()) 106 | 107 | meta.md5 = md5hex 108 | meta.uploadDate = get_utc_date(ngx.time() * 1000) 109 | meta.length = length 110 | r, err = self.file_col:insert({meta}, nil, safe) 111 | if safe and not r then return nil, err end 112 | return r 113 | end 114 | 115 | return gridfs 116 | -------------------------------------------------------------------------------- /lib/resty/mongol/gridfs_file.lua: -------------------------------------------------------------------------------- 1 | local mod_name = (...):match ( "^(.*)%..-$" ) 2 | 3 | local md5 = require "resty.md5" 4 | local str = require "resty.string" 5 | local bson = require ( mod_name .. ".bson" ) 6 | 7 | local gridfs_file_mt = { } 8 | local gridfs_file = { __index = gridfs_file_mt } 9 | local get_bin_data = bson.get_bin_data 10 | 11 | -- write size bytes from the buf string into mongo, by the offset 12 | function gridfs_file_mt:write(buf, offset, size) 13 | size = size or string.len(buf) 14 | if offset > self.file_size then return nil, "invalid offset" end 15 | if size > #buf then return nil, "invalid size" end 16 | 17 | local cn -- number of chunks to be updated 18 | local af -- number of bytes to be updated in first chunk 19 | local bn = 0 -- bytes number of buf already updated 20 | local nv = {} 21 | local od, t, i, r, err 22 | local of = offset % self.chunk_size 23 | local n = math.floor(offset/self.chunk_size) 24 | 25 | if of == 0 and size % self.chunk_size == 0 then 26 | -- chunk1 chunk2 chunk3 27 | -- old data ====== ====== ====== 28 | -- write buf ====== ====== 29 | -- 30 | -- old data ====== ====== ====== 31 | -- write buf ====== 32 | 33 | cn = size/self.chunk_size 34 | for i = 1, cn do 35 | nv["$set"] = {data = get_bin_data(string.sub(buf, 36 | self.chunk_size*(i-1) + 1, 37 | self.chunk_size*(i-1) + self.chunk_size))} 38 | r, err = self.chunk_col:update({files_id = self.files_id, 39 | n = n+i-1}, nv, 1, 0, true) 40 | if not r then return nil,"write failed: "..err end 41 | end 42 | bn = size 43 | else 44 | 45 | if of + size > self.chunk_size then 46 | -- chunk1 chunk2 chunk3 47 | -- old data ====== ====== ====== 48 | -- write buf ======= 49 | -- ... -> of 50 | -- ... -> af 51 | af = self.chunk_size - of 52 | else 53 | af = size 54 | end 55 | 56 | cn = math.ceil((size + offset)/self.chunk_size) - n 57 | for i = 1, cn do 58 | if i == 1 then 59 | od = self.chunk_col:find_one( 60 | {files_id = self.files_id, n = n+i-1}) 61 | if of ~= 0 and od then 62 | if size + of >= self.chunk_size then 63 | -- chunk1 chunk2 chunk3 64 | -- old data ====== ====== ====== 65 | -- write buf ===== 66 | t = string.sub(od.data, 1, of) 67 | .. string.sub(buf, 1, af) 68 | else 69 | -- chunk1 chunk2 chunk3 70 | -- old data ====== ====== ====== 71 | -- write buf == 72 | t = string.sub(od.data, 1, of) 73 | .. string.sub(buf, 1, af) 74 | .. string.sub(od.data, size + of + 1) 75 | end 76 | bn = af 77 | elseif of == 0 and od then 78 | if size < self.chunk_size then 79 | -- chunk1 chunk2 chunk3 80 | -- old data ====== ====== ====== 81 | -- write buf === 82 | t = string.sub(buf, 1) 83 | .. string.sub(od.data, size + 1) 84 | bn = bn + size 85 | else 86 | -- chunk1 chunk2 chunk3 87 | -- old data ====== ====== ====== 88 | -- write buf ========= 89 | t = string.sub(buf, 1, self.chunk_size) 90 | bn = bn + self.chunk_size 91 | end 92 | else 93 | t = string.sub(buf, 1, self.chunk_size) 94 | bn = bn + #t --self.chunk_size 95 | end 96 | nv["$set"] = {data = get_bin_data(t)} 97 | r,err = self.chunk_col:update({files_id = self.files_id, 98 | n = n+i-1}, nv, 1, 0, true) 99 | if not r then return nil,"write failed: "..err end 100 | elseif i == cn then 101 | od = self.chunk_col:find_one( 102 | {files_id = self.files_id, n = n + i - 1} 103 | ) 104 | if od then 105 | t = string.sub(buf, bn + 1, size) 106 | .. string.sub(od.data, size - bn + 1) 107 | else 108 | t = string.sub(buf, bn + 1, size) 109 | end 110 | nv["$set"] = {data = get_bin_data(t)} 111 | r,err = self.chunk_col:update({files_id = self.files_id, 112 | n = n+i-1}, nv, 1, 0, true) 113 | if not r then return nil,"write failed: "..err end 114 | bn = size 115 | else 116 | nv["$set"] = {data = get_bin_data(string.sub(buf, 117 | bn + 1, bn + self.chunk_size))} 118 | r,err = self.chunk_col:update({files_id = self.files_id, 119 | n = n+i-1}, nv, 1, 0, true) 120 | if not r then return nil,"write failed: "..err end 121 | bn = bn + self.chunk_size 122 | end 123 | end 124 | end 125 | 126 | local nf = offset + bn 127 | if nf > self.file_size then 128 | nv["$set"] = {length = nf} 129 | r,err = self.file_col:update({_id = self.files_id},nv, 130 | 0, 0, true) 131 | if not r then return nil,"write failed: "..err end 132 | end 133 | 134 | nv["$set"] = {md5 = 0} 135 | r,err = self.file_col:update({_id = self.files_id},nv, 136 | 0, 0, true) 137 | if not r then return nil,"write failed: "..err end 138 | return bn 139 | end 140 | 141 | -- read size bytes from mongo by the offset 142 | function gridfs_file_mt:read(size, offset) 143 | size = size or self.file_size 144 | if size < 0 then 145 | return nil, "invalid size" 146 | end 147 | offset = offset or 0 148 | if offset < 0 or offset >= self.file_size then 149 | return nil, "invalid offset" 150 | end 151 | 152 | local n = math.floor(offset / self.chunk_size) 153 | local r 154 | local bytes = "" 155 | local rn = 0 156 | while true do 157 | r = self.chunk_col:find_one({files_id = self.files_id, n = n}) 158 | if not r then return nil, "read chunk failed" end 159 | if size - rn < self.chunk_size then 160 | bytes = bytes .. string.sub(r.data, 1, size - rn) 161 | rn = size 162 | else 163 | bytes = bytes .. r.data 164 | rn = rn + self.chunk_size 165 | end 166 | n = n + 1 167 | if rn >= size then break end 168 | end 169 | return bytes 170 | end 171 | 172 | function gridfs_file_mt:update_md5() 173 | local n = math.floor(self.file_size/self.chunk_size) 174 | local md5_obj = md5:new() 175 | local r, i, err 176 | 177 | for i = 0, n do 178 | r = self.chunk_col:find_one({files_id = self.files_id, n = i}) 179 | if not r then return false, "read chunk failed" end 180 | 181 | md5_obj:update(r.data) 182 | end 183 | local md5hex = str.to_hex(md5_obj:final()) 184 | 185 | local nv = {} 186 | nv["$set"] = {md5 = md5hex} 187 | self.file_md5 = md5hex 188 | r,err = self.file_col:update({_id = self.files_id}, nv, 0, 0, true) 189 | if not r then return false, "update failed: "..err end 190 | return true 191 | end 192 | 193 | return gridfs_file 194 | -------------------------------------------------------------------------------- /lib/resty/mongol/init.lua: -------------------------------------------------------------------------------- 1 | module("resty.mongol", package.seeall) 2 | 3 | local mod_name = (...) 4 | 5 | local assert , pcall = assert , pcall 6 | local ipairs , pairs = ipairs , pairs 7 | local setmetatable = setmetatable 8 | 9 | local socket = ngx.socket.tcp 10 | 11 | local connmethods = { } 12 | local connmt = { __index = connmethods } 13 | 14 | local dbmt = require ( mod_name .. ".dbmt" ) 15 | 16 | function connmethods:ismaster() 17 | local db = self:new_db_handle("admin") 18 | local r, err = db:cmd({ismaster = true}) 19 | if not r then 20 | return nil, err 21 | end 22 | return r.ismaster, r.hosts 23 | end 24 | 25 | local function parse_host ( str ) 26 | local host , port = str:match ( "([^:]+):?(%d*)" ) 27 | port = port or 27017 28 | return host , port 29 | end 30 | 31 | function connmethods:getprimary ( searched ) 32 | searched = searched or { [ self.host .. ":" .. self.port ] = true } 33 | 34 | local db = self:new_db_handle("admin") 35 | local r, err = db:cmd({ ismaster = true }) 36 | if not r then 37 | return nil, "query admin failed: "..err 38 | elseif r.ismaster then return self 39 | else 40 | for i , v in ipairs ( r.hosts ) do 41 | if not searched[v] then 42 | searched[v] = true 43 | local host, port = parse_host(v) 44 | local conn = new() 45 | local ok, err = conn:connect(host, port) 46 | if not ok then 47 | return nil, "connect failed: "..err..v 48 | end 49 | 50 | local found = conn:getprimary(searched) 51 | if found then 52 | return found 53 | end 54 | end 55 | end 56 | end 57 | return nil , "No master server found" 58 | end 59 | 60 | function connmethods:databases() 61 | local db = self:new_db_handle("admin") 62 | local r = assert ( db:cmd({ listDatabases = true } )) 63 | return r.databases 64 | end 65 | 66 | function connmethods:shutdown() 67 | local db = self:new_db_handle("admin") 68 | db:cmd({ shutdown = true } ) 69 | end 70 | 71 | function connmethods:new_db_handle ( db ) 72 | if not db then 73 | return nil 74 | end 75 | 76 | return setmetatable ( { 77 | conn = self ; 78 | db = db ; 79 | } , dbmt ) 80 | end 81 | 82 | function connmethods:set_timeout(timeout) 83 | local sock = self.sock 84 | if not sock then 85 | return nil, "not initialized" 86 | end 87 | 88 | return sock:settimeout(timeout) 89 | end 90 | 91 | function connmethods:set_keepalive(...) 92 | local sock = self.sock 93 | if not sock then 94 | return nil, "not initialized" 95 | end 96 | 97 | return sock:setkeepalive(...) 98 | end 99 | 100 | function connmethods:get_reused_times() 101 | local sock = self.sock 102 | if not sock then 103 | return nil, "not initialized" 104 | end 105 | 106 | return sock:getreusedtimes() 107 | end 108 | 109 | function connmethods:connect(host, port) 110 | self.host = host or self.host 111 | self.port = port or self.port 112 | local sock = self.sock 113 | 114 | return sock:connect(self.host, self.port) 115 | end 116 | 117 | function connmethods:close() 118 | local sock = self.sock 119 | if not sock then 120 | return nil, "not initialized" 121 | end 122 | 123 | return sock:close() 124 | end 125 | 126 | connmt.__call = connmethods.new_db_handle 127 | 128 | function new(self) 129 | return setmetatable ( { 130 | sock = socket(); 131 | host = "localhost"; 132 | port = 27017; 133 | } , connmt ) 134 | end 135 | 136 | -- to prevent use of casual module global variables 137 | getmetatable(resty.mongol).__newindex = function (table, key, val) 138 | error('attempt to write to undeclared variable "' .. key .. '": ' 139 | .. debug.traceback()) 140 | end 141 | -------------------------------------------------------------------------------- /lib/resty/mongol/ll.lua: -------------------------------------------------------------------------------- 1 | -- Library for reading low level data 2 | 3 | local assert = assert 4 | local unpack = unpack 5 | local floor = math.floor 6 | local strbyte , strchar = string.byte , string.char 7 | 8 | local ll = { } 9 | 10 | local le_uint_to_num = function ( s , i , j ) 11 | i , j = i or 1 , j or #s 12 | local b = { strbyte ( s , i , j ) } 13 | local n = 0 14 | for i=#b , 1 , -1 do 15 | n = n*2^8 + b [ i ] 16 | end 17 | return n 18 | end 19 | local le_int_to_num = function ( s , i , j ) 20 | i , j = i or 1 , j or #s 21 | local n = le_uint_to_num ( s , i , j ) 22 | local overflow = 2^(8*(j-i) + 7) 23 | if n > 2^overflow then 24 | n = - ( n % 2^overflow ) 25 | end 26 | return n 27 | end 28 | local num_to_le_uint = function ( n , bytes ) 29 | bytes = bytes or 4 30 | local b = { } 31 | for i=1 , bytes do 32 | b [ i ] , n = n % 2^8 , floor ( n / 2^8 ) 33 | end 34 | assert ( n == 0 ) 35 | return strchar ( unpack ( b ) ) 36 | end 37 | local num_to_le_int = function ( n , bytes ) 38 | bytes = bytes or 4 39 | if n < 0 then -- Converted to unsigned. 40 | n = 2^(8*bytes) + n 41 | end 42 | return num_to_le_uint ( n , bytes ) 43 | end 44 | 45 | local be_uint_to_num = function ( s , i , j ) 46 | i , j = i or 1 , j or #s 47 | local b = { strbyte ( s , i , j ) } 48 | local n = 0 49 | for i=1 , #b do 50 | n = n*2^8 + b [ i ] 51 | end 52 | return n 53 | end 54 | local num_to_be_uint = function ( n , bytes ) 55 | bytes = bytes or 4 56 | local b = { } 57 | for i=bytes , 1 , -1 do 58 | b [ i ] , n = n % 2^8 , floor ( n / 2^8 ) 59 | end 60 | assert ( n == 0 ) 61 | return strchar ( unpack ( b ) ) 62 | end 63 | 64 | -- Returns (as a number); bits i to j (indexed from 0) 65 | local extract_bits = function ( s , i , j ) 66 | j = j or i 67 | local i_byte = floor ( i / 8 ) + 1 68 | local j_byte = floor ( j / 8 ) + 1 69 | 70 | local n = be_uint_to_num ( s , i_byte , j_byte ) 71 | n = n % 2^( j_byte*8 - i ) 72 | n = floor ( n / 2^( (-(j+1) ) % 8 ) ) 73 | return n 74 | end 75 | 76 | -- Look at ith bit in given string (indexed from 0) 77 | -- Returns boolean 78 | local le_bpeek = function ( s , bitnum ) 79 | local byte = floor ( bitnum / 8 ) + 1 80 | local bit = bitnum % 8 81 | local char = strbyte ( s , byte ) 82 | return floor ( ( char % 2^(bit+1) ) / 2^bit ) == 1 83 | end 84 | -- Test with: 85 | --local sum = 0 for i=0,31 do v = le_bpeek ( num_to_le_uint ( N , 4 ) , i ) sum=sum + ( v and 1 or 0 )*2^i end assert ( sum == N ) 86 | 87 | local be_bpeek = function ( s , bitnum ) 88 | local byte = floor ( bitnum / 8 ) + 1 89 | local bit = 7-bitnum % 8 90 | local char = strbyte ( s , byte ) 91 | return floor ( ( char % 2^(bit+1) ) / 2^bit ) == 1 92 | end 93 | -- Test with: 94 | --local sum = 0 for i=0,31 do v = be_bpeek ( num_to_be_uint ( N , 4 ) , i ) sum=sum + ( v and 1 or 0 )*2^(31-i) end assert ( sum == N ) 95 | 96 | 97 | local hasffi , ffi = pcall ( require , "ffi" ) 98 | local to_double , from_double 99 | do 100 | local s , e , d 101 | if hasffi then 102 | d = ffi.new ( "double[1]" ) 103 | else 104 | -- Can't use with to_double as we can't strip debug info :( 105 | d = string.dump ( loadstring ( [[return 523123.123145345]] ) ) 106 | s , e = d:find ( "\3\54\208\25\126\204\237\31\65" ) 107 | s = d:sub ( 1 , s ) 108 | e = d:sub ( e+1 , -1 ) 109 | end 110 | 111 | function to_double ( n ) 112 | if hasffi then 113 | d [ 0 ] = n 114 | return ffi.string ( d , 8 ) 115 | else 116 | -- Should be the 8 bytes following the second \3 (LUA_TSTRING == '\3') 117 | local str = string.dump ( loadstring ( [[return ]] .. n ) ) 118 | local loc , en , mat = str:find ( "\3(........)" , str:find ( "\3" ) + 1 ) 119 | return mat 120 | end 121 | end 122 | function from_double ( str ) 123 | assert ( #str == 8 ) 124 | if hasffi then 125 | ffi.copy ( d , str , 8 ) 126 | return d [ 0 ] 127 | else 128 | str = s .. str .. e 129 | return loadstring ( str ) ( ) 130 | end 131 | end 132 | end 133 | 134 | return { 135 | le_uint_to_num = le_uint_to_num ; 136 | le_int_to_num = le_int_to_num ; 137 | num_to_le_uint = num_to_le_uint ; 138 | num_to_le_int = num_to_le_int ; 139 | 140 | be_uint_to_num = be_uint_to_num ; 141 | num_to_be_uint = num_to_be_uint ; 142 | 143 | extract_bits = extract_bits ; 144 | 145 | le_bpeek = le_bpeek ; 146 | be_bpeek = be_bpeek ; 147 | 148 | to_double = to_double ; 149 | from_double = from_double ; 150 | } 151 | -------------------------------------------------------------------------------- /lib/resty/mongol/misc.lua: -------------------------------------------------------------------------------- 1 | local mod_name = (...):match ( "^(.*)%..-$" ) 2 | 3 | local ll = require ( mod_name .. ".ll" ) 4 | local num_to_le_uint = ll.num_to_le_uint 5 | local num_to_le_int = ll.num_to_le_int 6 | local le_uint_to_num = ll.le_uint_to_num 7 | local le_bpeek = ll.le_bpeek 8 | 9 | 10 | local getmetatable , setmetatable = getmetatable , setmetatable 11 | local pairs = pairs 12 | local next = next 13 | 14 | do 15 | -- Test to see if __pairs is natively supported 16 | local supported = false 17 | local test = setmetatable ( { } , { __pairs = function ( ) supported = true end } ) 18 | pairs ( test ) 19 | if not supported then 20 | _G.pairs = function ( t ) 21 | local mt = getmetatable ( t ) 22 | if mt then 23 | local f = mt.__pairs 24 | if f then 25 | return f ( t ) 26 | end 27 | end 28 | return pairs ( t ) 29 | end 30 | -- Confirm we added it 31 | _G.pairs ( test ) 32 | assert ( supported ) 33 | end 34 | end 35 | 36 | 37 | local pairs_start = function ( t , sk ) 38 | local i = 0 39 | return function ( t , k , v ) 40 | i = i + 1 41 | local nk, nv 42 | if i == 1 then 43 | return sk, t[sk] 44 | elseif i == 2 then 45 | nk, nv = next(t) 46 | else 47 | nk, nv = next(t, k) 48 | end 49 | return nk,nv 50 | end , t 51 | end 52 | 53 | local function attachpairs_start ( o , k ) 54 | local mt = getmetatable ( o ) 55 | if not mt then 56 | mt = { } 57 | setmetatable ( o , mt ) 58 | end 59 | mt.__pairs = function ( t ) 60 | return pairs_start ( t , k ) 61 | end 62 | return o 63 | end 64 | 65 | return { 66 | pairs_start = pairs_start ; 67 | attachpairs_start = attachpairs_start ; 68 | } 69 | 70 | -------------------------------------------------------------------------------- /lib/resty/mongol/object_id.lua: -------------------------------------------------------------------------------- 1 | local mod_name = (...):match ( "^(.*)%..-$" ) 2 | 3 | local setmetatable = setmetatable 4 | local strbyte = string.byte 5 | local strformat = string.format 6 | local t_insert = table.insert 7 | local t_concat = table.concat 8 | 9 | local hasposix , posix = pcall ( require , "posix" ) 10 | 11 | local ll = require ( mod_name .. ".ll" ) 12 | local num_to_le_uint = ll.num_to_le_uint 13 | local num_to_be_uint = ll.num_to_be_uint 14 | 15 | local function _tostring(ob) 16 | local t = {} 17 | for i = 1 , 12 do 18 | t_insert(t, strformat("%02x", strbyte(ob.id, i, i))) 19 | end 20 | return t_concat(t) 21 | end 22 | 23 | local function _get_ts(ob) 24 | return ll.be_uint_to_num(ob.id, 1, 4) 25 | end 26 | 27 | local function _get_hostname(ob) 28 | local t = {} 29 | for i = 5, 7 do 30 | t_insert(t, strformat("%02x", strbyte(ob.id, i, i))) 31 | end 32 | return t_concat(t) 33 | end 34 | 35 | local function _get_pid(ob) 36 | return ll.be_uint_to_num(ob.id, 8, 9) 37 | end 38 | 39 | local function _get_inc(ob) 40 | return ll.be_uint_to_num(ob.id, 10, 12) 41 | end 42 | 43 | local object_id_mt = { 44 | __tostring = _tostring; 45 | __eq = function ( a , b ) return a.id == b.id end ; 46 | __lt = function ( a , b ) return a.id < b.id end ; 47 | __le = function ( a , b ) return a.id <= b.id end ; 48 | } 49 | 50 | local machineid 51 | if hasposix then 52 | machineid = posix.uname("%n") 53 | else 54 | machineid = assert(io.popen("uname -n")):read("*l") 55 | end 56 | machineid = ngx.md5_bin(machineid):sub(1, 3) 57 | 58 | local pid = num_to_le_uint(bit.band(ngx.worker.pid(), 0xFFFF), 2) 59 | 60 | local inc = 0 61 | local function generate_id ( ) 62 | inc = inc + 1 63 | -- "A BSON ObjectID is a 12-byte value consisting of a 4-byte timestamp (seconds since epoch), a 3-byte machine id, a 2-byte process id, and a 3-byte counter. Note that the timestamp and counter fields must be stored big endian unlike the rest of BSON" 64 | inc = inc < 2^24 and inc or (2^24 - inc + 1) 65 | return num_to_be_uint ( os.time ( ) , 4 ) .. machineid .. pid .. num_to_be_uint ( inc , 3 ) 66 | end 67 | 68 | local function new_object_id(str) 69 | if str then 70 | assert(#str == 12) 71 | else 72 | str = generate_id() 73 | end 74 | return setmetatable({id = str, 75 | tostring = _tostring, 76 | get_ts = _get_ts, 77 | get_pid = _get_pid, 78 | get_hostname = _get_hostname, 79 | get_inc = _get_inc, 80 | } , object_id_mt) 81 | end 82 | 83 | return { 84 | new = new_object_id ; 85 | metatable = object_id_mt ; 86 | } 87 | -------------------------------------------------------------------------------- /lib/resty/mongol/orderedtable.lua: -------------------------------------------------------------------------------- 1 | 2 | local setmetatable = setmetatable 3 | local t_insert = table.insert 4 | local rawget,rawset = rawget,rawset 5 | 6 | local ordered_mt = {} 7 | ordered_mt.__newindex = function(t,key,value) 8 | local _keys = t._keys 9 | if rawget(t,key) == nil then 10 | t_insert(_keys,key) 11 | rawset(t,key,value) 12 | else 13 | rawset(t,key,value) 14 | end 15 | end 16 | 17 | function ordered_mt.__pairs(t) 18 | local _i = 1 19 | local _next = function(t,k) 20 | local _k = rawget(t._keys,_i) 21 | if _k == nil then 22 | _i = 1 23 | return nil 24 | end 25 | local _v = rawget(t,_k) 26 | if _v == nil then 27 | _i = 1 28 | return nil 29 | end 30 | _i = _i + 1 31 | return _k,_v 32 | end 33 | return _next,t 34 | end 35 | 36 | local function merge(self,t) 37 | if type(t) ~= "table" then 38 | return 39 | end 40 | for k,v in pairs(t) do 41 | self[k] = v 42 | end 43 | return self 44 | end 45 | 46 | local function ordered_table(a) 47 | local t = { _keys = {} } 48 | t.merge = merge 49 | setmetatable(t,ordered_mt) 50 | if a then 51 | for i=1,#a,2 do 52 | t[a[i]] = a[i+1] 53 | end 54 | end 55 | return t, ordered_mt 56 | end 57 | 58 | return ordered_table -------------------------------------------------------------------------------- /lib/resty/stats/cache.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | author: jie123108@163.com 3 | date: 20151130 4 | ]] 5 | local json = require("resty.stats.json") 6 | local mongo_dao = require("resty.stats.mongo_dao") 7 | local util = require("resty.stats.util") 8 | 9 | local ngx_log = ngx.log 10 | 11 | local ok, new_tab = pcall(require, "table.new") 12 | if not ok then 13 | new_tab = function (narr, nrec) return {} end 14 | end 15 | 16 | local _M = new_tab(5, 5) 17 | _M.debug = false 18 | _M.flushing = false 19 | _M.stats = new_tab(100, 100) 20 | _M.has_stats = false 21 | _M.flush_interval = 10 -- 10 seconds 22 | _M.max_retry_times = 3 23 | _M.retry_interval = 0.2 -- second 24 | _M.mongo_cfg = nil 25 | 26 | function _M.init(mongo_cfg, flush_interval, retry_interval) 27 | assert(mongo_cfg ~= nil, "mongo_cfg is a nil") 28 | _M.mongo_cfg = mongo_cfg 29 | 30 | if flush_interval then 31 | _M.flush_interval = flush_interval 32 | end 33 | if retry_interval then 34 | _M.retry_interval = retry_interval 35 | end 36 | end 37 | 38 | function _M.write_stats(stats) 39 | local write_count = 0 40 | local retrys = _M.max_retry_times 41 | for stats_key, update in pairs(stats) do 42 | local arr = util.splitex(stats_key, "#|#", 2) 43 | if #arr == 2 then 44 | local collname = arr[1] 45 | local selector = arr[2] 46 | local debug_sql = 47 | 48 | ngx.log(ngx.DEBUG, "write_stats: db.", collname, ".update(", selector, ",", json.dumps(update), ",{upsert: true});") 49 | 50 | selector = json.loads(selector) 51 | local dao = mongo_dao:new(_M.mongo_cfg, collname) 52 | for i=1, retrys do 53 | local ok, err = dao:upsert(selector, update) 54 | if not ok then 55 | if i < retrys then 56 | ngx.log(ngx.WARN, "db.", collname, ".update(", json.dumps(selector), ",", json.dumps(update), ",{\"upsert\": true}) failed! err:", tostring(err)) 57 | ngx.sleep(_M.retry_interval or 0.3 * i) 58 | 59 | else 60 | ngx.log(ngx.ERR, "db.",collname, ".update(", json.dumps(selector), ",", json.dumps(update), ",{\"upsert\": true}) failed! err:", tostring(err)) 61 | end 62 | else 63 | write_count = write_count + 1 64 | if _M.debug then 65 | ngx.log(ngx.INFO, "db.",collname, ".update(", json.dumps(selector), ",", json.dumps(update), ",{\"upsert\": true}) ok! err:", tostring(err)) 66 | end 67 | break 68 | end 69 | end 70 | else 71 | ngx.log(ngx.ERR, "invalid stats_key[", stats_key, "] ...") 72 | end 73 | end 74 | 75 | return true, write_count 76 | end 77 | 78 | function _M.do_flush() 79 | if not _M.has_stats then 80 | return 81 | end 82 | 83 | if _M.stats_send == nil then 84 | _M.stats_send = _M.stats 85 | _M.stats = new_tab(100, 100) 86 | _M.has_stats = false 87 | end 88 | 89 | if _M.debug then 90 | ngx_log(ngx.INFO, " begin do flush!") 91 | end 92 | 93 | if _M.flushing then 94 | ngx_log(ngx.INFO, "previous flush not finished") 95 | return true 96 | else 97 | if _M.debug then 98 | ngx_log(ngx.INFO, " get flush lock...") 99 | end 100 | _M.flushing = true 101 | end 102 | if _M.stats_send == nil then 103 | if _M.debug then 104 | ngx_log(ngx.INFO, " no stats to flush! release flush lock!") 105 | end 106 | _M.flushing = false 107 | _M.stats_send = nil 108 | return true 109 | end 110 | local log_count = 0 111 | local ok, err = _M.write_stats(_M.stats_send) 112 | if _M.debug then 113 | ngx_log(ngx.INFO, "end to flush the stats!") 114 | end 115 | if ok then 116 | if type(err) == 'number' and err > 0 then 117 | ngx_log(ngx.INFO, "success to flush ", err, " stats to db!") 118 | end 119 | else 120 | ngx_log(ngx.ERR, "failed to flush stats to db! err:", err, ", ", log_count, " stats will dropped!") 121 | end 122 | _M.stats_send = nil 123 | 124 | _M.flushing = false 125 | if _M.debug then 126 | ngx_log(ngx.INFO, " release flush lock...") 127 | end 128 | end 129 | 130 | local function inc_merge_table(values, to_values) 131 | for field, value in pairs(values) do 132 | local to_value = to_values[field] 133 | if to_value == nil then 134 | to_values[field] = value 135 | elseif type(value) == 'number' then 136 | to_values[field] = to_value + value 137 | elseif type(value) == 'table' then 138 | if type(to_value) == 'table' then 139 | inc_merge_table(value, to_value) 140 | else 141 | ngx.log(ngx.WARN, "$inc." .. field .. " target is not a table, will droped!") 142 | to_values[field] = value 143 | end 144 | else 145 | ngx.log(ngx.ERR, '$inc.' .. field .. "'s value must a number") 146 | end 147 | end 148 | end 149 | 150 | local function merge_stats(from, to) 151 | for op, values in pairs(from) do 152 | local to_values = to[op] 153 | if to_values == nil then 154 | to[op] = values 155 | else 156 | if op == "$inc" then 157 | if type(values) == 'table' then 158 | inc_merge_table(values, to_values) 159 | else 160 | ngx.log(ngx.ERR, "$inc's value must be a 'table'") 161 | end 162 | elseif op == "$set" then 163 | if type(values) == 'table' then 164 | -- $set 操作,直接使用新的值,覆盖旧的值。 165 | to[op] = values 166 | else 167 | ngx.log(ngx.ERR, "$set's value must be a 'table'") 168 | end 169 | else 170 | ngx.log(ngx.ERR, "unprocessed mongodb operator [", op, "] ...") 171 | end 172 | end 173 | end 174 | end 175 | 176 | function _M.add_stats(table_name, selector, update) 177 | _M.has_stats = true 178 | if _M.debug then 179 | ngx_log(ngx.INFO, "merge a stats..") 180 | end 181 | selector = json.dumps(selector) 182 | local stats_key = string.format("%s#|#%s", table_name, selector) 183 | 184 | if _M.stats[stats_key] == nil then 185 | _M.stats[stats_key] = update 186 | else 187 | merge_stats(update, _M.stats[stats_key]) 188 | end 189 | end 190 | 191 | 192 | local function start_stats_flush_timer() 193 | local stats_flush_callback = function(premature) 194 | _M.do_flush() 195 | start_stats_flush_timer() 196 | end 197 | 198 | local next_run_time = _M.flush_interval 199 | next_run_time = (next_run_time - ngx.time()%next_run_time) 200 | 201 | --ngx.log(ngx.INFO, " [start_stats_flush_timer] next run time:", next_run_time) 202 | local ok, err = ngx.timer.at(next_run_time, stats_flush_callback) 203 | if not ok then 204 | ngx.log(ngx.ERR, "failed to create timer: ", err) 205 | return 206 | end 207 | end 208 | 209 | _M.start_stats_flush_timer = start_stats_flush_timer 210 | 211 | return _M -------------------------------------------------------------------------------- /lib/resty/stats/coll_util.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | author: jie123108@163.com 3 | date: 20151120 4 | comment: create collection index。 5 | ]] 6 | 7 | local mongo_dao = require("resty.stats.mongo_dao") 8 | local util = require("resty.stats.util") 9 | local t_ordered = require("resty.stats.orderedtable") 10 | local json = require("resty.stats.json") 11 | 12 | local _M = {} 13 | 14 | 15 | -- created indexes 16 | _M.exist_indexes = {} 17 | 18 | -- create index if not exist! 19 | function _M.create_coll_index(mongo_cfg, collection, indexes) 20 | if _M.exist_indexes[collection] then 21 | return true, "idx-exist" 22 | end 23 | 24 | local dao = mongo_dao:new(mongo_cfg, collection) 25 | local ok, err, idx_name = nil 26 | for _, index_info in ipairs(indexes) do 27 | ngx.log(ngx.INFO, "--- coll: ", tostring(collection), " index_info: ", json.dumps(index_info)) 28 | local index_keys = index_info.keys or index_info.index_keys 29 | local index_options = index_info.options or index_info.index_options 30 | local keys = t_ordered({}) 31 | for _, key in ipairs(index_keys) do 32 | keys[key] = 1 33 | end 34 | local options = {} 35 | if index_options then 36 | for k, v in pairs(index_options) do 37 | options[k] = v 38 | end 39 | end 40 | ok, err, idx_name = dao:ensure_index(keys,options, collection) 41 | if ok then 42 | ngx.log(ngx.INFO, "create index [",tostring(idx_name), "] for [", collection, "] success! ") 43 | else 44 | local err = tostring(err) 45 | if(string.find(err, "already exists")) then 46 | ok = true 47 | end 48 | ngx.log(ngx.ERR, "create index [",tostring(idx_name), "] for [", collection, "] failed! err:", err) 49 | end 50 | end 51 | -- dao:uninit() 52 | if ok then 53 | _M.exist_indexes[collection] = true 54 | return ok , "OK" 55 | else 56 | return ok, err 57 | end 58 | end 59 | 60 | return _M 61 | -------------------------------------------------------------------------------- /lib/resty/stats/init.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2015 Xiaojie Liu (jie123108@163.com). 2 | -- 3 | --[[ 4 | author: jie123108@163.com 5 | date: 20151127 6 | ]] 7 | 8 | local json = require("resty.stats.json") 9 | local coll_util = require("resty.stats.coll_util") 10 | local cache = require("resty.stats.cache") 11 | 12 | local VAR_PATTERM = "(\\$[a-zA-Z0-9_]+)" 13 | 14 | local function date() return string.sub(ngx.localtime(), 1, 10) end 15 | local function time() return string.sub(ngx.localtime(), 12) end 16 | local function year() return string.sub(ngx.localtime(), 1,4) end 17 | local function month() return string.sub(ngx.localtime(), 6,7) end 18 | local function day() return string.sub(ngx.localtime(), 9,10) end 19 | local function hour() return string.sub(ngx.localtime(), 12,13) end 20 | local function minute() return string.sub(ngx.localtime(), 15,16) end 21 | local function second() return string.sub(ngx.localtime(), 18,19) end 22 | 23 | local def_vars = { 24 | now = ngx.time, 25 | date = date, 26 | time = time, 27 | year = year, 28 | month = month, 29 | day = day, 30 | hour = hour, 31 | minute = minute, 32 | second = second 33 | } 34 | 35 | local function get_variable_value(values, key) 36 | local value = values[key]; 37 | if value then 38 | return value 39 | end 40 | 41 | if def_vars[key] then 42 | return def_vars[key]() 43 | end 44 | 45 | return "-" 46 | end 47 | 48 | local function var_format(format, values) 49 | if type(format) ~= 'string' then 50 | return format 51 | end 52 | 53 | local replace = function(m) 54 | local var = string.sub(m[0], 2) 55 | local value = get_variable_value(values, var) 56 | return value 57 | end 58 | local newstr, n, err = ngx.re.gsub(format, VAR_PATTERM, replace) 59 | 60 | return newstr, err 61 | end 62 | 63 | local _M = {} 64 | 65 | _M._VERSION = '1.0.3' 66 | 67 | _M.def_mongo_cfg = { host = "127.0.0.1",port = 27017, dbname = "ngx_stats"} 68 | 69 | --[[ 70 | --可用的变量: 71 | $now: 系统当前时间(秒), unixtime 72 | $date, 当前时间日期部分。输出格式为:yyyy-MM-dd 73 | $time, 当前时间时间部分。输出格式为:hh:mm:ss 74 | $year,$month,$day 分别为年月日,长度分别为4,2,2。 75 | $hour,$minute,$second 分别为时分秒。 76 | ]] 77 | -- selector 更新使用的查询子 78 | -- update 更新语句。 79 | -- indexes 更新查询需要用到的索引的字段。 80 | local def_stats_configs = { 81 | stats_host={ 82 | selector={date='$date',key='$host'}, 83 | update={['$inc']= {count=1, ['hour_cnt.$hour']=1, ['status.$status']=1, 84 | ['req_time.all']="$request_time", ['req_time.$hour']="$request_time"}}, 85 | indexes={ 86 | {keys={'date', 'key'}, options={unique=true}}, 87 | {keys={'key'}, options={}} 88 | } 89 | } 90 | } 91 | 92 | local stats_configs = {} 93 | 94 | local function check_stats_config(stats_name, stats_config) 95 | local selector = stats_config.selector 96 | local update = stats_config.update 97 | 98 | assert(selector ~= nil and type(selector)=='table', stats_name .. "'s selector must a table") 99 | assert(update ~= nil and type(update)=='table', stats_name .. "'s update must a table") 100 | 101 | for op, values in pairs(update) do 102 | assert(type(values)=='table', stats_name .. "'s value of '" .. op .. "' type must be a table" ) 103 | if op == "$inc" then 104 | -- for field, value in pairs(values) do 105 | -- assert(type(value)=='number', "$inc." .. field .. "'s value [" .. tostring(value) .. "] must be a number!") 106 | -- end 107 | elseif op == "$set" then 108 | 109 | else 110 | assert(false, stats_name .. "'s unknow mongodb operator '" .. op .. "'") 111 | end 112 | end 113 | end 114 | 115 | local function check_config(stats_config) 116 | for stats_name, stats_config in pairs(stats_config) do 117 | check_stats_config(stats_name, stats_config) 118 | end 119 | end 120 | 121 | local mongo_ops = { 122 | ["$inc"] = true, 123 | ["$mul"] = true, 124 | ["$rename"] = true, 125 | ["$setOnInsert"] = true, 126 | ["$set"] = true, 127 | ["$unset"] = true, 128 | ["$min"] = true, 129 | ["$max"] = true, 130 | ["$currentDate"] = true, 131 | ["$addToSet"] = true, 132 | ["$pop"] = true, 133 | ["$pullAll"] = true, 134 | ["$pull"] = true, 135 | ["$pushAll"] = true, 136 | ["$push"] = true, 137 | ["$each"] = true, 138 | ["$slice"] = true, 139 | ["$sort"] = true, 140 | ["$position"] = true, 141 | ["$bit"] = true, 142 | ["$isolated"] = true, 143 | } 144 | local function table_format(t, jso, output, parent_op) 145 | assert(type(t)=='table') 146 | 147 | if output == nil then 148 | output = {} 149 | end 150 | 151 | for k, v in pairs(t) do 152 | local vtype = type(v) 153 | if type(k) == 'number' then 154 | k = k -1 155 | elseif type(k) == 'string' and (not mongo_ops[k]) then 156 | k = var_format(k, jso) 157 | end 158 | if vtype == 'string' then 159 | local value = var_format(v, jso) 160 | if parent_op == "$inc" then 161 | value = tonumber(value) or 0 162 | end 163 | output[k] = value 164 | elseif vtype == 'table' then 165 | output[k] = table_format(v, jso, nil, k) 166 | else 167 | output[k] = v 168 | end 169 | end 170 | 171 | return output 172 | end 173 | 174 | --[[ 175 | stats_name: stats name and table name. 176 | stats_config: 177 | -- selector: the mongodb update selector 178 | -- update: the mongodb update statement 179 | -- indexes the indexes of the selector used. 180 | eg.: 181 | stats_name: stats_host 182 | stats_config: 183 | { 184 | selector={date='$date',key='$host'}, 185 | update={['$inc']= {count=1, ['hour_cnt.$hour']=1, ['status.$status']=1, 186 | ['req_time.all']="$request_time", ['req_time.$hour']="$request_time"}}, 187 | indexes={ 188 | {keys={'date', 'key'}, options={unique=true}}, 189 | {keys={'key'}, options={}} 190 | } 191 | } 192 | ]] 193 | function _M.add_stats_config(stats_name, stats_config) 194 | check_stats_config(stats_name, stats_config) 195 | if stats_configs[stats_name] then 196 | return false, "stats_name [" .. stats_name .. "] exist!" 197 | end 198 | stats_configs[stats_name] = stats_config 199 | end 200 | 201 | _M.stats_names = {} 202 | function _M.add_stats_name(name) 203 | _M.stats_names[name] = true 204 | end 205 | 206 | function _M.get_stats_names() 207 | local names = {} 208 | for k, _ in pairs(stats_configs) do 209 | table.insert(names, k) 210 | end 211 | for k, _ in pairs(_M.stats_names) do 212 | table.insert(names, k) 213 | end 214 | return names 215 | end 216 | 217 | -- add the default stats configs 218 | function _M.add_def_stats() 219 | for stats_name, stats_config in pairs(def_stats_configs) do 220 | _M.add_stats_config(stats_name, stats_config) 221 | end 222 | end 223 | 224 | function _M.init(mongo_cfg, flush_interval, retry_interval) 225 | _M.mongo_cfg = mongo_cfg or _M.def_mongo_cfg 226 | cache.init(_M.mongo_cfg, flush_interval, retry_interval) 227 | check_config(stats_configs) 228 | local function create_index_callback(premature, stats_configs, mongo_cfg) 229 | for stats_name, stats_config in pairs(stats_configs) do 230 | local indexes = stats_config.indexes 231 | if indexes then 232 | local collection = stats_name 233 | local ok, err = coll_util.create_coll_index(mongo_cfg, collection, indexes) 234 | ngx.log(ngx.INFO, "create_coll_index(", collection, ") ok:", tostring(ok), ", err:", tostring(err)) 235 | end 236 | end 237 | end 238 | local ok, err = ngx.timer.at(0, create_index_callback, stats_configs, _M.mongo_cfg) 239 | if not ok then 240 | ngx.log(ngx.ERR, "failed to create timer: ", err) 241 | return 242 | end 243 | 244 | cache.start_stats_flush_timer() 245 | end 246 | 247 | function _M.log(stats_name, filter) 248 | local values = ngx.var 249 | local function log_format(stats_name, stats_config) 250 | local fmt_selector = stats_config.selector 251 | local fmt_update = stats_config.update 252 | local selector = table_format(fmt_selector, values) 253 | local update = table_format(fmt_update, values) 254 | if filter then 255 | -- 可以对selector, udpate 进行处理. 256 | filter(stats_name, selector, update) 257 | end 258 | cache.add_stats(stats_name, selector, update) 259 | end 260 | if stats_name == nil then 261 | for stats_name, stats_config in pairs(stats_configs) do 262 | log_format(stats_name, stats_config) 263 | end 264 | else 265 | local stats_config = stats_configs[stats_name] 266 | assert(stats_config ~= nil, "stats '" .. stats_name .. "' not exist!") 267 | log_format(stats_name, stats_config) 268 | end 269 | end 270 | 271 | function _M.filter_4monitor(stats_name, selector, update) 272 | local headers = ngx.req.get_headers() 273 | local monitor = headers["X-Monitor"] 274 | if monitor and update then 275 | local set = update["$set"] or {} 276 | set["monitor_time"] = ngx.time() 277 | update["$set"] = set 278 | end 279 | end 280 | 281 | return _M 282 | -------------------------------------------------------------------------------- /lib/resty/stats/json.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | author: jie123108@163.com 3 | date: 20151120 4 | ]] 5 | local cjson = require "cjson" 6 | 7 | local _M = {} 8 | 9 | function _M.loads(str) 10 | local ok, jso = pcall(function() return cjson.decode(str) end) 11 | if ok then 12 | return jso 13 | else 14 | return nil, jso 15 | end 16 | end 17 | 18 | function _M.dumps(tab) 19 | if tab and type(tab) == 'table' then 20 | return cjson.encode(tab) 21 | else 22 | return tostring(tab) 23 | end 24 | end 25 | 26 | _M.null = cjson.null 27 | 28 | return _M -------------------------------------------------------------------------------- /lib/resty/stats/mongo_dao.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | author: jie123108@163.com 3 | date: 20151120 4 | ]] 5 | local mongo = require "resty.mongol" 6 | local t_ordered = require("resty.stats.orderedtable") 7 | local _M = {} 8 | 9 | local function t_concat(t, seq) 10 | seq = seq or "" 11 | if type(t) ~= 'table' then 12 | return tostring(t) 13 | end 14 | if #t > 0 then 15 | return table.concat(t, seq) 16 | end 17 | local keys = {} 18 | for k, v in pairs(t) do 19 | table.insert(keys, k .. "_" .. v) 20 | end 21 | return table.concat(keys, seq) 22 | end 23 | 24 | 25 | _M.init_seed = function () 26 | local cur_time = ngx.time() 27 | math.randomseed(cur_time) 28 | end 29 | 30 | local mt = { __index = _M } 31 | 32 | function _M:new(mongo_cfg, collname) 33 | assert(type(mongo_cfg) == 'table', "mongo_cfg must be a table") 34 | local dbname = mongo_cfg.dbname 35 | local timeout = mongo_cfg.timeout or 1000*5 36 | mongo_cfg.timeout = timeout 37 | collname = collname or "test" 38 | 39 | local ns = dbname .. "." .. collname 40 | return setmetatable({ mongo_cfg = mongo_cfg, dbname=dbname, collname=collname, ns=ns}, mt) 41 | end 42 | 43 | function _M:init() 44 | if self.conn then 45 | return true 46 | end 47 | local host = self.mongo_cfg.host 48 | local port = self.mongo_cfg.port 49 | local conn = mongo:new() 50 | conn:set_timeout(self.mongo_cfg.timeout) 51 | local ok, err = conn:connect(host, port) 52 | if not ok then 53 | ngx.log(ngx.ERR, "connect to mongodb (", host, ":", port, ") failed! err:", tostring(err)) 54 | return ok, err 55 | end 56 | 57 | local db = conn:new_db_handle(self.dbname) 58 | local coll = db:get_col(self.collname) 59 | 60 | -- TODO: auth.. 61 | --r = db:auth("admin", "admin") 62 | self.conn = conn 63 | self.db = db 64 | self.coll = coll 65 | 66 | return true 67 | end 68 | 69 | 70 | function _M:uninit() 71 | if self.conn then 72 | local pool_timeout = self.mongo_cfg.pool_timeout or 1000 * 60 73 | local pool_size = self.mongo_cfg.pool_size or 30 74 | self.conn:set_keepalive(pool_timeout, pool_size) 75 | self.conn = nil 76 | self.db = nil 77 | self.coll = nil 78 | end 79 | end 80 | 81 | function _M:update(selector, update, upsert, multiupdate, safe) 82 | if self.coll == nil then 83 | return false, "init failed!" 84 | end 85 | return self.coll:update(selector, update, upsert, multiupdate, safe) 86 | end 87 | 88 | function _M:insert(obj, continue_on_error, safe) 89 | local ok, err = self:init() 90 | if not ok then 91 | return false, err 92 | end 93 | if type(obj) == 'table' and #obj < 1 then 94 | obj = {obj} 95 | end 96 | 97 | local ok, ret, err = pcall(self.coll.insert, self.coll, obj, continue_on_error, safe) 98 | self:uninit() 99 | if ok then 100 | return ret, err 101 | else 102 | return ok, ret 103 | end 104 | --return self.coll:insert(obj, continue_on_error, safe) 105 | end 106 | 107 | function _M:upsert(selector, update) 108 | local ok, err = self:init() 109 | if not ok then 110 | return false, err 111 | end 112 | -- update(selector, update, upsert, multiupdate, safe) 113 | local ok, ret, err = pcall(self.coll.update,self.coll, selector, update, 1, 0, 0) 114 | self:uninit() 115 | if ok then 116 | return ret, err 117 | else 118 | return ok, ret 119 | end 120 | --return self.coll:update(selector, update, 1, 0, 0) 121 | end 122 | 123 | -- For parameter types and descriptions, reference:https://docs.mongodb.org/manual/reference/method/db.collection.createIndex/#db.collection.createIndex 124 | function _M:ensure_index(keys, options, collname) 125 | local ok, err = self:init() 126 | if not ok then 127 | return false, err 128 | end 129 | options = options or {} 130 | 131 | local index = t_ordered({}) 132 | local _keys = t_ordered():merge(keys) 133 | index.key = _keys 134 | index.name = options.name or t_concat(_keys,'_') 135 | for i,v in ipairs({"unique","background", "sparse"}) do 136 | if options[v] ~= nil then 137 | index[v] = options[v] and true or false 138 | end 139 | end 140 | 141 | local doc = t_ordered() 142 | -- used $cmd: https://www.bookstack.cn/read/mongodb-4.2-manual/662e17e4e7c25f48.md#dbcmd.createIndexes 143 | doc:merge({createIndexes = collname}) 144 | doc:merge({indexes = {index}}) 145 | 146 | local retinfo, errmsg = self.db:cmd(doc) 147 | 148 | self:uninit() 149 | local ok = false 150 | if retinfo then 151 | ok = retinfo.ok == 1 152 | end 153 | 154 | return ok, errmsg, index.name 155 | end 156 | 157 | return _M -------------------------------------------------------------------------------- /lib/resty/stats/orderedtable.lua: -------------------------------------------------------------------------------- 1 | 2 | local setmetatable = setmetatable 3 | local t_insert = table.insert 4 | local rawget,rawset = rawget,rawset 5 | 6 | local ordered_mt = {} 7 | ordered_mt.__newindex = function(t,key,value) 8 | local _keys = t._keys 9 | if rawget(t,key) == nil then 10 | t_insert(_keys,key) 11 | rawset(t,key,value) 12 | else 13 | rawset(t,key,value) 14 | end 15 | end 16 | 17 | function ordered_mt.__pairs(t) 18 | local _i = 1 19 | local _next = function(t,k) 20 | local _k = rawget(t._keys,_i) 21 | if _k == nil then 22 | _i = 1 23 | return nil 24 | end 25 | local _v = rawget(t,_k) 26 | if _v == nil then 27 | _i = 1 28 | return nil 29 | end 30 | _i = _i + 1 31 | return _k,_v 32 | end 33 | return _next,t 34 | end 35 | 36 | local function merge(self,t) 37 | if type(t) ~= "table" then 38 | return 39 | end 40 | for k,v in pairs(t) do 41 | self[k] = v 42 | end 43 | return self 44 | end 45 | 46 | local function ordered_table(a) 47 | local t = { _keys = {} } 48 | t.merge = merge 49 | setmetatable(t,ordered_mt) 50 | if a then 51 | for i=1,#a,2 do 52 | t[a[i]] = a[i+1] 53 | end 54 | end 55 | return t, ordered_mt 56 | end 57 | 58 | return ordered_table -------------------------------------------------------------------------------- /lib/resty/stats/util.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | author: jie123108@163.com 3 | date: 20151120 4 | ]] 5 | 6 | local _M = {} 7 | 8 | function _M.ifnull(var, value) 9 | if var == nil then 10 | return value 11 | end 12 | return var 13 | end 14 | 15 | function _M.trim (s) 16 | return (string.gsub(s, "^%s*(.-)%s*$", "%1")) 17 | end 18 | 19 | function _M.replace(s, s1, s2) 20 | local str = string.gsub(s, s1, s2) 21 | return str 22 | end 23 | 24 | function _M.endswith(str,endstr) 25 | return endstr=='' or string.sub(str,-string.len(endstr))==endstr 26 | end 27 | 28 | function _M.startswith(str,startstr) 29 | return startstr=='' or string.sub(str,1, string.len(startstr))==startstr 30 | end 31 | 32 | -- ngx.log(ngx.INFO, "config.idc_name:", config.idc_name, ", config.is_root:", config.is_root) 33 | -- delimiter 应该是单个字符。如果是多个字符,表示以其中任意一个字符做分割。 34 | function _M.split(s, delimiter) 35 | if s == nil then 36 | return nil 37 | end 38 | local result = {}; 39 | for match in string.gmatch(s, "[^"..delimiter.."]+") do 40 | table.insert(result, match); 41 | end 42 | return result; 43 | end 44 | 45 | -- delim 可以是多个字符。 46 | -- maxNb 最多分割项数 47 | function _M.splitex(str, delim, maxNb) 48 | -- Eliminate bad cases... 49 | if delim == nil or string.find(str, delim) == nil then 50 | return { str } 51 | end 52 | if maxNb == nil or maxNb < 1 then 53 | maxNb = 0 -- No limit 54 | end 55 | local result = {} 56 | local pat = "(.-)" .. delim .. "()" 57 | local nb = 0 58 | local lastPos 59 | for part, pos in string.gmatch(str, pat) do 60 | nb = nb + 1 61 | result[nb] = part 62 | lastPos = pos 63 | if nb == maxNb then break end 64 | end 65 | -- Handle the last field 66 | if nb ~= maxNb then 67 | result[nb + 1] = string.sub(str, lastPos) 68 | end 69 | return result 70 | end 71 | 72 | return _M -------------------------------------------------------------------------------- /t/nginx.conf: -------------------------------------------------------------------------------- 1 | user root; 2 | worker_processes 2; 3 | 4 | #error_log logs/error.log notice; 5 | error_log logs/error.log info; 6 | 7 | #pid logs/nginx.pid; 8 | 9 | 10 | events { 11 | worker_connections 1024; 12 | } 13 | 14 | 15 | http { 16 | #include mime.types; 17 | default_type html/text; 18 | 19 | #log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 20 | # '$status $body_bytes_sent "$http_referer" ' 21 | # '"$http_user_agent" "$http_x_forwarded_for"'; 22 | 23 | #access_log logs/access.log main; 24 | 25 | sendfile on; 26 | #tcp_nopush on; 27 | 28 | #keepalive_timeout 0; 29 | keepalive_timeout 65; 30 | 31 | #set ngx_lua's environment variable: 32 | lua_package_path '/opt/lua-resty-stats/lib/?.lua;/opt/lua-resty-stats/lib/?/init.lua;/opt/lua-resty-stats/view/?.lua;;'; 33 | # init the lua-resty-stats 34 | init_worker_by_lua ' 35 | local stats = require("resty.stats") 36 | -- add the default stats that named "stats_host" 37 | stats.add_def_stats() 38 | -- the general stats"s config 39 | local update = {["$inc"]= {count=1, ["hour_cnt.$hour"]=1, ["status.$status"]=1, 40 | ["req_time.all"]="$request_time", ["req_time.$hour"]="$request_time"}} 41 | 42 | -- stats by uri 43 | stats.add_stats_config("stats_uri", 44 | {selector={date="$date",key="$uri"}, update=update, 45 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 46 | 47 | -- stats by arg 48 | stats.add_stats_config("stats_arg", 49 | {selector={date="$date",key="$arg_client_type"}, update=update, 50 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 51 | 52 | -- stats by uri and args 53 | stats.add_stats_config("stats_uri_arg", 54 | {selector={date="$date",key="$uri?$arg_from"}, update=update, 55 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 56 | 57 | -- stats by http request header 58 | stats.add_stats_config("stats_header_in", 59 | {selector={date="$date",key="city:$http_city"}, update=update, 60 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 61 | 62 | -- stats by http response header 63 | stats.add_stats_config("stats_header_out", 64 | {selector={date="$date",key="cache:$sent_http_cache"}, update=update, 65 | indexes={{keys={'date', 'key'}, options={unique=true}},{keys={'key'}, options={}}} }) 66 | 67 | local mongo_cfg = {host="127.0.0.1", port=27017, dbname="ngx_stats"} 68 | local flush_interval = 2 -- second 69 | local retry_interval = 0.2 -- second 70 | -- init stats and start flush timer. 71 | stats.init(mongo_cfg, flush_interval, retry_interval) 72 | '; 73 | 74 | server { 75 | listen 88; 76 | server_name localhost; 77 | 78 | location /stats { 79 | set $template_root /opt/lua-resty-stats/view; 80 | content_by_lua_file '/opt/lua-resty-stats/view/main.lua'; 81 | } 82 | 83 | location /byuri { 84 | echo "byuri: $uri"; 85 | log_by_lua ' 86 | local stats = require("resty.stats") 87 | stats.log("stats_uri") 88 | stats.log("stats_host") 89 | '; 90 | } 91 | 92 | location /byarg { 93 | echo_sleep 0.005; 94 | echo "login $args"; 95 | log_by_lua ' 96 | local stats = require("resty.stats") 97 | stats.log("stats_arg") 98 | '; 99 | } 100 | 101 | location /byarg/404 { 102 | request_stats statby_arg "clitype:$arg_client_type"; 103 | return 404; 104 | log_by_lua ' 105 | local stats = require("resty.stats") 106 | stats.log("stats_arg") 107 | '; 108 | } 109 | 110 | location /byuriarg { 111 | echo "$uri?$args"; 112 | log_by_lua ' 113 | local stats = require("resty.stats") 114 | stats.log("stats_uri_arg") 115 | '; 116 | } 117 | 118 | location /byhttpheaderin { 119 | echo "city: $http_city"; 120 | log_by_lua ' 121 | local stats = require("resty.stats") 122 | stats.log("stats_header_in") 123 | '; 124 | } 125 | 126 | location /byhttpheaderout/ { 127 | proxy_pass http://127.0.0.1:82; 128 | log_by_lua ' 129 | local stats = require("resty.stats") 130 | stats.log("stats_header_out") 131 | '; 132 | } 133 | } 134 | 135 | server { 136 | listen 82; 137 | server_name localhost; 138 | location /byhttpheaderout/hit { 139 | add_header cache hit; 140 | echo "cache: hit"; 141 | } 142 | location /byhttpheaderout/miss { 143 | add_header cache miss; 144 | echo "cache: miss"; 145 | } 146 | } 147 | 148 | } -------------------------------------------------------------------------------- /t/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | for((i=0;i<10;i++));do 4 | curl http://127.0.0.1:88/byuri/$RANDOM 5 | curl http://127.0.0.1:88/byarg?client_type=pc 6 | curl http://127.0.0.1:88/byarg?client_type=ios 7 | curl http://127.0.0.1:88/byarg?client_type=android 8 | curl http://127.0.0.1:88/byarg/404?client_type=android 9 | curl http://127.0.0.1:88/byuriarg?from=partner 10 | curl http://127.0.0.1:88/byuriarg?from=pc_cli 11 | curl http://127.0.0.1:88/byuriarg?from=mobile_cli 12 | curl http://127.0.0.1:88/byhttpheaderin -H"city: shanghai" 13 | curl http://127.0.0.1:88/byhttpheaderin -H"city: shengzheng" 14 | curl http://127.0.0.1:88/byhttpheaderin -H"city: beijing" 15 | curl http://127.0.0.1:88/byhttpheaderout/hit 16 | curl http://127.0.0.1:88/byhttpheaderout/miss 17 | done; 18 | -------------------------------------------------------------------------------- /t/wrk_test.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | test framework: https://github.com/wg/wrk 3 | --]] 4 | 5 | local random = {} 6 | function random.choice(arr) 7 | return arr[math.random(1, #arr)] 8 | end 9 | function random.sample(str, len) 10 | local t= {} 11 | for i=1, len do 12 | local idx = math.random(#str) 13 | table.insert(t, string.sub(str, idx,idx)) 14 | end 15 | return table.concat(t) 16 | end 17 | 18 | local urls = { 19 | "/byuri/", 20 | "/byarg", 21 | "/byarg/404?client_type=android", 22 | "/byuriarg", 23 | "/byhttpheaderin", 24 | "/byhttpheaderout" 25 | } 26 | 27 | function request() 28 | local url = random.choice(urls) 29 | local headers = nil 30 | if url == "/byuri/" then 31 | url = url .. random.sample("ACKD952303LL", 4) 32 | elseif url == "/byarg" then 33 | url = url .. "?client_type=" .. random.choice({"pc","ios","android", "web"}) 34 | elseif url == "/byuriarg" then 35 | url = url .. "?from=" .. random.choice({"partner","pc_cli","mobile_cli", "web_cli"}) 36 | elseif url == "/byhttpheaderin" then 37 | headers = {} 38 | headers.city = random.choice({"shanghai", "shengzheng","beijing"}) 39 | elseif url == "/byhttpheaderout" then 40 | url = url .. random.choice({"/hit", "/miss"}) 41 | end 42 | 43 | wrk.method = "GET" 44 | wrk.path = url 45 | 46 | return wrk.format(nil, nil, headers) 47 | end 48 | 49 | -------------------------------------------------------------------------------- /view/filter.lua: -------------------------------------------------------------------------------- 1 | 2 | local _M = {} 3 | 4 | local function sep(i) 5 | if i % 2 == 1 then 6 | return ", " 7 | else 8 | return "\n" 9 | end 10 | end 11 | 12 | function _M.key_trim(stats_key) 13 | if type(stats_key) ~= 'string' then 14 | return '' 15 | end 16 | if #stats_key > 55 then 17 | return string.sub(stats_key, 1, 55) .. "..." 18 | end 19 | return stats_key 20 | end 21 | 22 | function _M.percent_alt(stats) 23 | return string.format("%d/%d", stats.count or 0, stats.total or 1) 24 | end 25 | 26 | function _M.percent(percent) 27 | return string.format("%.2f%%", percent or 0) 28 | end 29 | 30 | function _M.requests_alt(stats) 31 | if stats.hour_cnt == nil then 32 | return "" 33 | end 34 | 35 | local hours = {} 36 | for hour, count in pairs(stats.hour_cnt) do 37 | table.insert(hours, hour) 38 | end 39 | table.sort(hours) 40 | local alts = {} 41 | for i, hour in ipairs(hours) do 42 | table.insert(alts, hour .. ": " .. tostring(stats.hour_cnt[hour])) 43 | table.insert(alts, sep(i)) 44 | end 45 | return table.concat(alts) 46 | end 47 | 48 | local function status_count(stats, begin, end_) 49 | if stats.status then 50 | local ok_count = 0 51 | for status, count in pairs(stats.status) do 52 | status = tonumber(status) or 0 53 | if status >= begin and status <= end_ then 54 | ok_count = ok_count + tonumber(count) 55 | end 56 | end 57 | return ok_count 58 | else 59 | return '0' 60 | end 61 | end 62 | 63 | local function status_alt(stats, begin, end_) 64 | if stats.status == nil then 65 | return "" 66 | end 67 | 68 | local status_all = {} 69 | for status, count in pairs(stats.status) do 70 | local xstatus = tonumber(status) or 0 71 | if xstatus >= begin and xstatus <= end_ then 72 | table.insert(status_all, status) 73 | end 74 | end 75 | 76 | table.sort(status_all) 77 | 78 | local alts = {} 79 | for i, status in ipairs(status_all) do 80 | table.insert(alts, status .. ": " .. tostring(stats.status[status])) 81 | end 82 | return table.concat(alts, ", ") 83 | end 84 | 85 | function _M.ok(stats) 86 | return status_count(stats, 200, 399) 87 | end 88 | 89 | function _M.ok_alt(stats) 90 | return status_alt(stats, 200, 399) 91 | end 92 | 93 | function _M.fail_4xx(stats) 94 | return status_count(stats, 400, 499) 95 | end 96 | 97 | function _M.fail_alt_4xx(stats) 98 | return status_alt(stats, 400, 499) 99 | end 100 | 101 | function _M.fail_5xx(stats) 102 | return status_count(stats, 500, 599) 103 | end 104 | 105 | function _M.fail_alt_5xx(stats) 106 | return status_alt(stats, 500, 599) 107 | end 108 | 109 | function _M.avgtime(stats) 110 | local req_time, count = stats.req_time, stats.count 111 | if req_time and count and count > 0 then 112 | local time_sum = req_time.all or 0 113 | return string.format("%.3f", time_sum/count) 114 | else 115 | return '0' 116 | end 117 | end 118 | 119 | function _M.avgtime_alt(stats) 120 | if stats.req_time == nil or stats.hour_cnt == nil then 121 | return '' 122 | end 123 | local hours = {} 124 | for hour, req_time in pairs(stats.req_time) do 125 | if hour ~= "all" then 126 | table.insert(hours, tostring(hour)) 127 | end 128 | end 129 | table.sort(hours) 130 | local alts = {} 131 | for i, hour in ipairs(hours) do 132 | local count = stats.hour_cnt[hour] or 1 133 | local req_time = stats.req_time[hour] or 0 134 | table.insert(alts, string.format("%s: %.3f", hour, req_time/count)) 135 | table.insert(alts, sep(i)) 136 | end 137 | return table.concat(alts) 138 | end 139 | 140 | function _M.mon_status(stats) 141 | if stats.monitor_time then 142 | return 'Y' 143 | else 144 | return 'N' 145 | end 146 | end 147 | 148 | -- changes percent = abs((count - pre_count)*100 / max(pre_count,1)) 149 | function _M.changes(stats) 150 | local count = stats.count or 0 151 | local pre_count = math.max(stats.pre_count or 1, 1) 152 | local changes = count - pre_count 153 | local flag = "=" 154 | if changes < 0 then 155 | flag = "-" 156 | changes = changes * -1 157 | elseif changes > 0 then 158 | flag = "+" 159 | end 160 | local percent = (changes * 100) / pre_count 161 | local percent_str = string.format("%s%2.1d%%", flag, percent) 162 | return percent_str, percent, flag, changes 163 | end 164 | 165 | 166 | return _M -------------------------------------------------------------------------------- /view/main.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2015 Xiaojie Liu (jie123108@163.com). 2 | -- 3 | --[[ 4 | author: jie123108@163.com 5 | date: 20151206 6 | ]] 7 | 8 | local mongo = require("mongo") 9 | local template = require "resty.template" 10 | local cjson = require("cjson") 11 | local stats = require("resty.stats") 12 | 13 | local cache_tmpl = true 14 | 15 | local function create_option(collnames, table_name) 16 | local options = {} 17 | for _, collname in ipairs(collnames) do 18 | local selected = '' 19 | if collname == table_name then 20 | selected = [[selected="selected"]] 21 | end 22 | table.insert(options, string.format([[]], collname, selected, collname)) 23 | end 24 | return table.concat(options,"\r\n") 25 | end 26 | 27 | local function get_all_table_info(table) 28 | local collnames = stats.get_stats_names() 29 | local options = create_option(collnames, table) 30 | return options 31 | end 32 | 33 | local function split(s, delimiter) 34 | local result = {}; 35 | for match in string.gmatch(s, "[^"..delimiter.."]+") do 36 | table.insert(result, match); 37 | end 38 | return result; 39 | end 40 | 41 | local function add_day(date, n) 42 | local arr = split(date, "-") 43 | local year = 2015 44 | local month = 12 45 | local day = 12 46 | if #arr >= 1 then year = tostring(arr[1]) end 47 | if #arr >= 2 then month = tostring(arr[2]) end 48 | if #arr >= 3 then day = tostring(arr[3]) end 49 | local now = os.time({day=day,month=month,year=year})+n*3600*24 50 | return os.date("%Y-%m-%d", now) 51 | end 52 | 53 | local function get_query_date(args) 54 | local date = args.date 55 | if date == nil or date == 'today' then 56 | date = string.sub(ngx.localtime(), 1, 10) 57 | end 58 | local prev_day = add_day(date, -1) 59 | local next_day = add_day(date, 1) 60 | local today = string.sub(ngx.localtime(), 1, 10) 61 | return date, prev_day, next_day, today 62 | end 63 | 64 | local function stats_api() 65 | local args, err = ngx.req.get_uri_args() 66 | local table = args.table 67 | local key = args.key 68 | local limit = tonumber(args.limit) or 300 69 | local date = args.date 70 | if date == 'today' then 71 | date = string.sub(ngx.localtime(), 1, 10) 72 | end 73 | 74 | local stats_list = {} 75 | local ok = nil 76 | local errmsg = nil 77 | local mongo_cfg = stats.mongo_cfg 78 | -- query stats 79 | if not table then 80 | errmsg = "args 'table' missing" 81 | elseif key then 82 | ok, stats_list = mongo.get_stats_by_key(mongo_cfg, table, key, limit) 83 | if not ok then 84 | ngx.log(ngx.ERR, "mongo.get_stats_by_key(", table, ",", key, ") failed! err:", tostring(stats_list)) 85 | errmsg = "error on query:" .. tostring(stats_list) 86 | stats_list = {} 87 | end 88 | else 89 | ok, stats_list = mongo.get_stats(mongo_cfg, table, date, nil, limit) 90 | if not ok then 91 | ngx.log(ngx.ERR, "mongo.get_stats(", table, ",", date, ") failed! err:", tostring(stats_list)) 92 | errmsg = "error on query:" .. tostring(stats_list) 93 | stats_list = {} 94 | end 95 | end 96 | local resp = { 97 | errmsg=errmsg, 98 | stats=stats_list, 99 | } 100 | ngx.header["Content-Type"] = "application/json; charset=utf-8" 101 | ngx.say(cjson.encode(resp)) 102 | end 103 | 104 | local function stats_def() 105 | local args, err = ngx.req.get_uri_args() 106 | local table = args.table 107 | local key_pattern = args.key 108 | local tables = get_all_table_info(table) 109 | local diff = args.diff or true-- get requests diff of pre day and current date 110 | local date, prev_day, next_day, today = get_query_date(args) 111 | 112 | if diff == "false" then 113 | diff = false 114 | end 115 | args.submit = nil 116 | args.date = prev_day 117 | local prev_uri = ngx.var.uri .. "?" .. ngx.encode_args(args) 118 | args.date = next_day 119 | local next_uri = ngx.var.uri .. "?" .. ngx.encode_args(args) 120 | args.date = today 121 | local today_uri = ngx.var.uri .. "?" .. ngx.encode_args(args) 122 | 123 | 124 | local stats_list = {} 125 | local errmsg = nil 126 | -- query stats 127 | if table and date then 128 | local mongo_cfg = stats.mongo_cfg 129 | local ok, stats = mongo.get_stats(mongo_cfg, table, date, key_pattern, 300) 130 | if not ok then 131 | ngx.log(ngx.ERR, "mongo.get_stats(", table, ",", date, ") failed! err:", tostring(stats)) 132 | errmsg = "error on query:" .. tostring(stats) 133 | else 134 | stats_list = stats 135 | if diff then 136 | local ok, pre_stats = mongo.get_stats(mongo_cfg, table, prev_day, key_pattern, 400) 137 | if ok then 138 | local pre_counts = {} 139 | for _, stats in ipairs(pre_stats) do 140 | if stats.key then 141 | pre_counts[stats.key] = stats.count 142 | end 143 | end 144 | for _, stats in ipairs(stats_list) do 145 | local pre_count = 0 146 | if stats.key then 147 | pre_count = pre_counts[stats.key] or 0 148 | end 149 | stats.pre_count = pre_count 150 | end 151 | end 152 | end 153 | end 154 | end 155 | 156 | local page_args = {tables=tables, 157 | uri=ngx.var.uri, mon=args.mon, 158 | table=table, date=date, key=key_pattern, 159 | prev_uri=prev_uri, next_uri=next_uri, today_uri=today_uri, 160 | errmsg=errmsg, prev_day=prev_day} 161 | 162 | ngx.log(ngx.INFO, "page_args: ", cjson.encode(page_args)) 163 | page_args.stats_list = stats_list 164 | 165 | template.caching(cache_tmpl or true) 166 | template.render("stats.html", page_args) 167 | end 168 | 169 | local function stats_key() 170 | local args, err = ngx.req.get_uri_args() 171 | local key = args.key 172 | local table = args.table 173 | local limit = tonumber(args.limit) 174 | 175 | local stats_list = {} 176 | local errmsg = nil 177 | -- query stats 178 | if key then 179 | local mongo_cfg = stats.mongo_cfg 180 | local ok, stats = mongo.get_stats_by_key(mongo_cfg, table, key) 181 | if not ok then 182 | ngx.log(ngx.ERR, "mongo.get_stats_by_key(", table, ",", key, ") failed! err:", tostring(stats)) 183 | errmsg = "error on query:" .. tostring(stats) 184 | else 185 | stats_list = stats 186 | end 187 | end 188 | 189 | local page_args = { 190 | key=key, limit=limit, 191 | errmsg=errmsg} 192 | ngx.log(ngx.INFO, "page_args: ", cjson.encode(page_args)) 193 | page_args.stats_list = stats_list 194 | 195 | template.caching(cache_tmpl or true) 196 | template.render("stats_key.html", page_args) 197 | end 198 | 199 | ngx.header["Content-Type"] = 'text/html' 200 | 201 | local uri = ngx.var.uri 202 | local router = { 203 | ["/stats"] = stats_def, 204 | ["/stats/api"] = stats_api, 205 | ["/stats/key"] = stats_key, 206 | } 207 | 208 | local func = router[uri] 209 | if func then 210 | func() 211 | else 212 | ngx.log(ngx.ERR, "invalid request [", uri, "]") 213 | ngx.exit(404) 214 | end 215 | -------------------------------------------------------------------------------- /view/mongo.lua: -------------------------------------------------------------------------------- 1 | -- Copyright (C) 2015 Xiaojie Liu (jie123108@163.com). 2 | -- 3 | --[[ 4 | author: jie123108@163.com 5 | date: 20151206 6 | ]] 7 | 8 | local mongo = require "resty.mongol" 9 | local json = require("resty.stats.json") 10 | 11 | local function conn_get(mongo_cfg) 12 | local conn = mongo:new() 13 | if mongo_cfg.timeout then 14 | conn:set_timeout(mongo_cfg.timeout) 15 | end 16 | local host = mongo_cfg.host or "127.0.0.1" 17 | local port = mongo_cfg.port or 1000*10 18 | local ok, err = conn:connect(host, port) 19 | if not ok then 20 | ngx.log(ngx.ERR, "connect to mongodb (", host, ":", port, ") failed! err:", tostring(err)) 21 | return ok, err 22 | end 23 | 24 | -- local db = conn:new_db_handle(self.dbname) 25 | -- local coll = db:get_col(self.collname) 26 | return ok, conn 27 | end 28 | 29 | local function conn_put(conn) 30 | conn:set_keepalive() 31 | end 32 | 33 | local function stats_filter_by_key(stats_list, key_pattern) 34 | if key_pattern == nil or key_pattern == "" then 35 | return stats_list 36 | end 37 | local stats_new = {} 38 | for _, stats in ipairs(stats_list) do 39 | if stats.key and ngx.re.match(stats.key,key_pattern) then 40 | table.insert(stats_new, stats) 41 | end 42 | end 43 | 44 | return stats_new 45 | end 46 | 47 | 48 | local function add_percent(stats_list) 49 | local total = 0 50 | for i, stats in ipairs(stats_list) do 51 | local count = stats.count or 0 52 | total = total + count 53 | end 54 | if total > 0 then 55 | for i, stats in ipairs(stats_list) do 56 | local count = stats.count or 0 57 | local percent = (count*1.0 / total) * 100.0 58 | stats.percent = percent 59 | stats.total = total 60 | end 61 | end 62 | end 63 | 64 | local function mongo_find(coll, selector, sortby, skip, limit) 65 | local objs = {} 66 | skip = skip or 0 67 | local cursor, err = coll:find(selector, {_id=0}, limit) 68 | if cursor then 69 | if skip then 70 | cursor:skip(skip) 71 | end 72 | if limit then 73 | cursor:limit(limit) 74 | end 75 | if sortby then 76 | cursor:sort(sortby) 77 | end 78 | for index, item in cursor:pairs() do 79 | table.insert(objs, item) 80 | end 81 | end 82 | 83 | if err then 84 | return false, err 85 | else 86 | return true, objs 87 | end 88 | end 89 | 90 | 91 | local function get_stats(mongo_cfg, collname, date, key_pattern, limit) 92 | local ok, conn = conn_get(mongo_cfg) 93 | if not ok then 94 | return ok, conn 95 | end 96 | ngx.log(ngx.INFO, "------- collection:", collname, ", date:", date) 97 | local stats = {} 98 | local dbname = mongo_cfg.dbname or "ngx_stats" 99 | local db = conn:new_db_handle(dbname) 100 | local coll = db:get_col(collname) 101 | local query = {date=date} 102 | local skip = 0 103 | limit = limit or 300 104 | local sortby = {count=-1} 105 | local ok, tmp_stats = mongo_find(coll, query, sortby, skip, limit) 106 | if ok then 107 | if tmp_stats and type(tmp_stats) == 'table' then 108 | stats = stats_filter_by_key(tmp_stats, key_pattern) 109 | add_percent(stats) 110 | end 111 | else 112 | ngx.log(ngx.ERR, "mongo_query(", json.dumps(query), ") failed! err:", tmp_stats) 113 | end 114 | conn_put(conn) 115 | 116 | return true, stats 117 | end 118 | 119 | local function get_stats_by_key(mongo_cfg, collname, key, limit) 120 | local ok, conn = conn_get(mongo_cfg) 121 | if not ok then 122 | return ok, conn 123 | end 124 | ngx.log(ngx.INFO, "------- collection:", collname, ", key:", key) 125 | local stats = {} 126 | local dbname = mongo_cfg.dbname or "ngx_stats" 127 | local db = conn:new_db_handle(dbname) 128 | local coll = db:get_col(collname) 129 | local query = {key=key} 130 | local skip = 0 131 | limit = limit or 300 132 | -- local ok, tmp_stats = mongo_query(coll, query, offset, limit) 133 | local sortby = {date=-1} 134 | local ok, tmp_stats = mongo_find(coll, query, sortby, skip, limit) 135 | if ok then 136 | if tmp_stats and type(tmp_stats) == 'table' then 137 | stats = tmp_stats 138 | end 139 | else 140 | ngx.log(ngx.ERR, "mongo_query(", json.dumps(query), ") failed! err:", tmp_stats) 141 | end 142 | conn_put(conn) 143 | return true, stats 144 | end 145 | 146 | -- get_all_collections({host="127.0.0.1", port=27017}) 147 | -- local ok, stats = get_stats({host="127.0.0.1", port=27017}, "stats_uri", "2015-12-07") 148 | -- cjson = require "cjson" 149 | -- ngx.say("stats:", cjson.encode(stats)) 150 | 151 | return { 152 | get_all_collections = get_all_collections, 153 | get_stats = get_stats, 154 | get_stats_by_key = get_stats_by_key, 155 | } 156 | -------------------------------------------------------------------------------- /view/resty/template.lua: -------------------------------------------------------------------------------- 1 | local setmetatable = setmetatable 2 | local tostring = tostring 3 | local setfenv = setfenv 4 | local concat = table.concat 5 | local assert = assert 6 | local write = io.write 7 | local open = io.open 8 | local load = load 9 | local type = type 10 | local dump = string.dump 11 | local find = string.find 12 | local gsub = string.gsub 13 | local byte = string.byte 14 | local sub = string.sub 15 | 16 | local HTML_ENTITIES = { 17 | ["&"] = "&", 18 | ["<"] = "<", 19 | [">"] = ">", 20 | ['"'] = """, 21 | ["'"] = "'", 22 | ["/"] = "/" 23 | } 24 | 25 | local CODE_ENTITIES = { 26 | ["{"] = "{", 27 | ["}"] = "}", 28 | ["&"] = "&", 29 | ["<"] = "<", 30 | [">"] = ">", 31 | ['"'] = """, 32 | ["'"] = "'", 33 | ["/"] = "/" 34 | } 35 | 36 | local ok, newtab = pcall(require, "table.new") 37 | if not ok then newtab = function() return {} end end 38 | 39 | local caching, ngx_var, ngx_capture, ngx_null = true 40 | local template = newtab(0, 13); 41 | 42 | template._VERSION = "1.5" 43 | template.cache = {} 44 | template.concat = concat 45 | 46 | local function enabled(val) 47 | if val == nil then return true end 48 | return val == true or (val == "1" or val == "true" or val == "on") 49 | end 50 | 51 | local function trim(s) 52 | return gsub(gsub(s, "^%s+", ""), "%s+$", "") 53 | end 54 | 55 | local function rpos(view, s) 56 | while s > 0 do 57 | local c = sub(view, s, s) 58 | if c == " " or c == "\t" or c == "\0" or c == "\x0B" then 59 | s = s - 1 60 | else 61 | break; 62 | end 63 | end 64 | return s 65 | end 66 | 67 | local function read_file(path) 68 | local file = open(path, "rb") 69 | if not file then return nil end 70 | local content = file:read "*a" 71 | file:close() 72 | return content 73 | end 74 | 75 | local function load_lua(path) 76 | return read_file(path) or path 77 | end 78 | 79 | local function load_ngx(path) 80 | local file, location = path, ngx_var.template_location 81 | if sub(file, 1) == "/" then file = sub(file, 2) end 82 | if location and location ~= "" then 83 | if sub(location, -1) == "/" then location = sub(location, 1, -2) end 84 | local res = ngx_capture(location .. '/' .. file) 85 | if res.status == 200 then return res.body end 86 | end 87 | local root = ngx_var.template_root or ngx_var.document_root 88 | if sub(root, -1) == "/" then root = sub(root, 1, -2) end 89 | return read_file(root .. "/" .. file) or path 90 | end 91 | 92 | if ngx then 93 | template.print = ngx.print or write 94 | template.load = load_ngx 95 | ngx_var, ngx_capture, ngx_null = ngx.var, ngx.location.capture, ngx.null 96 | caching = enabled(ngx_var.template_cache) 97 | else 98 | template.print = write 99 | template.load = load_lua 100 | end 101 | 102 | local load_chunk 103 | 104 | if _VERSION == "Lua 5.1" then 105 | local context = { __index = function(t, k) 106 | return t.context[k] or t.template[k] or _G[k] 107 | end } 108 | if jit then 109 | load_chunk = function(view) 110 | return assert(load(view, nil, "tb", setmetatable({ template = template }, context))) 111 | end 112 | else 113 | load_chunk = function(view) 114 | local func = assert(loadstring(view)) 115 | setfenv(func, setmetatable({ template = template }, context)) 116 | return func 117 | end 118 | end 119 | else 120 | local context = { __index = function(t, k) 121 | return t.context[k] or t.template[k] or _ENV[k] 122 | end } 123 | load_chunk = function(view) 124 | return assert(load(view, nil, "tb", setmetatable({ template = template }, context))) 125 | end 126 | end 127 | 128 | function template.caching(enable) 129 | if enable ~= nil then caching = enable == true end 130 | return caching 131 | end 132 | 133 | function template.output(s) 134 | if s == nil or s == ngx_null then return "" end 135 | if type(s) == "function" then return template.output(s()) end 136 | return tostring(s) 137 | end 138 | 139 | function template.escape(s, c) 140 | if type(s) == "string" then 141 | if c then return gsub(s, "[}{\">/<'&]", CODE_ENTITIES) end 142 | return gsub(s, "[\">/<'&]", HTML_ENTITIES) 143 | end 144 | return template.output(s) 145 | end 146 | 147 | function template.new(view, layout) 148 | assert(view, "view was not provided for template.new(view, layout).") 149 | local render, compile = template.render, template.compile 150 | if layout then 151 | return setmetatable({ render = function(self, context) 152 | local context = context or self 153 | context.blocks = context.blocks or {} 154 | context.view = compile(view)(context) 155 | return render(layout, context) 156 | end }, { __tostring = function(self) 157 | local context = context or self 158 | context.blocks = context.blocks or {} 159 | context.view = compile(view)(context) 160 | return compile(layout)(context) 161 | end }) 162 | end 163 | return setmetatable({ render = function(self, context) 164 | return render(view, context or self) 165 | end }, { __tostring = function(self) 166 | return compile(view)(context or self) 167 | end }) 168 | end 169 | 170 | function template.precompile(view, path, strip) 171 | local chunk = dump(template.compile(view), strip ~= false) 172 | if path then 173 | local file = open(path, "wb") 174 | file:write(chunk) 175 | file:close() 176 | end 177 | return chunk 178 | end 179 | 180 | function template.compile(view, key, plain) 181 | assert(view, "view was not provided for template.compile(view, key, plain).") 182 | if key == "no-cache" then 183 | return load_chunk(template.parse(view, plain)), false 184 | end 185 | key = key or view 186 | local cache = template.cache 187 | if cache[key] then return cache[key], true end 188 | local func = load_chunk(template.parse(view, plain)) 189 | if caching then cache[key] = func end 190 | return func, false 191 | end 192 | 193 | function template.parse(view, plain) 194 | assert(view, "view was not provided for template.parse(view, plain).") 195 | if not plain then 196 | view = template.load(view) 197 | if byte(sub(view, 1, 1)) == 27 then return view end 198 | end 199 | local j = 2 200 | local c = {[[ 201 | context=(...) or {} 202 | local function include(v, c) 203 | return template.compile(v)(c or context) 204 | end 205 | local ___,blocks,layout={},blocks or {},nil 206 | ]] } 207 | local i, s = 1, find(view, "{", 1, true) 208 | while s do 209 | local t, p = sub(view, s + 1, s + 1), s + 2 210 | if t == "{" then 211 | local e = find(view, "}}", p, true) 212 | if e then 213 | if i < s then 214 | c[j] = "___[#___+1]=[=[\n" 215 | c[j+1] = sub(view, i, s - 1) 216 | c[j+2] = "]=]\n" 217 | j=j+3 218 | end 219 | c[j] = "___[#___+1]=template.escape(" 220 | c[j+1] = trim(sub(view, p, e - 1)) 221 | c[j+2] = ")\n" 222 | j=j+3 223 | s, i = e + 1, e + 2 224 | end 225 | elseif t == "*" then 226 | local e = (find(view, "*}", p, true)) 227 | if e then 228 | if i < s then 229 | c[j] = "___[#___+1]=[=[\n" 230 | c[j+1] = sub(view, i, s - 1) 231 | c[j+2] = "]=]\n" 232 | j=j+3 233 | end 234 | c[j] = "___[#___+1]=template.output(" 235 | c[j+1] = trim(sub(view, p, e - 1)) 236 | c[j+2] = ")\n" 237 | j=j+3 238 | s, i = e + 1, e + 2 239 | end 240 | elseif t == "%" then 241 | local e = find(view, "%}", p, true) 242 | if e then 243 | local n = e + 2 244 | if sub(view, n, n) == "\n" then 245 | n = n + 1 246 | end 247 | local r = rpos(view, s - 1) 248 | if i <= r then 249 | c[j] = "___[#___+1]=[=[\n" 250 | c[j+1] = sub(view, i, r) 251 | c[j+2] = "]=]\n" 252 | j=j+3 253 | end 254 | c[j] = trim(sub(view, p, e - 1)) 255 | c[j+1] = "\n" 256 | j=j+2 257 | s, i = n - 1, n 258 | end 259 | elseif t == "(" then 260 | local e = find(view, ")}", p, true) 261 | if e then 262 | local f = sub(view, p, e - 1) 263 | local x = (find(f, ",", 2, true)) 264 | if i < s then 265 | c[j] = "___[#___+1]=[=[\n" 266 | c[j+1] = sub(view, i, s - 1) 267 | c[j+2] = "]=]\n" 268 | j=j+3 269 | end 270 | if x then 271 | c[j] = "___[#___+1]=include([=[" 272 | c[j+1] = trim(sub(f, 1, x - 1)) 273 | c[j+2] = "]=]," 274 | c[j+3] = trim(sub(f, x + 1)) 275 | c[j+4] = ")\n" 276 | j=j+5 277 | else 278 | c[j] = "___[#___+1]=include([=[" 279 | c[j+1] = trim(f) 280 | c[j+2] = "]=])\n" 281 | j=j+3 282 | end 283 | s, i = e + 1, e + 2 284 | end 285 | elseif t == "[" then 286 | local e = find(view, "]}", p, true) 287 | if e then 288 | if i < s then 289 | c[j] = "___[#___+1]=[=[\n" 290 | c[j+1] = sub(view, i, s - 1) 291 | c[j+2] = "]=]\n" 292 | j=j+3 293 | end 294 | c[j] = "___[#___+1]=include(" 295 | c[j+1] = trim(sub(view, p, e - 1)) 296 | c[j+2] = ")\n" 297 | j=j+3 298 | s, i = e + 1, e + 2 299 | end 300 | elseif t == "-" then 301 | local e = find(view, "-}", p, true) 302 | if e then 303 | local x, y = find(view, sub(view, s, e + 1), e + 2, true) 304 | if x then 305 | y = y + 1 306 | x = x - 1 307 | if sub(view, y, y) == "\n" then 308 | y = y + 1 309 | end 310 | local b = trim(sub(view, p, e - 1)) 311 | if b == "verbatim" or b == "raw" then 312 | if i < s then 313 | c[j] = "___[#___+1]=[=[\n" 314 | c[j+1] = sub(view, i, s - 1) 315 | c[j+2] = "]=]\n" 316 | j=j+3 317 | end 318 | c[j] = "___[#___+1]=[=[" 319 | c[j+1] = sub(view, e + 2, x) 320 | c[j+2] = "]=]\n" 321 | j=j+3 322 | else 323 | if sub(view, x, x) == "\n" then 324 | x = x - 1 325 | end 326 | local r = rpos(view, s - 1) 327 | if i <= r then 328 | c[j] = "___[#___+1]=[=[\n" 329 | c[j+1] = sub(view, i, r) 330 | c[j+2] = "]=]\n" 331 | j=j+3 332 | end 333 | c[j] = 'blocks["' 334 | c[j+1] = b 335 | c[j+2] = '"]=include[=[' 336 | c[j+3] = sub(view, e + 2, x) 337 | c[j+4] = "]=]\n" 338 | j=j+5 339 | end 340 | s, i = y - 1, y 341 | end 342 | end 343 | elseif t == "#" then 344 | local e = find(view, "#}", p, true) 345 | if e then 346 | e = e + 2 347 | if sub(view, e, e) == "\n" then 348 | e = e + 1 349 | end 350 | s, i = e - 1, e 351 | end 352 | end 353 | s = find(view, "{", s + 1, true) 354 | end 355 | local rest = sub(view, i) 356 | if rest and rest ~= "" then 357 | c[j] = "___[#___+1]=[=[\n" 358 | c[j+1] = rest 359 | c[j+2] = "]=]\n" 360 | j=j+3 361 | end 362 | c[j] = "return layout and include(layout,setmetatable({view=template.concat(___),blocks=blocks},{__index=context})) or template.concat(___)" 363 | return concat(c) 364 | end 365 | 366 | function template.render(view, context, key, plain) 367 | assert(view, "view was not provided for template.render(view, context, key, plain).") 368 | return template.print(template.compile(view, key, plain)(context)) 369 | end 370 | 371 | return template -------------------------------------------------------------------------------- /view/stats.html: -------------------------------------------------------------------------------- 1 | {% 2 | local f = require("filter") 3 | %} 4 | 5 | 6 | 7 | Stats Query 8 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
50 |
51 | 52 | 53 | 60 | 61 | 64 | 65 | 68 | 71 | 72 | 73 |
54 |   55 | Table: 56 | 59 | Date: 62 | 63 | Key Filter: 66 | 67 | 69 |      Prev | {{date}} | Next | Today 70 |    
74 |
75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | {% if mon then %} 89 | 90 | {% end %} 91 | 92 | {% for i, stats in ipairs(stats_list) do 93 | local bgcolor = "#CCCCCC" 94 | if i%2==0 then bgcolor="#F1F1F1" end 95 | local ok_count = tonumber(f.ok(stats)) or 0 96 | local fail_4xx_count = tonumber(f.fail_4xx(stats)) or 0 97 | local fail_5xx_count = tonumber(f.fail_5xx(stats)) or 0 98 | local avgtime = f.avgtime(stats) or 0 99 | local percent_str, percent, flag, changes = f.changes(stats) 100 | local percent_class = "no_change" 101 | if changes > 100 then 102 | if percent > 100 then 103 | percent_class = "change_3" 104 | elseif percent > 60 then 105 | percent_class = "change_2" 106 | elseif percent > 20 then 107 | percent_class = "change_1" 108 | end 109 | end 110 | %} 111 | 112 | 113 | 114 | 117 | 118 | 119 | 123 | 124 | 125 | 128 | 131 | 132 | {% if mon then %} 133 | 134 | {% end %} 135 | 136 | {% end %} 137 |
 No DateStats Keypre_reqsrequestschangespercentOk(2xx/3xx)Fail(4xx)Fail(5xx)Resp TimeMon
 {{i}} {{stats.date}} 115 | {{f.key_trim(stats.key)}} 116 | {{stats.pre_count}}{{stats.count}} 121 | {{percent_str}} 122 | {{f.percent(stats.percent)}}{{ok_count}} 1 then %} class="fail_4xx" {% end %}> 126 | {{fail_4xx_count}} 127 | 1 then %} class="fail" {% end %}> 129 | {{fail_5xx_count}} 130 | 0.200 then %} class="slow" {% end %}>{{avgtime}}{{f.mon_status(stats)}}
138 | 139 | 140 | 149 | -------------------------------------------------------------------------------- /view/stats_key.html: -------------------------------------------------------------------------------- 1 | {% 2 | local f = require("filter") 3 | %} 4 | 5 | 6 | 7 | URL Stats Query 8 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 |
Stats Key: {{key}}
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | {% for i, stats in ipairs(stats_list) do 57 | local bgcolor = "#CCCCCC" 58 | if i%2==0 then bgcolor="#F1F1F1" end 59 | local ok_count = tonumber(f.ok(stats)) or 0 60 | local fail_4xx_count = tonumber(f.fail_4xx(stats)) or 0 61 | local fail_5xx_count = tonumber(f.fail_5xx(stats)) or 0 62 | local avgtime = f.avgtime(stats) or 0 63 | %} 64 | 65 | 66 | 67 | 68 | 69 | 72 | 75 | 76 | 77 | 78 | {% end %} 79 |
 No DaterequestsOk(2xx/3xx)Fail(4xx)Fail(5xx)Avg Response TimeOperate
 {{i}} {{stats.date}}{{stats.count}}{{ok_count}} 1 then %} class="fail_4xx" {% end %}> 70 | {{fail_4xx_count}} 71 | 1 then %} class="fail" {% end %}> 73 | {{fail_5xx_count}} 74 | 0.200 then %} class="slow" {% end %}>{{avgtime}} 
80 | 81 | 82 | --------------------------------------------------------------------------------