From 7169df7b88315cecd6bf343bc70303e4282a0f70 Mon Sep 17 00:00:00 2001 From: Seebomega Date: Fri, 3 Mar 2017 20:20:06 +0100 Subject: [PATCH] Prometheus lua metrics creation 0.14 --- control.lua | 27 ++ prometheus/Dockerfile | 4 + prometheus/LICENSE | 29 ++ prometheus/README.md | 257 ++++++++++++ prometheus/example.lua | 52 +++ prometheus/tarantool-metrics.lua | 79 ++++ .../tarantool-prometheus-scm-1.rockspec | 22 + prometheus/tarantool-prometheus.lua | 390 ++++++++++++++++++ prometheus/test.lua | 180 ++++++++ 9 files changed, 1040 insertions(+) create mode 100644 prometheus/Dockerfile create mode 100644 prometheus/LICENSE create mode 100644 prometheus/README.md create mode 100755 prometheus/example.lua create mode 100755 prometheus/tarantool-metrics.lua create mode 100644 prometheus/tarantool-prometheus-scm-1.rockspec create mode 100755 prometheus/tarantool-prometheus.lua create mode 100755 prometheus/test.lua diff --git a/control.lua b/control.lua index 5cffd1e..2dbfa00 100644 --- a/control.lua +++ b/control.lua @@ -3,6 +3,8 @@ TECH_NAME = "advanced-logistics-systems" require "gui" require "interface" +prometheus = require("prometheus/tarantool-prometheus") +gauge_objects = prometheus.gauge("factorio_objects_owned", "items owned by player", {"force", "item", "container"}) --- Enable/Disable Debugging local DEV = false @@ -77,6 +79,9 @@ script.on_event(defines.events.on_tick, function(event) end end end + if ( not initdone ) or ( event.tick % 600 == 0 ) then + writeMetrics() + end end) --- handles mod updates @@ -889,6 +894,15 @@ function getLogisticsItems(force, index) end end end + + for item_name, counts in pairs(items) do + for container_name, count in pairs(counts) do + if container_name ~= "total" then + gauge_objects:set(count, {force.name, item_name, container_name}) + end + end + end + global.logisticsItems[force.name] = items global.logisticsItemsTotal[force.name] = total return items @@ -958,6 +972,15 @@ function getNormalItems(force) end end end + + for item_name, counts in pairs(items) do + for container_name, count in pairs(counts) do + if container_name ~= "total" then + gauge_objects:set(count, {force.name, item_name, container_name}) + end + end + end + global.normalItems[force.name] = items global.normalItemsTotal[force.name] = total return items @@ -1306,3 +1329,7 @@ function debugLog(msg, force) end end end + +function writeMetrics() + game.write_file("metrics/als.prom", prometheus:collect(), false) +end \ No newline at end of file diff --git a/prometheus/Dockerfile b/prometheus/Dockerfile new file mode 100644 index 0000000..40edcc4 --- /dev/null +++ b/prometheus/Dockerfile @@ -0,0 +1,4 @@ +FROM tarantool/tarantool:1.7 + +COPY example.lua /opt/tarantool/ +CMD ["tarantool", "/opt/tarantool/example.lua"] diff --git a/prometheus/LICENSE b/prometheus/LICENSE new file mode 100644 index 0000000..366e1ed --- /dev/null +++ b/prometheus/LICENSE @@ -0,0 +1,29 @@ +Copyright (C) 2014-2016 Tarantool AUTHORS: +please see AUTHORS file in tarantool/tarantool repository. + +Redistribution and use in source and binary forms, with or +without modification, are permitted provided that the following +conditions are met: + +1. Redistributions of source code must retain the above + copyright notice, this list of conditions and the + following disclaimer. + +2. 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. + +THIS SOFTWARE IS PROVIDED BY AUTHORS ``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 +AUTHORS 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. diff --git a/prometheus/README.md b/prometheus/README.md new file mode 100644 index 0000000..a06c85a --- /dev/null +++ b/prometheus/README.md @@ -0,0 +1,257 @@ + + + + + +# Prometheus metric collector for Tarantool + +This is a Lua library that makes it easy to collect metrics from your Tarantool +apps and databases and expose them via the Prometheus protocol. You may use the +library to instrument your code and get an insight into performance bottlenecks. + +At the moment, 3 types of metrics are supported: +* Counter: a non-decreasing numeric value, used e.g. for counting the number of + requests +* Gauge: an arbitrary numeric value, which can be used e.g. to report memory + usage +* Histogram: for counting value distribution by user-specified buckets. Can be + used for recording request/response times. + +## Table of contents + +* [Limitations](#limitations) +* [Getting started](#getting-started) + * [Basic examples](#basic-examples) + * [A more detailed example](#a-more-detailed-example) +* [Usage](#usage) + * [counter(name, help, labels)](#countername-help-labels) + * [gauge(name, help, labels)](#gaugename-help-labels) + * [histogram(name, help, labels, buckets)](#histogramname-help-labels-buckets) + * [Counter:inc(value, labels)](#counterincvalue-labels) + * [Gauge:set(value, labels)](#gaugesetvalue-labels) + * [Gauge:inc(value, labels)](#gaugeincvalue-labels) + * [Gauge:dec(value, labels)](#gaugedecvalue-labels) + * [Histogram:observe(value, labels)](#histogramobservevalue-labels) + * [collect()](#collect) + * [collect\_http()](#collect_http) +* [Development](#development) +* [Credits](#credits) +* [License](#license) + +## Limitations + +The Summary metric is not implemented yet. It may be implemented in future. + +## Getting started + +The easiest way is, of course, to use +[one of the official Docker images](https://hub.docker.com/r/tarantool/tarantool/), +which already contain the Prometheus collector. But if you run on a regular +Linux distro, first install the library from +[Tarantool Rocks server](http://rocks.tarantool.org): + +```bash +$ luarocks install tarantool-prometheus +``` +### Basic examples + +To report the arena size, you can write the following code: + +```lua +prometheus = require('tarantool-prometheus') +fiber = require('fiber') + +box.cfg{} +httpd = http.new('0.0.0.0', 8080) + +arena_used = prometheus.gauge("tarantool_arena_used", + "The amount of arena used by Tarantool") + +function monitor_arena_size() + while true do + arena_used:set(box.slab.info().arena_used) + fiber.sleep(5) + end +end +fiber.create(monitor_arena_size) + +httpd:route( { path = '/metrics' }, prometheus.collect_http) +httpd:start() +``` + +The code will periodically measure the arena size and update the `arena_used` +metric. Later, when Prometheus polls the instance, it will get the values of all +metrics the instance created. + +There are 3 important bits in the code above: + +```lua +arena_used = prometheus.gauge(...) +``` + +This creates a [Gauge](https://prometheus.io/docs/concepts/metric_types/#gauge) +object that can be set to an arbitrary numeric value. After this, the metric from +this object will be automatically collected by Prometheus every time metrics are +polled. + +```lua +arena_used:set(...) +``` + +This sets the current value of the metric. + +```lua +httpd:route( { path = '/metrics' }, prometheus.collect_http) +``` + +This exposes metrics over the text/plain HTTP protocol on +[http://localhost:8080/metrics](http://localhost:8080/metrics) for Prometheus to +collect. Prometheus periodically polls this endpoint and stores the results in +its time series database. + +### A more detailed example + +If you want a more detailed example, there is an `example.lua` file in the root +of this repo. It demonstrates the usage of each of the 3 metric types. + +To run it with Docker, you can do as follows: + +``` bash +$ docker build -t tarantool-prometheus . +$ docker run --rm -t -i -p8080:8080 tarantool-prometheus +``` + +Then visit [http://localhost:8080/metrics](http://localhost:8080/metrics) and +refresh the page a few times to see the metrics change. + +## Usage + +This section documents the user-facing API of the module. + +### counter(name, help, labels) + +Creates and registers a [Counter](https://prometheus.io/docs/concepts/metric_types/#counter). + +* `name` is the name of the metric. Required. +* `help` is the metric docstring. You can use newlines and quotes here. Optional. +* `labels` is an array of label names for the metric. Optional. + +Example: + +```lua +num_of_logins = prometheus.counter( + "tarantool_number_of_logins", "Total number of user logins") + +http_requests = prometheus:counter( + "tarantool_http_requests_total", "Number of HTTP requests", {"host", "status"}) +``` + +### gauge(name, help, labels) + +Creates and registers a [Gauge](https://prometheus.io/docs/concepts/metric_types/#gauge). + +* `name` is the name of the metric. Required. +* `help` is the metric docstring. You can use newlines and quotes here. Optional. +* `labels` is an array of label names for the metric. Optional. + +Example: + +``` lua +arena_used = prometheus.gauge( + "tarantool_arena_used_size", "Total size of the arena used") + +requests_inprogress = prometheus.gauge( + "tarantool_requests_inprogress", "Number of requests in progress", {"request_type"}) +``` + +### histogram(name, help, labels, buckets) + +Creates and registers a [Histogram](https://prometheus.io/docs/concepts/metric_types/#histogram). + +* `name` is the name of the metric. Required. +* `help` is the metric docstring. You can use newlines and quotes here. Optional. +* `labels` is an array of label names for the metric. Optional. +* `buckets` is an array of numbers defining histogram buckets. Optional. Defaults to + `{.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, INF}`. + +Example: + +``` lua +request_latency = prometheus.histogram( + "tarantool_request_latency_seconds", "Incoming request latency", {"client"}) +response_size = prometheus.histogram( + "tarantool_response_size", "Size of response, in bytes", nil, {100, 1000, 100000}) +``` + +### Counter:inc(value, labels) + +Increments a counter created by `prometheus.counter()`. + +* `value` specifies by how much to increment. Optional. Defaults to `1`. +* `labels` is an array of label values. Optional. + +### Gauge:set(value, labels) + +Sets a value of a gauge created by `prometheus.gauge()`. + +* `value` is the value to set. Optional. Defaults to `0`. +* `labels` is an array of label values. Optional. + +### Gauge:inc(value, labels) + +Increments a gauge created by `prometheus.gauge()`. + +* `value` specifies by how much to increment. Optional. Defaults to `1`. +* `labels` is an array of label values. Optional. + +### Gauge:dec(value, labels) + +Decrements a gauge created by `prometheus.gauge()`. + +* `value` specifies by how much to decrement. Optional. Defaults to `1`. +* `labels` is an array of label values. Optional. + +### Histogram:observe(value, labels) + +Records a value to a histogram created by `prometheus.histogram()`. + +* `value` is the value to record. Optional. Defaults to `0`. +* `labels` is an array of label values. Optional. + +### collect() + +Presents all metrics in a text format compatible with Prometheus. This can be +called either by `http.server` callback or by +[Tarantool nginx_upstream_module](https://github.com/tarantool/nginx_upstream_module). + +### collect_http() + +Convenience function, specially for one-line registration in the Tarantool +`http.server`, as follows: + +```lua +httpd:route( { path = '/metrics' }, prometheus.collect_http) +``` + +## Development + +Contributions are welcome. Report issues and feature requests at +https://github.com/tarantool/tarantool-prometheus/issues + +To run tests, do: + +```bash +$ tarantool test.lua +``` + +NB: Tests require `luaunit` library. + +## Credits + +Loosely based on the implementation by @knyar: https://github.com/knyar/nginx-lua-prometheus + +## License + +Licensed under the BSD license. See the LICENSE file. \ No newline at end of file diff --git a/prometheus/example.lua b/prometheus/example.lua new file mode 100755 index 0000000..67af396 --- /dev/null +++ b/prometheus/example.lua @@ -0,0 +1,52 @@ +#!/usr/bin/env tarantool + +http = require('http.server') +prometheus = require('tarantool-prometheus') +fiber = require('fiber') + +box.cfg{} +prometheus.init() + +httpd = http.new('0.0.0.0', 8080) + +space = box.schema.space.create("test_space") +space:create_index('primary', {type = 'hash', parts = {1, 'NUM'}}) + +function random_write() + num = math.random(10000) + + box.space.test_space:truncate() + for i=0,num do + box.space.test_space:insert({i, tostring(i)}) + end +end + +function worker() + exec_count = prometheus.counter("tarantool_worker_execution_count", + "Number of times worker process has been executed") + exec_time = prometheus.histogram("tarantool_worker_execution_time", + "Time of each worker process execution") + arena_used = prometheus.gauge("tarantool_arena_used", + "The amount of arena used by Tarantool") + + + while true do + time_start = fiber.time() + random_write() + time_end = fiber.time() + + exec_time:observe(time_end - time_start) + exec_count:inc() + arena_used:set(box.slab.info().arena_used) + + fiber.sleep(1) + end + +end + + + +httpd:route( { path = '/metrics' }, prometheus.collect_http) + +httpd:start() +fiber.create(worker) diff --git a/prometheus/tarantool-metrics.lua b/prometheus/tarantool-metrics.lua new file mode 100755 index 0000000..11a888e --- /dev/null +++ b/prometheus/tarantool-metrics.lua @@ -0,0 +1,79 @@ +#!/usr/bin/env tarantool + +local prometheus = require('tarantool-prometheus') + +local memory_limit_bytes = prometheus.gauge( + 'tarantool_memory_limit_bytes', + 'Maximum amount of memory Tarantool can use') +local memory_used_bytes = prometheus.gauge( + 'tarantool_memory_used_bytes', + 'Amount of memory currently used by Tarantool') +local tuples_memory_bytes = prometheus.gauge( + 'tarantool_tuples_memory_bytes', + 'Amount of memory allocated for Tarantool tuples') +local system_memory_bytes = prometheus.gauge( + 'tarantool_system_memory_bytes', + 'Amount of memory used by Tarantool indexes and system') + +local requests_total = prometheus.gauge( + 'tarantool_requests_total', + 'Total number of requests by request type', + {'request_type'}) + +local uptime_seconds = prometheus.gauge( + 'tarantool_uptime_seconds', + 'Number of seconds since the server started') + +local tuples_total = prometheus.gauge( + 'tarantool_space_tuples_total', + 'Total number of tuples in a space', + {'space_name'}) + + +local function measure_tarantool_memory_usage() + local slabs = box.slab.info() + local memory_limit = slabs.quota_size + local memory_used = slabs.quota_used + local tuples_memory = slabs.arena_used + local system_memory = memory_used - tuples_memory + + memory_limit_bytes:set(memory_limit) + memory_used_bytes:set(memory_used) + tuples_memory_bytes:set(tuples_memory) + system_memory_bytes:set(system_memory) +end + +local function measure_tarantool_request_stats() + local stat = box.stat() + local request_types = {'delete', 'select', 'insert', 'eval', 'call', + 'replace', 'upsert', 'auth', 'error', 'update'} + + for _, request_type in ipairs(request_types) do + requests_total:set(stat[string.upper(request_type)].total, + {request_type}) + end +end + +local function measure_tarantool_uptime() + uptime_seconds:set(box.info.uptime) +end + +local function measure_tarantool_space_stats() + for _, space in box.space._space:pairs() do + local space_name = space[3] + + if string.sub(space_name, 1,1) ~= '_' then + tuples_total:set(box.space[space_name]:len(), {space_name}) + end + end + +end + +local function measure_tarantool_metrics() + measure_tarantool_memory_usage() + measure_tarantool_request_stats() + measure_tarantool_uptime() + measure_tarantool_space_stats() +end + +return {measure_tarantool_metrics=measure_tarantool_metrics} diff --git a/prometheus/tarantool-prometheus-scm-1.rockspec b/prometheus/tarantool-prometheus-scm-1.rockspec new file mode 100644 index 0000000..1e89ecc --- /dev/null +++ b/prometheus/tarantool-prometheus-scm-1.rockspec @@ -0,0 +1,22 @@ +package = 'tarantool-prometheus' +version = 'scm-1' +source = { + url = 'git://github.com/tarantool/prometheus.git', + branch = 'master', +} +description = { + summary = 'Prometheus library to collect metrics from Tarantool', + homepage = 'https://github.com/tarantool/prometheus.git', + license = 'MIT', +} +dependencies = { + 'lua >= 5.1'; +} +build = { + type = 'builtin', + + modules = { + ['tarantool-prometheus.tarantool-metrics'] = 'tarantool-metrics.lua', + ['tarantool-prometheus'] = 'tarantool-prometheus.lua' + } +} diff --git a/prometheus/tarantool-prometheus.lua b/prometheus/tarantool-prometheus.lua new file mode 100755 index 0000000..4128102 --- /dev/null +++ b/prometheus/tarantool-prometheus.lua @@ -0,0 +1,390 @@ +-- vim: ts=2:sw=2:sts=2:expandtab + +local INF = math.huge +local NAN = math.huge * 0 +local DEFAULT_BUCKETS = {.005, .01, .025, .05, .075, .1, .25, .5, + .75, 1.0, 2.5, 5.0, 7.5, 10.0, INF} + +local REGISTRY = nil + +local Registry = {} +Registry.__index = Registry + +function Registry.new() + local obj = {} + setmetatable(obj, Registry) + obj.collectors = {} + obj.callbacks = {} + return obj +end + +function Registry:register(collector) + if self.collectors[collector.name]~=nil then + return self.collectors[collector.name] + end + self.collectors[collector.name] = collector + return collector +end + +function Registry:unregister(collector) + if self.collectors[collector.name]~=nil then + table.remove(self.collectors, collector.name) + end +end + +function Registry:collect() + for _, registered_callback in ipairs(self.callbacks) do + registered_callback() + end + + local result = {} + for _, collector in pairs(self.collectors) do + for _, metric in ipairs(collector:collect()) do + table.insert(result, metric) + end + table.insert(result, '') + end + return result +end + +function Registry:register_callback(callback) + local found = false + for _, registered_callback in ipairs(self.callbacks) do + if registered_callback == calback then + found = true + end + end + if not found then + table.insert(self.callbacks, callback) + end +end + +local function get_registry() + if not REGISTRY then + REGISTRY = Registry.new() + end + return REGISTRY +end + +local function register(collector) + local registry = get_registry() + registry:register(collector) + + return collector +end + +local function register_callback(callback) + local registry = get_registry() + registry:register_callback(callback) +end + +function zip(lhs, rhs) + if lhs == nil or rhs == nil then + return {} + end + + local len = math.min(#lhs, #rhs) + local result = {} + for i=1,len do + table.insert(result, {lhs[i], rhs[i]}) + end + return result +end + +local function metric_to_string(value) + if value == INF then + return "+Inf" + elseif value == -INF then + return "-Inf" + elseif value ~= value then + return "Nan" + else + return tostring(value) + end +end + +local function escape_string(str) + return str + :gsub("\\", "\\\\") + :gsub("\n", "\\n") + :gsub('"', '\\"') +end + +local function labels_to_string(label_pairs) + if #label_pairs == 0 then + return "" + end + local label_parts = {} + for _, label in ipairs(label_pairs) do + local label_name = label[1] + local label_value = label[2] + local label_value_escaped = escape_string(string.format("%s", label_value)) + table.insert(label_parts, label_name .. '="' .. label_value_escaped .. '"') + end + return "{" .. table.concat(label_parts, ",") .. "}" +end + + +local Counter = {} +Counter.__index = Counter + +function Counter.new(name, help, labels) + local obj = {} + setmetatable(obj, Counter) + if not name then + error("Name should be set for Counter") + end + obj.name = name + obj.help = help or "" + obj.labels = labels or {} + obj.observations = {} + obj.label_values = {} + + return obj +end + +function Counter:inc(num, label_values) + local num = num or 1 + local label_values = label_values or {} + if num < 0 then + error("Counter increment should not be negative") + end + local key = table.concat(label_values, '\0') + local old_value = self.observations[key] or 0 + self.observations[key] = old_value + num + self.label_values[key] = label_values +end + +function Counter:collect() + local result = {} + + if next(self.observations) == nil then + return {} + end + + table.insert(result, '# HELP '..self.name..' '..escape_string(self.help)) + table.insert(result, "# TYPE "..self.name.." counter") + + for key, observation in pairs(self.observations) do + local label_values = self.label_values[key] + local prefix = self.name + local labels = zip(self.labels, label_values) + + local str = prefix..labels_to_string(labels).. + ' '..metric_to_string(observation) + table.insert(result, str) + end + + return result +end + + +local Gauge = {} +Gauge.__index = Gauge + +function Gauge.new(name, help, labels) + local obj = {} + setmetatable(obj, Gauge) + if not name then + error("Name should be set for Gauge") + end + obj.name = name + obj.help = help or "" + obj.labels = labels or {} + obj.observations = {} + obj.label_values = {} + + return obj +end + +function Gauge:inc(num, label_values) + local num = num or 1 + local label_values = label_values or {} + local key = table.concat(label_values, '\0') + local old_value = self.observations[key] or 0 + self.observations[key] = old_value + num + self.label_values[key] = label_values +end + +function Gauge:dec(num, label_values) + local num = num or 1 + local label_values = label_values or {} + local key = table.concat(label_values, '\0') + local old_value = self.observations[key] or 0 + self.observations[key] = old_value - num + self.label_values[key] = label_values +end + +function Gauge:set(num, label_values) + local num = num or 0 + local label_values = label_values or {} + local key = table.concat(label_values, '\0') + self.observations[key] = num + self.label_values[key] = label_values +end + +function Gauge:collect() + local result = {} + + if next(self.observations) == nil then + return {} + end + + table.insert(result, '# HELP '..self.name..' '..escape_string(self.help)) + table.insert(result, "# TYPE "..self.name.." gauge") + + for key, observation in pairs(self.observations) do + local label_values = self.label_values[key] + local prefix = self.name + local labels = zip(self.labels, label_values) + + local str = prefix..labels_to_string(labels).. + ' '..metric_to_string(observation) + table.insert(result, str) + end + + return result +end + +local Histogram = {} +Histogram.__index = Histogram + +function Histogram.new(name, help, labels, + buckets) + local obj = {} + setmetatable(obj, Histogram) + if not name then + error("Name should be set for Histogram") + end + obj.name = name + obj.help = help or "" + obj.labels = labels or {} + obj.buckets = buckets or DEFAULT_BUCKETS + table.sort(obj.buckets) + if obj.buckets[#obj.buckets] ~= INF then + obj.buckets[#obj.buckets+1] = INF + end + obj.observations = {} + obj.label_values = {} + obj.counts = {} + obj.sums = {} + + return obj +end + +function Histogram:observe(num, label_values) + local num = num or 0 + local label_values = label_values or {} + local key = table.concat(label_values, '\0') + + local obs = nil + if self.observations[key] == nil then + obs = {} + for i=1, #self.buckets do + obs[i] = 0 + end + self.observations[key] = obs + self.label_values[key] = label_values + self.counts[key] = 0 + self.sums[key] = 0 + else + obs = self.observations[key] + end + + self.counts[key] = self.counts[key] + 1 + self.sums[key] = self.sums[key] + num + for i, bucket in ipairs(self.buckets) do + if num <= bucket then + obs[i] = obs[i] + 1 + end + end +end + + +function Histogram:collect() + local result = {} + + if next(self.observations) == nil then + return {} + end + + table.insert(result, '# HELP '..self.name..' '..escape_string(self.help)) + table.insert(result, "# TYPE "..self.name.." histogram") + + for key, observation in pairs(self.observations) do + local label_values = self.label_values[key] + local prefix = self.name + local labels = zip(self.labels, label_values) + labels[#labels+1] = {le="0"} + for i, bucket in ipairs(self.buckets) do + labels[#labels] = {"le", metric_to_string(bucket)} + str = prefix.."_bucket"..labels_to_string(labels).. + ' '..metric_to_string(observation[i]) + table.insert(result, str) + end + table.remove(labels, #labels) + + table.insert(result, + prefix.."_sum"..labels_to_string(labels)..' '..self.sums[key]) + table.insert(result, + prefix.."_count"..labels_to_string(labels)..' '..self.counts[key]) + end + + return result +end + + +-- #################### Public API #################### + + +local function counter(name, help, labels) + local obj = Counter.new(name, help, labels) + obj = register(obj) + return obj +end + +local function gauge(name, help, labels) + local obj = Gauge.new(name, help, labels) + obj = register(obj) + return obj +end + +local function histogram(name, help, labels, buckets) + local obj = Histogram.new(name, help, labels, buckets) + obj = register(obj) + return obj +end + +local function collect() + local registry = get_registry() + + return table.concat(registry:collect(), '\n')..'\n' +end + +local function collect_http() + return { + status = 200, + headers = { ['content-type'] = 'text/plain; charset=utf8' }, + body = collect() + } +end + +local function clear() + local registry = get_registry() + registry.collectors = {} + registry.callbacks = {} +end + +local function init() + local registry = get_registry() + local tarantool_metrics = require('tarantool-prometheus.tarantool-metrics') + registry:register_callback(tarantool_metrics.measure_tarantool_metrics) +end + +return {counter=counter, + gauge=gauge, + histogram=histogram, + collect=collect, + collect_http=collect_http, + clear=clear, + init=init} diff --git a/prometheus/test.lua b/prometheus/test.lua new file mode 100755 index 0000000..1875eb9 --- /dev/null +++ b/prometheus/test.lua @@ -0,0 +1,180 @@ +#!/usr/bin/env tarantool + +luaunit = require('luaunit') +prometheus = require('tarantool-prometheus') + + +TestPrometheus = {} + +function TestPrometheus:tearDown() + prometheus.clear() +end + +function TestPrometheus:testCounterNegativeValue() + c = prometheus.counter("counter") + luaunit.assertErrorMsgContains("should not be negative", c.inc, c, -1) +end + +function TestPrometheus:testLabelNames() + c = prometheus.counter("counter", "", {'a1', 'foo', "var"}) + c:inc(1, {1, '2', 'q4'}) + + r = c:collect() + luaunit.assertEquals(r[3], 'counter{a1="1",foo="2",var="q4"} 1') +end + +function TestPrometheus:testLabelEscape() + c = prometheus.counter("counter", "", {'a1', 'foo', "var"}) + c:inc(1, {'"', '\\a', '\n'}) + + r = c:collect() + luaunit.assertEquals(r[3], 'counter{a1="\\"",foo="\\\\a",var="\\n"} 1') +end + +function TestPrometheus:testHelpEscape() + c = prometheus.counter("counter", "some\" escaped\\strings\n") + c:inc(1, {'"', '\\a', '\n'}) + + r = c:collect() + luaunit.assertEquals(r[1], '# HELP counter some\\" escaped\\\\strings\\n') +end + +function TestPrometheus:testCounters() + first = prometheus.counter("counter1", "", {"a", "b"}) + second = prometheus.counter("counter2", "", {"a", "b"}) + + first:inc() + first:inc(4) + + second:inc(1, {"v1", "v2"}) + second:inc(3, {"v1", "v3"}) + second:inc(2, {"v1", "v3"}) + + r = first:collect() + luaunit.assertEquals(r[1], "# HELP counter1 ") + luaunit.assertEquals(r[2], "# TYPE counter1 counter") + luaunit.assertEquals(r[3], "counter1 5") + luaunit.assertEquals(r[4], nil) + + + r = second:collect() + luaunit.assertEquals(r[3], 'counter2{a="v1",b="v2"} 1') + luaunit.assertEquals(r[4], 'counter2{a="v1",b="v3"} 5') + luaunit.assertEquals(r[5], nil) + +end + +function TestPrometheus:testGauge() + first = prometheus.gauge("gauge1", "", {"a", "b"}) + second = prometheus.gauge("gauge2", "", {"a", "b"}) + + first:inc() + first:inc(4) + first:set(2) + first:dec() + + second:set(1, {"v1", "v2"}) + second:inc(3, {"v1", "v3"}) + second:dec(1, {"v1", "v3"}) + second:inc(0, {"v1", "v3"}) + + r = first:collect() + luaunit.assertEquals(r[1], "# HELP gauge1 ") + luaunit.assertEquals(r[2], "# TYPE gauge1 gauge") + luaunit.assertEquals(r[3], "gauge1 1") + luaunit.assertEquals(r[4], nil) + + + r = second:collect() + luaunit.assertEquals(r[3], 'gauge2{a="v1",b="v2"} 1') + luaunit.assertEquals(r[4], 'gauge2{a="v1",b="v3"} 2') + luaunit.assertEquals(r[5], nil) + +end + +function TestPrometheus:testSpecialValues() + gauge = prometheus.gauge("gauge") + + gauge:set(math.huge) + r = gauge:collect() + luaunit.assertEquals(r[3], "gauge +Inf") + + gauge:set(-math.huge) + r = gauge:collect() + luaunit.assertEquals(r[3], "gauge -Inf") + + gauge:set(math.huge * 0) + r = gauge:collect() + luaunit.assertEquals(r[3], "gauge Nan") +end + + + +function TestPrometheus:testHistogram() + hist1 = prometheus.histogram("l1", "Histogram 1") + hist2 = prometheus.histogram("l2", "Histogram 2", {"var", "site"}, {0.1, 0.2}) + hist3 = prometheus.histogram("l3", "Histogram 3", {}) + + hist1:observe(0.35) + hist1:observe(0.9) + hist1:observe(5) + hist1:observe(15) + + hist2:observe(0.001, {"ok", "site1"}) + hist2:observe(0.15, {"ok", "site1"}) + hist2:observe(0.15, {"ok", "site2"}) + + r = hist1:collect() + luaunit.assertEquals(r[1], "# HELP l1 Histogram 1") + luaunit.assertEquals(r[2], "# TYPE l1 histogram") + luaunit.assertEquals(r[3], 'l1_bucket{le="0.005"} 0') + luaunit.assertEquals(r[4], 'l1_bucket{le="0.01"} 0') + luaunit.assertEquals(r[5], 'l1_bucket{le="0.025"} 0') + luaunit.assertEquals(r[6], 'l1_bucket{le="0.05"} 0') + luaunit.assertEquals(r[7], 'l1_bucket{le="0.075"} 0') + luaunit.assertEquals(r[8], 'l1_bucket{le="0.1"} 0') + luaunit.assertEquals(r[9], 'l1_bucket{le="0.25"} 0') + luaunit.assertEquals(r[10], 'l1_bucket{le="0.5"} 1') + luaunit.assertEquals(r[11], 'l1_bucket{le="0.75"} 1') + luaunit.assertEquals(r[12], 'l1_bucket{le="1"} 2') + luaunit.assertEquals(r[13], 'l1_bucket{le="2.5"} 2') + luaunit.assertEquals(r[14], 'l1_bucket{le="5"} 3') + luaunit.assertEquals(r[15], 'l1_bucket{le="7.5"} 3') + luaunit.assertEquals(r[16], 'l1_bucket{le="10"} 3') + luaunit.assertEquals(r[17], 'l1_bucket{le="+Inf"} 4') + luaunit.assertEquals(r[18], 'l1_sum 21.25') + luaunit.assertEquals(r[19], 'l1_count 4') + + r = hist2:collect() + luaunit.assertEquals(r[3], 'l2_bucket{var="ok",site="site1",le="0.1"} 1') + luaunit.assertEquals(r[4], 'l2_bucket{var="ok",site="site1",le="0.2"} 2') + luaunit.assertEquals(r[5], 'l2_bucket{var="ok",site="site1",le="+Inf"} 2') + luaunit.assertEquals(r[6], 'l2_sum{var="ok",site="site1"} 0.151') + luaunit.assertEquals(r[7], 'l2_count{var="ok",site="site1"} 2') + luaunit.assertEquals(r[8], 'l2_bucket{var="ok",site="site2",le="0.1"} 0') + luaunit.assertEquals(r[9], 'l2_bucket{var="ok",site="site2",le="0.2"} 1') + luaunit.assertEquals(r[10], 'l2_bucket{var="ok",site="site2",le="+Inf"} 1') + luaunit.assertEquals(r[11], 'l2_sum{var="ok",site="site2"} 0.15') + luaunit.assertEquals(r[12], 'l2_count{var="ok",site="site2"} 1') + + r = hist3:collect() + luaunit.assertEquals(r[3], nil) +end + +function TestPrometheus:testHistogramUnorderedBuckets() + hist = prometheus.histogram("l2", "Histogram 2", {}, {0.2, 0.1, 0.5}) + + hist:observe(0.15) + hist:observe(0.4) + + r = hist:collect() + luaunit.assertEquals(r[3], 'l2_bucket{le="0.1"} 0') + luaunit.assertEquals(r[4], 'l2_bucket{le="0.2"} 1') + luaunit.assertEquals(r[5], 'l2_bucket{le="0.5"} 2') + luaunit.assertEquals(r[6], 'l2_bucket{le="+Inf"} 2') + luaunit.assertEquals(r[7], 'l2_sum 0.55') + luaunit.assertEquals(r[8], 'l2_count 2') + luaunit.assertEquals(r[9], nil) +end + +os.exit(luaunit.run())