local MetricsGraphite = {} MetricsGraphite.__index = MetricsGraphite function MetricsGraphite.init(carbon_hosts, interval, mbase) local self = setmetatable({}, MetricsGraphite) ngx.log(ngx.INFO, "nginx-metrics-graphite initializing on nginx version " .. ngx.config.nginx_version .. " with ngx_lua version " .. ngx.config.ngx_lua_version) self.carbon_hosts = carbon_hosts self.interval = interval self.mbase = mbase -- metadata tables for more flexible metric creation self.query_status = { status_5xx = 500, status_4xx = 400, status_3xx = 300, status_2xx = 200, status_1xx = 100 } self.query_method = { method_get = "GET", method_head = "HEAD", method_put = "PUT", method_post = "POST", method_delete = "DELETE", method_options = "OPTIONS", method_other = "" } self.query_http = { http_09 = 0.9, http_10 = 1.0, http_11 = 1.1, http_20 = 2.0 } -- initialize/reset counters -- Note: ngx.shared.DICT is thread-safe, see https://github.com/openresty/lua-nginx-module#ngxshareddict self.stats = ngx.shared.metrics_graphite self.stats:set("main_loop_worker", 0) self.stats:set("requests", 0) -- total number self.stats:set("upstream_requests", 0) -- requests which used an upstream server self.stats:set("gzip_requests", 0) -- responses which used gzip self.stats:set("ssl_requests", 0) -- requests which used ssl self.stats:set("request_length", 0) self.stats:set("bytes_sent", 0) self.stats:set("request_time_sum", 0) self.stats:set("request_time_num", 0) for k,_ in pairs(self.query_status) do self.stats:set(k, 0) end for k,_ in pairs(self.query_method) do self.stats:set(k, 0) end for k,_ in pairs(self.query_http) do self.stats:set(k, 0) end return self end function MetricsGraphite:worker() -- determine which worker should handle the main loop, relies on the atomicity of ngx.shared.DICT:incr -- see https://github.com/openresty/lua-nginx-module#ngxshareddict -- caveeat: if the main loop worker dies no further metrics will be sent! if self.stats:incr("main_loop_worker", 1) ~= 1 then return end ngx.log(ngx.INFO, "nginx-metrics-graphite main loop worker PID is " .. ngx.worker.pid()) local this = self local callback callback = function (premature) -- first create the new timer to keep our intervals as good as possible -- (not when called premature since nginx is going to shut down soon) if not premature then local ok, err = ngx.timer.at(this.interval, callback) if not ok then ngx.log(ngx.ERR, "nginx-metrics-graphite callback failed to create interval timer: ", err) return end end -- then do the work which might incur delays -- submit the metrics to each configured carbon host for i,carbon_host in ipairs(this.carbon_hosts) do local sock, err = ngx.socket.tcp() if err then ngx.log(ngx.ERR, "nginx-metrics-graphite callback failed to create carbon host #" .. i .. " socket: ", err) return end -- connect to carbon host with submission port via TCP local ok, err2 = sock:connect(carbon_host, 2003) if not ok then ngx.log(ngx.ERR, "nginx-metrics-graphite callback failed to connect carbon host #" .. i .. " socket: ", err2) return end local avg_request_time = nil if this.stats:get("request_time_num") > 0 then avg_request_time = this.stats:get("request_time_sum") / this.stats:get("request_time_num") end self.stats:set("request_time_sum", 0) self.stats:set("request_time_num", 0) -- submit metrics sock:send(this.mbase .. ".nginx_metrics.num_requests " .. this.stats:get("requests") .. " " .. ngx.time() .. "\n") sock:send(this.mbase .. ".nginx_metrics.num_upstream_requests " .. this.stats:get("upstream_requests") .. " " .. ngx.time() .. "\n") sock:send(this.mbase .. ".nginx_metrics.num_gzip_requests " .. this.stats:get("gzip_requests") .. " " .. ngx.time() .. "\n") sock:send(this.mbase .. ".nginx_metrics.num_ssl_requests " .. this.stats:get("ssl_requests") .. " " .. ngx.time() .. "\n") sock:send(this.mbase .. ".nginx_metrics.acc_request_length " .. this.stats:get("request_length") .. " " .. ngx.time() .. "\n") sock:send(this.mbase .. ".nginx_metrics.acc_bytes_sent " .. this.stats:get("bytes_sent") .. " " .. ngx.time() .. "\n") if avg_request_time then sock:send(this.mbase .. ".nginx_metrics.avg_request_time " .. avg_request_time .. " " .. ngx.time() .. "\n") end for k,_ in pairs(self.query_status) do sock:send(this.mbase .. ".nginx_metrics.num_" .. k .. " " .. this.stats:get(k) .. " " .. ngx.time() .. "\n") end for k,_ in pairs(self.query_method) do sock:send(this.mbase .. ".nginx_metrics.num_" .. k .. " " .. this.stats:get(k) .. " " .. ngx.time() .. "\n") end for k,_ in pairs(self.query_http) do sock:send(this.mbase .. ".nginx_metrics.num_" .. k .. " " .. this.stats:get(k) .. " " .. ngx.time() .. "\n") end sock:close() end end -- start first timer local ok, err = ngx.timer.at(this.interval, callback) if not ok then ngx.log(ngx.ERR, "nginx-metrics-graphite callback failed to create interval timer: ", err) return end end function MetricsGraphite:log() -- function by default called on every request, -- should be fast and only do important calculations here self.stats:incr("requests", 1) if ngx.var.upstream_response_time ~= nil then self.stats:incr("upstream_requests", 1) end if ngx.var.gzip_ratio ~= nil then self.stats:incr("gzip_requests", 1) end if ngx.var.ssl_protocol ~= nil then self.stats:incr("ssl_requests", 1) end for k,v in pairs(self.query_status) do if ngx.status >= v and ngx.status < v+100 then self.stats:incr(k, 1) break end end local is_method_other = true for k,v in pairs(self.query_method) do if ngx.req.get_method() == v then self.stats:incr(k, 1) is_method_other = false break end end if is_method_other then self.stats:incr("method_other", 1) end for k,v in pairs(self.query_http) do -- float equaliy if math.abs(v - ngx.req.http_version()) < 0.01 then self.stats:incr(k, 1) break end end local request_length = ngx.var.request_length -- in bytes self.stats:incr("request_length", request_length) local bytes_sent = ngx.var.bytes_sent -- in bytes self.stats:incr("bytes_sent", bytes_sent) local request_time = ngx.now() - ngx.req.start_time() -- in seconds self.stats:incr("request_time_sum", request_time) self.stats:incr("request_time_num", 1) end return MetricsGraphite