├── Dockerfile ├── LICENSE ├── README.md ├── lib └── prometheus.lua ├── metrics.vhost ├── nginx.conf └── test.vhost /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openresty/openresty:alpine 2 | 3 | MAINTAINER Sophos 4 | 5 | COPY nginx.conf /usr/local/openresty/nginx/conf/ 6 | COPY *.vhost /usr/local/openresty/nginx/conf/ 7 | COPY lib/prometheus.lua /usr/local/openresty/luajit/lib 8 | 9 | RUN nginx -t -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sophos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nginx Prometheus Metrics 2 | 3 | [![Docker Repository on Quay](https://quay.io/repository/hnlq715/nginx-prometheus-metrics/status "Docker Repository on Quay")](https://quay.io/repository/hnlq715/nginx-prometheus-metrics) 4 | [![Docker Pulls](https://img.shields.io/docker/pulls/sophos/nginx-prometheus-metrics.svg)](https://hub.docker.com/r/sophos/nginx-prometheus-metrics/) 5 | 6 | A simple demo to collect prometheus metrics for nginx, version 1.11.4 or above recommended. 7 | 8 | [Docker Hub: sophos/nginx-prometheus-metrics](https://hub.docker.com/r/sophos/nginx-prometheus-metrics) 9 | 10 | ## Why use nginx_prometheus_metrics 11 | This is absolutely a good question, but please just try it before you ask. 12 | 13 | ## How to build 14 | 15 | ``` 16 | docker build -t nginx_prometheus_metrics . 17 | ``` 18 | 19 | ## How to run 20 | 21 | ``` 22 | docker pull sophos/nginx-prometheus-metrics 23 | docker run -d --rm -it -p 80:80 -p 1314:1314 -p 9527:9527 sophos/nginx-prometheus-metrics 24 | ``` 25 | 26 | Visit [http://localhost:1314](http://localhost:1314) to generate some test metrics. 27 | 28 | Then visit [http://localhost:9527/metrics](http://localhost:9527/metrics) in your browser(safari/chrome). 29 | 30 | And you will see the prometheus output below: 31 | ``` 32 | # HELP nginx_http_connections Number of HTTP connections 33 | # TYPE nginx_http_connections gauge 34 | nginx_http_connections{state="active"} 1 35 | nginx_http_connections{state="reading"} 0 36 | nginx_http_connections{state="waiting"} 0 37 | nginx_http_connections{state="writing"} 1 38 | # HELP nginx_http_request_time HTTP request time 39 | # TYPE nginx_http_request_time histogram 40 | nginx_http_request_time_bucket{host="localhost",le="03.000"} 1 41 | nginx_http_request_time_bucket{host="localhost",le="04.000"} 1 42 | nginx_http_request_time_bucket{host="localhost",le="05.000"} 1 43 | nginx_http_request_time_bucket{host="localhost",le="10.000"} 1 44 | nginx_http_request_time_bucket{host="localhost",le="+Inf"} 1 45 | nginx_http_request_time_bucket{host="testservers",le="00.005"} 1 46 | nginx_http_request_time_bucket{host="testservers",le="00.010"} 1 47 | nginx_http_request_time_bucket{host="testservers",le="00.020"} 1 48 | nginx_http_request_time_bucket{host="testservers",le="00.030"} 1 49 | nginx_http_request_time_bucket{host="testservers",le="00.050"} 1 50 | nginx_http_request_time_bucket{host="testservers",le="00.075"} 1 51 | nginx_http_request_time_bucket{host="testservers",le="00.100"} 1 52 | nginx_http_request_time_bucket{host="testservers",le="00.200"} 1 53 | nginx_http_request_time_bucket{host="testservers",le="00.300"} 1 54 | nginx_http_request_time_bucket{host="testservers",le="00.400"} 1 55 | nginx_http_request_time_bucket{host="testservers",le="00.500"} 1 56 | nginx_http_request_time_bucket{host="testservers",le="00.750"} 1 57 | nginx_http_request_time_bucket{host="testservers",le="01.000"} 1 58 | nginx_http_request_time_bucket{host="testservers",le="01.500"} 1 59 | nginx_http_request_time_bucket{host="testservers",le="02.000"} 1 60 | nginx_http_request_time_bucket{host="testservers",le="03.000"} 1 61 | nginx_http_request_time_bucket{host="testservers",le="04.000"} 1 62 | nginx_http_request_time_bucket{host="testservers",le="05.000"} 1 63 | nginx_http_request_time_bucket{host="testservers",le="10.000"} 1 64 | nginx_http_request_time_bucket{host="testservers",le="+Inf"} 1 65 | nginx_http_request_time_count{host="localhost"} 1 66 | nginx_http_request_time_count{host="testservers"} 1 67 | nginx_http_request_time_sum{host="localhost"} 2.0099999904633 68 | nginx_http_request_time_sum{host="testservers"} 0 69 | # HELP nginx_http_requests Number of HTTP requests 70 | # TYPE nginx_http_requests counter 71 | nginx_http_requests{host="localhost",status="200"} 1 72 | nginx_http_requests{host="testservers",status="200"} 1 73 | # HELP nginx_http_upstream_connect_time HTTP upstream connect time 74 | # TYPE nginx_http_upstream_connect_time histogram 75 | nginx_http_upstream_connect_time_bucket{addr="10.12.13.14:80",le="03.000"} 1 76 | nginx_http_upstream_connect_time_bucket{addr="10.12.13.14:80",le="04.000"} 1 77 | nginx_http_upstream_connect_time_bucket{addr="10.12.13.14:80",le="05.000"} 1 78 | nginx_http_upstream_connect_time_bucket{addr="10.12.13.14:80",le="10.000"} 1 79 | nginx_http_upstream_connect_time_bucket{addr="10.12.13.14:80",le="+Inf"} 1 80 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.005"} 1 81 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.010"} 1 82 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.020"} 1 83 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.030"} 1 84 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.050"} 1 85 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.075"} 1 86 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.100"} 1 87 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.200"} 1 88 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.300"} 1 89 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.400"} 1 90 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.500"} 1 91 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="00.750"} 1 92 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="01.000"} 1 93 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="01.500"} 1 94 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="02.000"} 1 95 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="03.000"} 1 96 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="04.000"} 1 97 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="05.000"} 1 98 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="10.000"} 1 99 | nginx_http_upstream_connect_time_bucket{addr="127.0.0.1:80",le="+Inf"} 1 100 | nginx_http_upstream_connect_time_count{addr="10.12.13.14:80"} 1 101 | nginx_http_upstream_connect_time_count{addr="127.0.0.1:80"} 1 102 | nginx_http_upstream_connect_time_sum{addr="10.12.13.14:80"} 2.006 103 | nginx_http_upstream_connect_time_sum{addr="127.0.0.1:80"} 0 104 | # HELP nginx_http_upstream_header_time HTTP upstream header time 105 | # TYPE nginx_http_upstream_header_time histogram 106 | nginx_http_upstream_header_time_bucket{addr="10.12.13.14:80",le="03.000"} 1 107 | nginx_http_upstream_header_time_bucket{addr="10.12.13.14:80",le="04.000"} 1 108 | nginx_http_upstream_header_time_bucket{addr="10.12.13.14:80",le="05.000"} 1 109 | nginx_http_upstream_header_time_bucket{addr="10.12.13.14:80",le="10.000"} 1 110 | nginx_http_upstream_header_time_bucket{addr="10.12.13.14:80",le="+Inf"} 1 111 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.005"} 1 112 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.010"} 1 113 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.020"} 1 114 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.030"} 1 115 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.050"} 1 116 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.075"} 1 117 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.100"} 1 118 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.200"} 1 119 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.300"} 1 120 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.400"} 1 121 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.500"} 1 122 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="00.750"} 1 123 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="01.000"} 1 124 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="01.500"} 1 125 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="02.000"} 1 126 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="03.000"} 1 127 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="04.000"} 1 128 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="05.000"} 1 129 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="10.000"} 1 130 | nginx_http_upstream_header_time_bucket{addr="127.0.0.1:80",le="+Inf"} 1 131 | nginx_http_upstream_header_time_count{addr="10.12.13.14:80"} 1 132 | nginx_http_upstream_header_time_count{addr="127.0.0.1:80"} 1 133 | nginx_http_upstream_header_time_sum{addr="10.12.13.14:80"} 2.006 134 | nginx_http_upstream_header_time_sum{addr="127.0.0.1:80"} 0.004 135 | # HELP nginx_http_upstream_requests Number of HTTP upstream requests 136 | # TYPE nginx_http_upstream_requests counter 137 | nginx_http_upstream_requests{addr="10.12.13.14:80",status="504"} 1 138 | nginx_http_upstream_requests{addr="127.0.0.1:80",status="200"} 1 139 | # HELP nginx_http_upstream_response_time HTTP upstream response time 140 | # TYPE nginx_http_upstream_response_time histogram 141 | nginx_http_upstream_response_time_bucket{addr="10.12.13.14:80",le="03.000"} 1 142 | nginx_http_upstream_response_time_bucket{addr="10.12.13.14:80",le="04.000"} 1 143 | nginx_http_upstream_response_time_bucket{addr="10.12.13.14:80",le="05.000"} 1 144 | nginx_http_upstream_response_time_bucket{addr="10.12.13.14:80",le="10.000"} 1 145 | nginx_http_upstream_response_time_bucket{addr="10.12.13.14:80",le="+Inf"} 1 146 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.005"} 1 147 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.010"} 1 148 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.020"} 1 149 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.030"} 1 150 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.050"} 1 151 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.075"} 1 152 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.100"} 1 153 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.200"} 1 154 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.300"} 1 155 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.400"} 1 156 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.500"} 1 157 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="00.750"} 1 158 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="01.000"} 1 159 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="01.500"} 1 160 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="02.000"} 1 161 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="03.000"} 1 162 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="04.000"} 1 163 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="05.000"} 1 164 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="10.000"} 1 165 | nginx_http_upstream_response_time_bucket{addr="127.0.0.1:80",le="+Inf"} 1 166 | nginx_http_upstream_response_time_count{addr="10.12.13.14:80"} 1 167 | nginx_http_upstream_response_time_count{addr="127.0.0.1:80"} 1 168 | nginx_http_upstream_response_time_sum{addr="10.12.13.14:80"} 2.006 169 | nginx_http_upstream_response_time_sum{addr="127.0.0.1:80"} 0.004 170 | # HELP nginx_metric_errors_total Number of nginx-lua-prometheus errors 171 | # TYPE nginx_metric_errors_total counter 172 | nginx_metric_errors_total 0 173 | ``` 174 | -------------------------------------------------------------------------------- /lib/prometheus.lua: -------------------------------------------------------------------------------- 1 | -- vim: ts=2:sw=2:sts=2:expandtab 2 | -- 3 | -- This module uses a single dictionary shared between Nginx workers to keep 4 | -- all metrics. Each counter is stored as a separate entry in that dictionary, 5 | -- which allows us to increment them using built-in `incr` method. 6 | -- 7 | -- Prometheus requires that (a) all samples for a given metric are presented 8 | -- as one uninterrupted group, and (b) buckets of a histogram appear in 9 | -- increasing numerical order. We satisfy that by carefully constructing full 10 | -- metric names (i.e. metric name along with all labels) so that they meet 11 | -- those requirements while being sorted alphabetically. In particular: 12 | -- 13 | -- * all labels for a given metric are presented in reproducible order (the one 14 | -- used when labels were declared). "le" label for histogram metrics always 15 | -- goes last; 16 | -- * bucket boundaries (which are exposed as values of the "le" label) are 17 | -- presented as floating point numbers with leading and trailing zeroes. 18 | -- Number of of zeroes is determined for each bucketer automatically based on 19 | -- bucket boundaries; 20 | -- * internally "+Inf" bucket is stored as "Inf" (to make it appear after 21 | -- all numeric buckets), and gets replaced by "+Inf" just before we 22 | -- expose the metrics. 23 | -- 24 | -- For example, if you define your bucket boundaries as {0.00005, 10, 1000} 25 | -- then we will keep the following samples for a metric `m1` with label 26 | -- `site` set to `site1`: 27 | -- 28 | -- m1_bucket{site="site1",le="0000.00005"} 29 | -- m1_bucket{site="site1",le="0010.00000"} 30 | -- m1_bucket{site="site1",le="1000.00000"} 31 | -- m1_bucket{site="site1",le="Inf"} 32 | -- m1_count{site="site1"} 33 | -- m1_sum{site="site1"} 34 | -- 35 | -- "Inf" will be replaced by "+Inf" while publishing metrics. 36 | -- 37 | -- You can find the latest version and documentation at 38 | -- https://github.com/knyar/nginx-lua-prometheus 39 | -- Released under MIT license. 40 | 41 | 42 | -- Default set of latency buckets, 5ms to 10s: 43 | local DEFAULT_BUCKETS = {0.005, 0.01, 0.02, 0.03, 0.05, 0.075, 0.1, 0.2, 0.3, 44 | 0.4, 0.5, 0.75, 1, 1.5, 2, 3, 4, 5, 10} 45 | 46 | -- Metric is a "parent class" for all metrics. 47 | local Metric = {} 48 | function Metric:new(o) 49 | o = o or {} 50 | setmetatable(o, self) 51 | self.__index = self 52 | return o 53 | end 54 | 55 | -- Checks that the right number of labels values have been passed. 56 | -- 57 | -- Args: 58 | -- label_values: an array of label values. 59 | -- 60 | -- Returns: 61 | -- an error message or nil 62 | function Metric:check_labels(label_values) 63 | if self.label_names == nil and label_values == nil then 64 | return 65 | elseif self.label_names == nil and label_values ~= nil then 66 | return "Expected no labels for " .. self.name .. ", got " .. #label_values 67 | elseif label_values == nil and self.label_names ~= nil then 68 | return "Expected " .. #self.label_names .. " labels for " .. 69 | self.name .. ", got none" 70 | elseif #self.label_names ~= #label_values then 71 | return "Wrong number of labels for " .. self.name .. ". Expected " .. 72 | #self.label_names .. ", got " .. #label_values 73 | else 74 | for i, k in ipairs(self.label_names) do 75 | if label_values[i] == nil then 76 | return "Unexpected nil value for label " .. k .. " of " .. self.name 77 | end 78 | end 79 | end 80 | end 81 | 82 | local Counter = Metric:new() 83 | -- Increase a given counter by `value` 84 | -- 85 | -- Args: 86 | -- value: (number) a value to add to the counter. Defaults to 1 if skipped. 87 | -- label_values: an array of label values. Can be nil (i.e. not defined) for 88 | -- metrics that have no labels. 89 | function Counter:inc(value, label_values) 90 | local err = self:check_labels(label_values) 91 | if err ~= nil then 92 | self.prometheus:log_error(err) 93 | return 94 | end 95 | self.prometheus:inc(self.name, self.label_names, label_values, value or 1) 96 | end 97 | 98 | local Gauge = Metric:new() 99 | -- Set a given gauge to `value` 100 | -- 101 | -- Args: 102 | -- value: (number) a value to set the gauge to. Should be defined. 103 | -- label_values: an array of label values. Can be nil (i.e. not defined) for 104 | -- metrics that have no labels. 105 | function Gauge:set(value, label_values) 106 | if value == nil then 107 | self.prometheus:log_error("No value passed for " .. self.name) 108 | return 109 | end 110 | local err = self:check_labels(label_values) 111 | if err ~= nil then 112 | self.prometheus:log_error(err) 113 | return 114 | end 115 | self.prometheus:set(self.name, self.label_names, label_values, value) 116 | end 117 | 118 | local Histogram = Metric:new() 119 | -- Record a given value in a histogram. 120 | -- 121 | -- Args: 122 | -- value: (number) a value to record. Should be defined. 123 | -- label_values: an array of label values. Can be nil (i.e. not defined) for 124 | -- metrics that have no labels. 125 | function Histogram:observe(value, label_values) 126 | if value == nil then 127 | self.prometheus:log_error("No value passed for " .. self.name) 128 | return 129 | end 130 | local err = self:check_labels(label_values) 131 | if err ~= nil then 132 | self.prometheus:log_error(err) 133 | return 134 | end 135 | self.prometheus:histogram_observe(self.name, self.label_names, label_values, value) 136 | end 137 | 138 | local Prometheus = {} 139 | Prometheus.__index = Prometheus 140 | Prometheus.initialized = false 141 | 142 | -- Generate full metric name that includes all labels. 143 | -- 144 | -- Args: 145 | -- name: string 146 | -- label_names: (array) a list of label keys. 147 | -- label_values: (array) a list of label values. 148 | -- Returns: 149 | -- (string) full metric name. 150 | local function full_metric_name(name, label_names, label_values) 151 | if not label_names then 152 | return name 153 | end 154 | local label_parts = {} 155 | for idx, key in ipairs(label_names) do 156 | local label_value = (string.format("%s", label_values[idx]) 157 | :gsub("\\", "\\\\") 158 | :gsub("\n", "\\n") 159 | :gsub('"', '\\"')) 160 | table.insert(label_parts, key .. '="' .. label_value .. '"') 161 | end 162 | return name .. "{" .. table.concat(label_parts, ",") .. "}" 163 | end 164 | 165 | -- Construct bucket format for a list of buckets. 166 | -- 167 | -- This receives a list of buckets and returns a sprintf template that should 168 | -- be used for bucket boundaries to make them come in increasing order when 169 | -- sorted alphabetically. 170 | -- 171 | -- To re-phrase, this is where we detect how many leading and trailing zeros we 172 | -- need. 173 | -- 174 | -- Args: 175 | -- buckets: a list of buckets 176 | -- 177 | -- Returns: 178 | -- (string) a sprintf template. 179 | local function construct_bucket_format(buckets) 180 | local max_order = 1 181 | local max_precision = 1 182 | for _, bucket in ipairs(buckets) do 183 | assert(type(bucket) == "number", "bucket boundaries should be numeric") 184 | -- floating point number with all trailing zeros removed 185 | local as_string = string.format("%f", bucket):gsub("0*$", "") 186 | local dot_idx = as_string:find(".", 1, true) 187 | max_order = math.max(max_order, dot_idx - 1) 188 | max_precision = math.max(max_precision, as_string:len() - dot_idx) 189 | end 190 | return "%0" .. (max_order + max_precision + 1) .. "." .. max_precision .. "f" 191 | end 192 | 193 | -- Extract short metric name from the full one. 194 | -- 195 | -- Args: 196 | -- full_name: (string) full metric name that can include labels. 197 | -- 198 | -- Returns: 199 | -- (string) short metric name with no labels. For a `*_bucket` metric of 200 | -- histogram the _bucket suffix will be removed. 201 | local function short_metric_name(full_name) 202 | local labels_start, _ = full_name:find("{") 203 | if not labels_start then 204 | -- no labels 205 | return full_name 206 | end 207 | local suffix_idx, _ = full_name:find("_bucket{") 208 | if suffix_idx and full_name:find("le=") then 209 | -- this is a histogram metric 210 | return full_name:sub(1, suffix_idx - 1) 211 | end 212 | -- this is not a histogram metric 213 | return full_name:sub(1, labels_start - 1) 214 | end 215 | 216 | -- Makes a shallow copy of a table 217 | local function copy_table(table) 218 | local new = {} 219 | if table ~= nil then 220 | for k, v in ipairs(table) do 221 | new[k] = v 222 | end 223 | end 224 | return new 225 | end 226 | 227 | -- Initialize the module. 228 | -- 229 | -- This should be called once from the `init_by_lua` section in nginx 230 | -- configuration. 231 | -- 232 | -- Args: 233 | -- dict_name: (string) name of the nginx shared dictionary which will be 234 | -- used to store all metrics 235 | -- prefix: (optional string) if supplied, prefix is added to all 236 | -- metric names on output 237 | -- 238 | -- Returns: 239 | -- an object that should be used to register metrics. 240 | function Prometheus.init(dict_name, prefix) 241 | local self = setmetatable({}, Prometheus) 242 | self.dict = ngx.shared[dict_name or "prometheus_metrics"] 243 | self.help = {} 244 | if prefix then 245 | self.prefix = prefix 246 | else 247 | self.prefix = '' 248 | end 249 | self.type = {} 250 | self.registered = {} 251 | self.buckets = {} 252 | self.bucket_format = {} 253 | self.initialized = true 254 | 255 | self:counter("nginx_metric_errors_total", 256 | "Number of nginx-lua-prometheus errors") 257 | self.dict:set("nginx_metric_errors_total", 0) 258 | return self 259 | end 260 | 261 | function Prometheus:log_error(...) 262 | ngx.log(ngx.ERR, ...) 263 | self.dict:incr("nginx_metric_errors_total", 1) 264 | end 265 | 266 | function Prometheus:log_error_kv(key, value, err) 267 | self:log_error( 268 | "Error while setting '", key, "' to '", value, "': '", err, "'") 269 | end 270 | 271 | -- Register a counter. 272 | -- 273 | -- Args: 274 | -- name: (string) name of the metric. Required. 275 | -- description: (string) description of the metric. Will be used for the HELP 276 | -- comment on the metrics page. Optional. 277 | -- label_names: array of strings, defining a list of metrics. Optional. 278 | -- 279 | -- Returns: 280 | -- a Counter object. 281 | function Prometheus:counter(name, description, label_names) 282 | if not self.initialized then 283 | ngx.log(ngx.ERR, "Prometheus module has not been initialized") 284 | return 285 | end 286 | 287 | if self.registered[name] then 288 | self:log_error("Duplicate metric " .. name) 289 | return 290 | end 291 | self.registered[name] = true 292 | self.help[name] = description 293 | self.type[name] = "counter" 294 | 295 | return Counter:new{name=name, label_names=label_names, prometheus=self} 296 | end 297 | 298 | -- Register a gauge. 299 | -- 300 | -- Args: 301 | -- name: (string) name of the metric. Required. 302 | -- description: (string) description of the metric. Will be used for the HELP 303 | -- comment on the metrics page. Optional. 304 | -- label_names: array of strings, defining a list of metrics. Optional. 305 | -- 306 | -- Returns: 307 | -- a Gauge object. 308 | function Prometheus:gauge(name, description, label_names) 309 | if not self.initialized then 310 | ngx.log(ngx.ERR, "Prometheus module has not been initialized") 311 | return 312 | end 313 | 314 | if self.registered[name] then 315 | self:log_error("Duplicate metric " .. name) 316 | return 317 | end 318 | self.registered[name] = true 319 | self.help[name] = description 320 | self.type[name] = "gauge" 321 | 322 | return Gauge:new{name=name, label_names=label_names, prometheus=self} 323 | end 324 | 325 | 326 | -- Register a histogram. 327 | -- 328 | -- Args: 329 | -- name: (string) name of the metric. Required. 330 | -- description: (string) description of the metric. Will be used for the HELP 331 | -- comment on the metrics page. Optional. 332 | -- label_names: array of strings, defining a list of metrics. Optional. 333 | -- buckets: array if numbers, defining bucket boundaries. Optional. 334 | -- 335 | -- Returns: 336 | -- a Histogram object. 337 | function Prometheus:histogram(name, description, label_names, buckets) 338 | if not self.initialized then 339 | ngx.log(ngx.ERR, "Prometheus module has not been initialized") 340 | return 341 | end 342 | 343 | for _, label_name in ipairs(label_names or {}) do 344 | if label_name == "le" then 345 | self:log_error("Invalid label name 'le' in " .. name) 346 | return 347 | end 348 | end 349 | 350 | for _, suffix in ipairs({"", "_bucket", "_count", "_sum"}) do 351 | if self.registered[name .. suffix] then 352 | self:log_error("Duplicate metric " .. name .. suffix) 353 | return 354 | end 355 | self.registered[name .. suffix] = true 356 | end 357 | self.help[name] = description 358 | self.type[name] = "histogram" 359 | 360 | self.buckets[name] = buckets or DEFAULT_BUCKETS 361 | self.bucket_format[name] = construct_bucket_format(self.buckets[name]) 362 | 363 | return Histogram:new{name=name, label_names=label_names, prometheus=self} 364 | end 365 | 366 | -- Set a given dictionary key. 367 | -- This overwrites existing values, so it should only be used when initializing 368 | -- metrics or when explicitely overwriting the previous value of a metric. 369 | function Prometheus:set_key(key, value) 370 | local ok, err = self.dict:safe_set(key, value) 371 | if not ok then 372 | self:log_error_kv(key, value, err) 373 | end 374 | end 375 | 376 | -- Increment a given counter by `value`. 377 | -- 378 | -- Args: 379 | -- name: (string) short metric name without any labels. 380 | -- label_names: (array) a list of label keys. 381 | -- label_values: (array) a list of label values. 382 | -- value: (number) value to add. Optional, defaults to 1. 383 | function Prometheus:inc(name, label_names, label_values, value) 384 | local key = full_metric_name(name, label_names, label_values) 385 | if value == nil then value = 1 end 386 | if value < 0 then 387 | self:log_error_kv(key, value, "Value should not be negative") 388 | return 389 | end 390 | 391 | local newval, err = self.dict:incr(key, value) 392 | if newval then 393 | return 394 | end 395 | -- Yes, this looks like a race, so I guess we might under-report some values 396 | -- when multiple workers simultaneously try to create the same metric. 397 | -- Hopefully this does not happen too often (shared dictionary does not get 398 | -- reset during configuation reload). 399 | if err == "not found" then 400 | self:set_key(key, value) 401 | return 402 | end 403 | -- Unexpected error 404 | self:log_error_kv(key, value, err) 405 | end 406 | 407 | -- Set the current value of a gauge to `value` 408 | -- 409 | -- Args: 410 | -- name: (string) short metric name without any labels. 411 | -- label_names: (array) a list of label keys. 412 | -- label_values: (array) a list of label values. 413 | -- value: (number) the new value for the gauge. 414 | function Prometheus:set(name, label_names, label_values, value) 415 | local key = full_metric_name(name, label_names, label_values) 416 | self:set_key(key, value) 417 | end 418 | 419 | -- Record a given value into a histogram metric. 420 | -- 421 | -- Args: 422 | -- name: (string) short metric name without any labels. 423 | -- label_names: (array) a list of label keys. 424 | -- label_values: (array) a list of label values. 425 | -- value: (number) value to observe. 426 | function Prometheus:histogram_observe(name, label_names, label_values, value) 427 | self:inc(name .. "_count", label_names, label_values, 1) 428 | self:inc(name .. "_sum", label_names, label_values, value) 429 | 430 | -- we are going to mutate arrays of label names and values, so create a copy. 431 | local l_names = copy_table(label_names) 432 | local l_values = copy_table(label_values) 433 | 434 | -- Last bucket. Note, that the label value is "Inf" rather than "+Inf" 435 | -- required by Prometheus. This is necessary for this bucket to be the last 436 | -- one when all metrics are lexicographically sorted. "Inf" will get replaced 437 | -- by "+Inf" in Prometheus:collect(). 438 | table.insert(l_names, "le") 439 | table.insert(l_values, "Inf") 440 | self:inc(name .. "_bucket", l_names, l_values, 1) 441 | 442 | local label_count = #l_names 443 | for _, bucket in ipairs(self.buckets[name]) do 444 | if value <= bucket then 445 | -- last label is now "le" 446 | l_values[label_count] = self.bucket_format[name]:format(bucket) 447 | self:inc(name .. "_bucket", l_names, l_values, 1) 448 | end 449 | end 450 | end 451 | 452 | -- Present all metrics in a text format compatible with Prometheus. 453 | -- 454 | -- This function should be used to expose the metrics on a separate HTTP page. 455 | -- It will get the metrics from the dictionary, sort them, and expose them 456 | -- aling with TYPE and HELP comments. 457 | function Prometheus:collect() 458 | ngx.header.content_type = "text/plain" 459 | if not self.initialized then 460 | ngx.log(ngx.ERR, "Prometheus module has not been initialized") 461 | return 462 | end 463 | 464 | local keys = self.dict:get_keys(0) 465 | -- Prometheus server expects buckets of a histogram to appear in increasing 466 | -- numerical order of their label values. 467 | table.sort(keys) 468 | 469 | local seen_metrics = {} 470 | for _, key in ipairs(keys) do 471 | local value, err = self.dict:get(key) 472 | if value then 473 | local short_name = short_metric_name(key) 474 | if not seen_metrics[short_name] then 475 | if self.help[short_name] then 476 | ngx.say("# HELP " .. self.prefix .. short_name .. " " .. self.help[short_name]) 477 | end 478 | if self.type[short_name] then 479 | ngx.say("# TYPE " .. self.prefix .. short_name .. " " .. self.type[short_name]) 480 | end 481 | seen_metrics[short_name] = true 482 | end 483 | -- Replace "Inf" with "+Inf" in each metric's last bucket 'le' label. 484 | ngx.say(self.prefix .. key:gsub('le="Inf"', 'le="+Inf"'), " ", value) 485 | else 486 | self:log_error("Error getting '", key, "': ", err) 487 | end 488 | end 489 | end 490 | 491 | return Prometheus 492 | -------------------------------------------------------------------------------- /metrics.vhost: -------------------------------------------------------------------------------- 1 | lua_shared_dict prometheus_metrics 10M; 2 | lua_package_path '/usr/local/openresty/luajit/lib/?.lua;;'; 3 | 4 | init_by_lua_block { 5 | prometheus = require("prometheus").init("prometheus_metrics") 6 | 7 | http_requests = prometheus:counter( 8 | "nginx_http_requests", "Number of HTTP requests", {"host", "status"}) 9 | http_request_time = prometheus:histogram( 10 | "nginx_http_request_time", "HTTP request time", {"host"}) 11 | http_request_bytes_received = prometheus:counter( 12 | "nginx_http_request_bytes_received", "Number of HTTP request bytes received", {"host"}) 13 | http_request_bytes_sent = prometheus:counter( 14 | "nginx_http_request_bytes_sent", "Number of HTTP request bytes sent", {"host"}) 15 | http_connections = prometheus:gauge( 16 | "nginx_http_connections", "Number of HTTP connections", {"state"}) 17 | http_upstream_cache_status = prometheus:counter( 18 | "nginx_http_upstream_cache_status", "Number of HTTP upstream cache status", {"host", "status"}) 19 | http_upstream_requests = prometheus:counter( 20 | "nginx_http_upstream_requests", "Number of HTTP upstream requests", {"addr", "status"}) 21 | http_upstream_response_time = prometheus:histogram( 22 | "nginx_http_upstream_response_time", "HTTP upstream response time", {"addr"}) 23 | http_upstream_header_time = prometheus:histogram( 24 | "nginx_http_upstream_header_time", "HTTP upstream header time", {"addr"}) 25 | http_upstream_bytes_received = prometheus:counter( 26 | "nginx_http_upstream_bytes_received", "Number of HTTP upstream bytes received", {"addr"}) 27 | http_upstream_bytes_sent = prometheus:counter( 28 | "nginx_http_upstream_bytes_sent", "Number of HTTP upstream bytes sent", {"addr"}) 29 | http_upstream_connect_time = prometheus:histogram( 30 | "nginx_http_upstream_connect_time", "HTTP upstream connect time", {"addr"}) 31 | http_upstream_first_byte_time = prometheus:histogram( 32 | "nginx_http_upstream_first_byte_time", "HTTP upstream first byte time", {"addr"}) 33 | http_upstream_session_time = prometheus:histogram( 34 | "nginx_http_upstream_session_time", "HTTP upstream session time", {"addr"}) 35 | 36 | } 37 | 38 | log_by_lua_block { 39 | local function split(str) 40 | local array = {} 41 | for mem in string.gmatch(str, '([^, ]+)') do 42 | table.insert(array, mem) 43 | end 44 | return array 45 | end 46 | 47 | local function getWithIndex(str, idx) 48 | if str == nil then 49 | return nil 50 | end 51 | 52 | return split(str)[idx] 53 | end 54 | 55 | local host = ngx.var.host 56 | local status = ngx.var.status 57 | 58 | http_requests:inc(1, {host, status}) 59 | http_request_time:observe(ngx.now() - ngx.req.start_time(), {host}) 60 | 61 | http_request_bytes_sent:inc(tonumber(ngx.var.bytes_sent), {host}) 62 | if ngx.var.bytes_received ~= nil then 63 | http_request_bytes_received:inc(tonumber(ngx.var.bytes_received), {host}) 64 | end 65 | 66 | local upstream_cache_status = ngx.var.upstream_cache_status 67 | if upstream_cache_status ~= nil then 68 | http_upstream_cache_status:inc(1, {host, upstream_cache_status}) 69 | end 70 | 71 | local upstream_addr = ngx.var.upstream_addr 72 | if upstream_addr ~= nil then 73 | local addrs = split(upstream_addr) 74 | 75 | local upstream_status = ngx.var.upstream_status 76 | local upstream_response_time = ngx.var.upstream_response_time 77 | local upstream_connect_time = ngx.var.upstream_connect_time 78 | local upstream_first_byte_time = ngx.var.upstream_first_byte_time 79 | local upstream_header_time = ngx.var.upstream_header_time 80 | local upstream_session_time = ngx.var.upstream_session_time 81 | local upstream_bytes_received = ngx.var.upstream_bytes_received 82 | local upstream_bytes_sent = ngx.var.upstream_bytes_sent 83 | 84 | -- compatible for nginx commas format 85 | for idx, addr in ipairs(addrs) do 86 | if table.getn(addrs) > 1 then 87 | upstream_status = getWithIndex(ngx.var.upstream_status, idx) 88 | upstream_response_time = getWithIndex(ngx.var.upstream_response_time, idx) 89 | upstream_connect_time = getWithIndex(ngx.var.upstream_connect_time, idx) 90 | upstream_first_byte_time = getWithIndex(ngx.var.upstream_first_byte_time, idx) 91 | upstream_header_time = getWithIndex(ngx.var.upstream_header_time, idx) 92 | upstream_session_time = getWithIndex(ngx.var.upstream_session_time, idx) 93 | upstream_bytes_received = getWithIndex(ngx.var.upstream_bytes_received, idx) 94 | upstream_bytes_sent = getWithIndex(ngx.var.upstream_bytes_sent, idx) 95 | end 96 | 97 | http_upstream_requests:inc(1, {addr, upstream_status}) 98 | http_upstream_response_time:observe(tonumber(upstream_response_time), {addr}) 99 | http_upstream_header_time:observe(tonumber(upstream_header_time), {addr}) 100 | 101 | -- ngx.config.nginx_version >= 1011004 102 | if upstream_first_byte_time ~= nil then 103 | http_upstream_first_byte_time:observe(tonumber(upstream_first_byte_time), {addr}) 104 | end 105 | if upstream_connect_time ~= nil then 106 | http_upstream_connect_time:observe(tonumber(upstream_connect_time), {addr}) 107 | end 108 | if upstream_session_time ~= nil then 109 | http_upstream_session_time:observe(tonumber(upstream_session_time), {addr}) 110 | end 111 | if upstream_bytes_received ~= nil then 112 | http_upstream_bytes_received:inc(tonumber(upstream_bytes_received), {addr}) 113 | end 114 | if upstream_bytes_sent ~= nil then 115 | http_upstream_bytes_sent:inc(tonumber(upstream_bytes_sent), {addr}) 116 | end 117 | end 118 | end 119 | } 120 | 121 | server { 122 | listen 9527; 123 | #allow 192.168.0.0/16; 124 | #deny all; 125 | 126 | location /metrics { 127 | content_by_lua_block { 128 | if ngx.var.connections_active ~= nil then 129 | http_connections:set(ngx.var.connections_active, {"active"}) 130 | http_connections:set(ngx.var.connections_reading, {"reading"}) 131 | http_connections:set(ngx.var.connections_waiting, {"waiting"}) 132 | http_connections:set(ngx.var.connections_writing, {"writing"}) 133 | end 134 | prometheus:collect() 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | #include mime.types; 9 | default_type text/html; 10 | 11 | sendfile on; 12 | keepalive_timeout 65; 13 | 14 | server { 15 | listen 80; 16 | server_name localhost; 17 | 18 | location / { 19 | content_by_lua_block { 20 | ngx.say("hello world") 21 | } 22 | } 23 | } 24 | 25 | include *.vhost; 26 | } 27 | 28 | -------------------------------------------------------------------------------- /test.vhost: -------------------------------------------------------------------------------- 1 | upstream testservers { 2 | server 10.12.13.14; 3 | server 127.0.0.1; 4 | } 5 | 6 | server { 7 | listen 1314; 8 | server_name localhost; 9 | 10 | location / { 11 | proxy_pass http://testservers; 12 | proxy_connect_timeout 2s; 13 | } 14 | } --------------------------------------------------------------------------------