├── .travis.yml ├── deps.sh ├── AUTHORS ├── Makefile ├── Dockerfile.test ├── Dockerfile ├── .gitignore ├── CHANGELOG.md ├── rockspecs ├── prometheus-1.0.0-1.rockspec └── prometheus-scm-1.rockspec ├── LICENSE ├── example.lua ├── tarantool-metrics.lua ├── test.lua ├── README.md └── prometheus.lua /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | services: 4 | - docker 5 | 6 | script: 7 | - make docker_test 8 | -------------------------------------------------------------------------------- /deps.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | tarantoolctl rocks install https://luarocks.org/manifests/bluebird75/luaunit-3.2.1-1.rockspec 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # 2 | # Below is complete list of people, who contributed their 3 | # code. 4 | # 5 | # NOTE: If you can commit a change this list, please do not hesitate 6 | # to add your name to it. 7 | # 8 | 9 | Konstantin Nazarov 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_IMAGE:=document_test 2 | 3 | clean: 4 | rm -rf .test 5 | 6 | docker_test: 7 | docker build -t ${DOCKER_IMAGE} -f Dockerfile.test . 8 | docker run \ 9 | --rm=true --tty=true \ 10 | ${DOCKER_IMAGE} \ 11 | tarantool /opt/tarantool/test.lua 12 | 13 | test: 14 | tarantool test.lua 15 | -------------------------------------------------------------------------------- /Dockerfile.test: -------------------------------------------------------------------------------- 1 | FROM tarantool/tarantool:1.7 2 | 3 | COPY . /opt/tarantool/ 4 | 5 | WORKDIR /opt/tarantool 6 | 7 | RUN set -x \ 8 | && apk add --no-cache --virtual .build-deps \ 9 | git\ 10 | && ./deps.sh \ 11 | && tarantoolctl rocks make rockspecs/prometheus-scm-1.rockspec \ 12 | && : "---------- remove build deps ----------" \ 13 | && apk del .build-deps 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tarantool/tarantool:1.7 2 | 3 | COPY . /opt/tarantool/ 4 | 5 | WORKDIR /opt/tarantool 6 | 7 | RUN set -x \ 8 | && apk add --no-cache --virtual .build-deps \ 9 | git\ 10 | && tarantoolctl rocks make rockspecs/prometheus-scm-1.rockspec \ 11 | && : "---------- remove build deps ----------" \ 12 | && apk del .build-deps 13 | 14 | CMD ["tarantool", "/opt/tarantool/example.lua"] 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Lua sources 2 | luac.out 3 | 4 | # luarocks build files 5 | *.src.rock 6 | *.zip 7 | *.tar.gz 8 | 9 | # Object files 10 | *.o 11 | *.os 12 | *.ko 13 | *.obj 14 | *.elf 15 | 16 | # Precompiled Headers 17 | *.gch 18 | *.pch 19 | 20 | # Libraries 21 | *.lib 22 | *.a 23 | *.la 24 | *.lo 25 | *.def 26 | *.exp 27 | 28 | # Shared objects (inc. Windows DLLs) 29 | *.dll 30 | *.so 31 | *.so.* 32 | *.dylib 33 | 34 | # Executables 35 | *.exe 36 | *.out 37 | *.app 38 | *.i*86 39 | *.x86_64 40 | *.hex 41 | 42 | *.snap 43 | *.xlog 44 | .rocks -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [1.0.0] - 2018-01-10 9 | ### Added 10 | - Support for counter, gauge and histogram metrics 11 | - Exporting metrics to a prometheus plaintext format 12 | - Serving metrics via tarantool 'http' module 13 | - Collecting basic tarantool stats: memory, request count and tuple counts by space 14 | - Luarock-based packaging 15 | - Basic unit tests 16 | -------------------------------------------------------------------------------- /rockspecs/prometheus-1.0.0-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'prometheus' 2 | version = '1.0.0-1' 3 | source = { 4 | url = 'git://github.com/tarantool/prometheus.git', 5 | tag = '1.0.0', 6 | } 7 | description = { 8 | summary = 'Prometheus library to collect metrics from Tarantool', 9 | homepage = 'https://github.com/tarantool/prometheus.git', 10 | license = 'BSD', 11 | } 12 | dependencies = { 13 | 'lua >= 5.1'; 14 | } 15 | build = { 16 | type = 'builtin', 17 | 18 | modules = { 19 | ['prometheus.tarantool-metrics'] = 'tarantool-metrics.lua', 20 | ['prometheus'] = 'prometheus.lua' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /rockspecs/prometheus-scm-1.rockspec: -------------------------------------------------------------------------------- 1 | package = 'prometheus' 2 | version = 'scm-1' 3 | source = { 4 | url = 'git://github.com/tarantool/prometheus.git', 5 | branch = 'master', 6 | } 7 | description = { 8 | summary = 'Prometheus library to collect metrics from Tarantool', 9 | homepage = 'https://github.com/tarantool/prometheus.git', 10 | license = 'BSD', 11 | } 12 | dependencies = { 13 | 'lua >= 5.1'; 14 | } 15 | build = { 16 | type = 'builtin', 17 | 18 | modules = { 19 | ['prometheus.tarantool-metrics'] = 'tarantool-metrics.lua', 20 | ['prometheus'] = 'prometheus.lua' 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2010-2018, Tarantool AUTHORS: please see AUTHORS file. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /example.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | -- luacheck: globals box 3 | 4 | local http = require('http.server') 5 | local prometheus = require('prometheus') 6 | local fiber = require('fiber') 7 | 8 | box.cfg{} 9 | prometheus.init() 10 | 11 | local httpd = http.new('0.0.0.0', 8080) 12 | 13 | local space = box.schema.space.create("test_space") 14 | space:create_index('primary', {type = 'hash', parts = {1, 'NUM'}}) 15 | 16 | local function random_write() 17 | local num = math.random(10000) 18 | 19 | box.space.test_space:truncate() 20 | for i=0,num do 21 | box.space.test_space:insert({i, tostring(i)}) 22 | end 23 | end 24 | 25 | local function worker() 26 | local exec_count = prometheus.counter("tarantool_worker_execution_count", 27 | "Number of times worker process has been executed") 28 | local exec_time = prometheus.histogram("tarantool_worker_execution_time", 29 | "Time of each worker process execution") 30 | local arena_used = prometheus.gauge("tarantool_arena_used", 31 | "The amount of arena used by Tarantool") 32 | 33 | 34 | while true do 35 | local time_start = fiber.time() 36 | random_write() 37 | local time_end = fiber.time() 38 | 39 | exec_time:observe(time_end - time_start) 40 | exec_count:inc() 41 | arena_used:set(box.slab.info().arena_used) 42 | 43 | fiber.sleep(1) 44 | end 45 | 46 | end 47 | 48 | 49 | 50 | httpd:route( { path = '/metrics' }, prometheus.collect_http) 51 | 52 | httpd:start() 53 | fiber.create(worker) 54 | -------------------------------------------------------------------------------- /tarantool-metrics.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | -- luacheck: globals box 3 | 4 | local prometheus = require('prometheus') 5 | 6 | local memory_limit_bytes = prometheus.gauge( 7 | 'tarantool_memory_limit_bytes', 8 | 'Maximum amount of memory Tarantool can use') 9 | local memory_used_bytes = prometheus.gauge( 10 | 'tarantool_memory_used_bytes', 11 | 'Amount of memory currently used by Tarantool') 12 | local tuples_memory_bytes = prometheus.gauge( 13 | 'tarantool_tuples_memory_bytes', 14 | 'Amount of memory allocated for Tarantool tuples') 15 | local system_memory_bytes = prometheus.gauge( 16 | 'tarantool_system_memory_bytes', 17 | 'Amount of memory used by Tarantool indexes and system') 18 | 19 | local requests_total = prometheus.gauge( 20 | 'tarantool_requests_total', 21 | 'Total number of requests by request type', 22 | {'request_type'}) 23 | 24 | local uptime_seconds = prometheus.gauge( 25 | 'tarantool_uptime_seconds', 26 | 'Number of seconds since the server started') 27 | 28 | local tuples_total = prometheus.gauge( 29 | 'tarantool_space_tuples_total', 30 | 'Total number of tuples in a space', 31 | {'space_name'}) 32 | 33 | local replication_lag = prometheus.gauge( 34 | 'tarantool_replication_lag', 35 | 'The time difference between the instance and the master', 36 | {'uuid'}) 37 | local replication_state_normal = prometheus.gauge( 38 | 'tarantool_is_replication_healthy', 39 | 'Is replication healthy?') 40 | 41 | 42 | local function measure_tarantool_memory_usage() 43 | local slabs = box.slab.info() 44 | local memory_limit = slabs.quota_size 45 | local memory_used = slabs.quota_used 46 | local tuples_memory = slabs.arena_used 47 | local system_memory = memory_used - tuples_memory 48 | 49 | memory_limit_bytes:set(memory_limit) 50 | memory_used_bytes:set(memory_used) 51 | tuples_memory_bytes:set(tuples_memory) 52 | system_memory_bytes:set(system_memory) 53 | end 54 | 55 | local function measure_tarantool_request_stats() 56 | local stat = box.stat() 57 | local request_types = {'delete', 'select', 'insert', 'eval', 'call', 58 | 'replace', 'upsert', 'auth', 'error', 'update'} 59 | 60 | for _, request_type in ipairs(request_types) do 61 | requests_total:set(stat[string.upper(request_type)].total, 62 | {request_type}) 63 | end 64 | end 65 | 66 | local function measure_tarantool_uptime() 67 | uptime_seconds:set(box.info.uptime) 68 | end 69 | 70 | local function measure_tarantool_space_stats() 71 | for _, space in box.space._space:pairs() do 72 | local space_name = space[3] 73 | 74 | if string.sub(space_name, 1,1) ~= '_' then 75 | tuples_total:set(box.space[space_name]:len(), {space_name}) 76 | end 77 | end 78 | end 79 | 80 | local function measure_tarantool_replication_lag() 81 | local idle = 0 82 | 83 | for _, replica in ipairs(box.info.replication) do 84 | if replica.upstream ~= nil then 85 | replication_lag:set(replica.upstream.lag, { replica.uuid }) 86 | if replica.upstream.idle > idle then 87 | idle = replica.upstream.idle 88 | end 89 | end 90 | end 91 | 92 | if idle ~= 0 then 93 | local replication_timeout = box.cfg.replication_timeout 94 | if idle <= replication_timeout then 95 | replication_state_normal:set(1) 96 | else 97 | replication_state_normal:set(0) 98 | end 99 | end 100 | end 101 | 102 | local function measure_tarantool_metrics() 103 | if type(box.cfg) ~= 'function' then 104 | measure_tarantool_memory_usage() 105 | measure_tarantool_request_stats() 106 | measure_tarantool_uptime() 107 | measure_tarantool_space_stats() 108 | measure_tarantool_replication_lag() 109 | end 110 | end 111 | 112 | return {measure_tarantool_metrics=measure_tarantool_metrics} 113 | -------------------------------------------------------------------------------- /test.lua: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env tarantool 2 | 3 | local luaunit = require('luaunit') 4 | local prometheus = require('prometheus') 5 | 6 | TestPrometheus = {} 7 | 8 | function TestPrometheus.tearDown() 9 | prometheus.clear() 10 | end 11 | 12 | function TestPrometheus.testCounterNegativeValue() 13 | local c = prometheus.counter("counter") 14 | luaunit.assertErrorMsgContains("should not be negative", c.inc, c, -1) 15 | end 16 | 17 | function TestPrometheus.testLabelNames() 18 | local c = prometheus.counter("counter", "", {'a1', 'foo', "var"}) 19 | c:inc(1, {1, '2', 'q4'}) 20 | 21 | local r = c:collect() 22 | luaunit.assertEquals(r[3], 'counter{a1="1",foo="2",var="q4"} 1') 23 | end 24 | 25 | function TestPrometheus.testLabelEscape() 26 | local c = prometheus.counter("counter", "", {'a1', 'foo', "var"}) 27 | c:inc(1, {'"', '\\a', '\n'}) 28 | 29 | local r = c:collect() 30 | luaunit.assertEquals(r[3], 'counter{a1="\\"",foo="\\\\a",var="\\n"} 1') 31 | end 32 | 33 | function TestPrometheus.testHelpEscape() 34 | local c = prometheus.counter("counter", "some\" escaped\\strings\n") 35 | c:inc(1, {'"', '\\a', '\n'}) 36 | 37 | local r = c:collect() 38 | luaunit.assertEquals(r[1], '# HELP counter some\\" escaped\\\\strings\\n') 39 | end 40 | 41 | function TestPrometheus.testCounters() 42 | local first = prometheus.counter("counter1", "", {"a", "b"}) 43 | local second = prometheus.counter("counter2", "", {"a", "b"}) 44 | 45 | first:inc() 46 | first:inc(4) 47 | 48 | second:inc(1, {"v1", "v2"}) 49 | second:inc(3, {"v1", "v3"}) 50 | second:inc(2, {"v1", "v3"}) 51 | 52 | local r = first:collect() 53 | luaunit.assertEquals(r[1], "# HELP counter1 ") 54 | luaunit.assertEquals(r[2], "# TYPE counter1 counter") 55 | luaunit.assertEquals(r[3], "counter1 5") 56 | luaunit.assertEquals(r[4], nil) 57 | 58 | 59 | r = second:collect() 60 | luaunit.assertEquals(r[3], 'counter2{a="v1",b="v2"} 1') 61 | luaunit.assertEquals(r[4], 'counter2{a="v1",b="v3"} 5') 62 | luaunit.assertEquals(r[5], nil) 63 | 64 | end 65 | 66 | function TestPrometheus.testGauge() 67 | local first = prometheus.gauge("gauge1", "", {"a", "b"}) 68 | local second = prometheus.gauge("gauge2", "", {"a", "b"}) 69 | 70 | first:inc() 71 | first:inc(4) 72 | first:set(2) 73 | first:dec() 74 | 75 | second:set(1, {"v1", "v2"}) 76 | second:inc(3, {"v1", "v3"}) 77 | second:dec(1, {"v1", "v3"}) 78 | second:inc(0, {"v1", "v3"}) 79 | 80 | local r 81 | 82 | r = first:collect() 83 | luaunit.assertEquals(r[1], "# HELP gauge1 ") 84 | luaunit.assertEquals(r[2], "# TYPE gauge1 gauge") 85 | luaunit.assertEquals(r[3], "gauge1 1") 86 | luaunit.assertEquals(r[4], nil) 87 | 88 | 89 | r = second:collect() 90 | luaunit.assertEquals(r[3], 'gauge2{a="v1",b="v2"} 1') 91 | luaunit.assertEquals(r[4], 'gauge2{a="v1",b="v3"} 2') 92 | luaunit.assertEquals(r[5], nil) 93 | 94 | end 95 | 96 | function TestPrometheus.testSpecialValues() 97 | local gauge = prometheus.gauge("gauge") 98 | 99 | local r 100 | 101 | gauge:set(math.huge) 102 | r = gauge:collect() 103 | luaunit.assertEquals(r[3], "gauge +Inf") 104 | 105 | gauge:set(-math.huge) 106 | r = gauge:collect() 107 | luaunit.assertEquals(r[3], "gauge -Inf") 108 | 109 | gauge:set(math.huge * 0) 110 | r = gauge:collect() 111 | luaunit.assertEquals(r[3], "gauge Nan") 112 | end 113 | 114 | 115 | 116 | function TestPrometheus.testHistogram() 117 | local hist1 = prometheus.histogram("l1", "Histogram 1") 118 | local hist2 = prometheus.histogram("l2", "Histogram 2", {"var", "site"}, {0.1, 0.2}) 119 | local hist3 = prometheus.histogram("l3", "Histogram 3", {}) 120 | 121 | hist1:observe(0.35) 122 | hist1:observe(0.9) 123 | hist1:observe(5) 124 | hist1:observe(15) 125 | 126 | hist2:observe(0.001, {"ok", "site1"}) 127 | hist2:observe(0.15, {"ok", "site1"}) 128 | hist2:observe(0.15, {"ok", "site2"}) 129 | 130 | local r = hist1:collect() 131 | luaunit.assertEquals(r[1], "# HELP l1 Histogram 1") 132 | luaunit.assertEquals(r[2], "# TYPE l1 histogram") 133 | luaunit.assertEquals(r[3], 'l1_bucket{le="0.005"} 0') 134 | luaunit.assertEquals(r[4], 'l1_bucket{le="0.01"} 0') 135 | luaunit.assertEquals(r[5], 'l1_bucket{le="0.025"} 0') 136 | luaunit.assertEquals(r[6], 'l1_bucket{le="0.05"} 0') 137 | luaunit.assertEquals(r[7], 'l1_bucket{le="0.075"} 0') 138 | luaunit.assertEquals(r[8], 'l1_bucket{le="0.1"} 0') 139 | luaunit.assertEquals(r[9], 'l1_bucket{le="0.25"} 0') 140 | luaunit.assertEquals(r[10], 'l1_bucket{le="0.5"} 1') 141 | luaunit.assertEquals(r[11], 'l1_bucket{le="0.75"} 1') 142 | luaunit.assertEquals(r[12], 'l1_bucket{le="1"} 2') 143 | luaunit.assertEquals(r[13], 'l1_bucket{le="2.5"} 2') 144 | luaunit.assertEquals(r[14], 'l1_bucket{le="5"} 3') 145 | luaunit.assertEquals(r[15], 'l1_bucket{le="7.5"} 3') 146 | luaunit.assertEquals(r[16], 'l1_bucket{le="10"} 3') 147 | luaunit.assertEquals(r[17], 'l1_bucket{le="+Inf"} 4') 148 | luaunit.assertEquals(r[18], 'l1_sum 21.25') 149 | luaunit.assertEquals(r[19], 'l1_count 4') 150 | 151 | r = hist2:collect() 152 | luaunit.assertEquals(r[3], 'l2_bucket{var="ok",site="site1",le="0.1"} 1') 153 | luaunit.assertEquals(r[4], 'l2_bucket{var="ok",site="site1",le="0.2"} 2') 154 | luaunit.assertEquals(r[5], 'l2_bucket{var="ok",site="site1",le="+Inf"} 2') 155 | luaunit.assertEquals(r[6], 'l2_sum{var="ok",site="site1"} 0.151') 156 | luaunit.assertEquals(r[7], 'l2_count{var="ok",site="site1"} 2') 157 | luaunit.assertEquals(r[8], 'l2_bucket{var="ok",site="site2",le="0.1"} 0') 158 | luaunit.assertEquals(r[9], 'l2_bucket{var="ok",site="site2",le="0.2"} 1') 159 | luaunit.assertEquals(r[10], 'l2_bucket{var="ok",site="site2",le="+Inf"} 1') 160 | luaunit.assertEquals(r[11], 'l2_sum{var="ok",site="site2"} 0.15') 161 | luaunit.assertEquals(r[12], 'l2_count{var="ok",site="site2"} 1') 162 | 163 | r = hist3:collect() 164 | luaunit.assertEquals(r[3], nil) 165 | end 166 | 167 | function TestPrometheus.testHistogramUnorderedBuckets() 168 | local hist = prometheus.histogram("l2", "Histogram 2", {}, {0.2, 0.1, 0.5}) 169 | 170 | hist:observe(0.15) 171 | hist:observe(0.4) 172 | 173 | local r = hist:collect() 174 | luaunit.assertEquals(r[3], 'l2_bucket{le="0.1"} 0') 175 | luaunit.assertEquals(r[4], 'l2_bucket{le="0.2"} 1') 176 | luaunit.assertEquals(r[5], 'l2_bucket{le="0.5"} 2') 177 | luaunit.assertEquals(r[6], 'l2_bucket{le="+Inf"} 2') 178 | luaunit.assertEquals(r[7], 'l2_sum 0.55') 179 | luaunit.assertEquals(r[8], 'l2_count 2') 180 | luaunit.assertEquals(r[9], nil) 181 | end 182 | 183 | os.exit(luaunit.run()) 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | ### Please note, this project is deprecated and no longer being maintained, please use [metrics/prometheus](https://github.com/tarantool/metrics/tree/master/metrics/plugins/prometheus). 4 | 5 | [![Build Status](https://travis-ci.org/tarantool/prometheus.svg?branch=master)](https://travis-ci.org/tarantool/prometheus) 6 | 7 | # Prometheus metric collector for Tarantool 8 | 9 | This is a Lua library that makes it easy to collect metrics from your Tarantool 10 | apps and databases and expose them via the Prometheus protocol. You may use the 11 | library to instrument your code and get an insight into performance bottlenecks. 12 | 13 | At the moment, 3 types of metrics are supported: 14 | * Counter: a non-decreasing numeric value, used e.g. for counting the number of 15 | requests 16 | * Gauge: an arbitrary numeric value, which can be used e.g. to report memory 17 | usage 18 | * Histogram: for counting value distribution by user-specified buckets. Can be 19 | used for recording request/response times. 20 | 21 | ## Table of contents 22 | 23 | * [Limitations](#limitations) 24 | * [Getting started](#getting-started) 25 | * [Basic examples](#basic-examples) 26 | * [A more detailed example](#a-more-detailed-example) 27 | * [Usage](#usage) 28 | * [counter(name, help, labels)](#countername-help-labels) 29 | * [gauge(name, help, labels)](#gaugename-help-labels) 30 | * [histogram(name, help, labels, buckets)](#histogramname-help-labels-buckets) 31 | * [Counter:inc(value, labels)](#counterincvalue-labels) 32 | * [Gauge:set(value, labels)](#gaugesetvalue-labels) 33 | * [Gauge:inc(value, labels)](#gaugeincvalue-labels) 34 | * [Gauge:dec(value, labels)](#gaugedecvalue-labels) 35 | * [Histogram:observe(value, labels)](#histogramobservevalue-labels) 36 | * [collect()](#collect) 37 | * [collect\_http()](#collect_http) 38 | * [Development](#development) 39 | * [Credits](#credits) 40 | * [License](#license) 41 | 42 | ## Limitations 43 | 44 | The Summary metric is not implemented yet. It may be implemented in future. 45 | 46 | ## Getting started 47 | 48 | The easiest way is, of course, to use 49 | [one of the official Docker images](https://hub.docker.com/r/tarantool/tarantool/), 50 | which already contain the Prometheus collector. But if you run on a regular 51 | Linux distro, first install the library from 52 | [Tarantool Rocks server](http://rocks.tarantool.org): 53 | 54 | ```bash 55 | $ tarantoolctl rocks install prometheus 56 | ``` 57 | 58 | This will install the module to the `.rocks` under your current working directory. 59 | 60 | ### Basic examples 61 | 62 | To report the arena size, you can write the following code: 63 | 64 | ```lua 65 | prometheus = require('prometheus') 66 | http = require('http.server') 67 | fiber = require('fiber') 68 | 69 | box.cfg{} 70 | httpd = http.new('0.0.0.0', 8080) 71 | 72 | arena_used = prometheus.gauge("tarantool_arena_used", 73 | "The amount of arena used by Tarantool") 74 | 75 | function monitor_arena_size() 76 | while true do 77 | arena_used:set(box.slab.info().arena_used) 78 | fiber.sleep(5) 79 | end 80 | end 81 | fiber.create(monitor_arena_size) 82 | 83 | httpd:route( { path = '/metrics' }, prometheus.collect_http) 84 | httpd:start() 85 | ``` 86 | 87 | The code will periodically measure the arena size and update the `arena_used` 88 | metric. Later, when Prometheus polls the instance, it will get the values of all 89 | metrics the instance created. 90 | 91 | There are 3 important bits in the code above: 92 | 93 | ```lua 94 | arena_used = prometheus.gauge(...) 95 | ``` 96 | 97 | This creates a [Gauge](https://prometheus.io/docs/concepts/metric_types/#gauge) 98 | object that can be set to an arbitrary numeric value. After this, the metric from 99 | this object will be automatically collected by Prometheus every time metrics are 100 | polled. 101 | 102 | ```lua 103 | arena_used:set(...) 104 | ``` 105 | 106 | This sets the current value of the metric. 107 | 108 | ```lua 109 | httpd:route( { path = '/metrics' }, prometheus.collect_http) 110 | ``` 111 | 112 | This exposes metrics over the text/plain HTTP protocol on 113 | [http://localhost:8080/metrics](http://localhost:8080/metrics) for Prometheus to 114 | collect. Prometheus periodically polls this endpoint and stores the results in 115 | its time series database. 116 | 117 | ### A more detailed example 118 | 119 | If you want a more detailed example, there is an `example.lua` file in the root 120 | of this repo. It demonstrates the usage of each of the 3 metric types. 121 | 122 | To run it with Docker, you can do as follows: 123 | 124 | ``` bash 125 | $ docker build -t prometheus . 126 | $ docker run --rm -t -i -p8080:8080 prometheus 127 | ``` 128 | 129 | Then visit [http://localhost:8080/metrics](http://localhost:8080/metrics) and 130 | refresh the page a few times to see the metrics change. 131 | 132 | ## Usage 133 | 134 | This section documents the user-facing API of the module. 135 | 136 | ### counter(name, help, labels) 137 | 138 | Creates and registers a [Counter](https://prometheus.io/docs/concepts/metric_types/#counter). 139 | 140 | * `name` is the name of the metric. Required. 141 | * `help` is the metric docstring. You can use newlines and quotes here. Optional. 142 | * `labels` is an array of label names for the metric. Optional. 143 | 144 | Example: 145 | 146 | ```lua 147 | num_of_logins = prometheus.counter( 148 | "tarantool_number_of_logins", "Total number of user logins") 149 | 150 | http_requests = prometheus:counter( 151 | "tarantool_http_requests_total", "Number of HTTP requests", {"host", "status"}) 152 | ``` 153 | 154 | ### gauge(name, help, labels) 155 | 156 | Creates and registers a [Gauge](https://prometheus.io/docs/concepts/metric_types/#gauge). 157 | 158 | * `name` is the name of the metric. Required. 159 | * `help` is the metric docstring. You can use newlines and quotes here. Optional. 160 | * `labels` is an array of label names for the metric. Optional. 161 | 162 | Example: 163 | 164 | ``` lua 165 | arena_used = prometheus.gauge( 166 | "tarantool_arena_used_size", "Total size of the arena used") 167 | 168 | requests_inprogress = prometheus.gauge( 169 | "tarantool_requests_inprogress", "Number of requests in progress", {"request_type"}) 170 | ``` 171 | 172 | ### histogram(name, help, labels, buckets) 173 | 174 | Creates and registers a [Histogram](https://prometheus.io/docs/concepts/metric_types/#histogram). 175 | 176 | * `name` is the name of the metric. Required. 177 | * `help` is the metric docstring. You can use newlines and quotes here. Optional. 178 | * `labels` is an array of label names for the metric. Optional. 179 | * `buckets` is an array of numbers defining histogram buckets. Optional. Defaults to 180 | `{.005, .01, .025, .05, .075, .1, .25, .5, .75, 1.0, 2.5, 5.0, 7.5, 10.0, INF}`. 181 | 182 | Example: 183 | 184 | ``` lua 185 | request_latency = prometheus.histogram( 186 | "tarantool_request_latency_seconds", "Incoming request latency", {"client"}) 187 | response_size = prometheus.histogram( 188 | "tarantool_response_size", "Size of response, in bytes", nil, {100, 1000, 100000}) 189 | ``` 190 | 191 | ### Counter:inc(value, labels) 192 | 193 | Increments a counter created by `prometheus.counter()`. 194 | 195 | * `value` specifies by how much to increment. Optional. Defaults to `1`. 196 | * `labels` is an array of label values. Optional. 197 | 198 | ### Gauge:set(value, labels) 199 | 200 | Sets a value of a gauge created by `prometheus.gauge()`. 201 | 202 | * `value` is the value to set. Optional. Defaults to `0`. 203 | * `labels` is an array of label values. Optional. 204 | 205 | ### Gauge:inc(value, labels) 206 | 207 | Increments a gauge created by `prometheus.gauge()`. 208 | 209 | * `value` specifies by how much to increment. Optional. Defaults to `1`. 210 | * `labels` is an array of label values. Optional. 211 | 212 | ### Gauge:dec(value, labels) 213 | 214 | Decrements a gauge created by `prometheus.gauge()`. 215 | 216 | * `value` specifies by how much to decrement. Optional. Defaults to `1`. 217 | * `labels` is an array of label values. Optional. 218 | 219 | ### Histogram:observe(value, labels) 220 | 221 | Records a value to a histogram created by `prometheus.histogram()`. 222 | 223 | * `value` is the value to record. Optional. Defaults to `0`. 224 | * `labels` is an array of label values. Optional. 225 | 226 | ### collect() 227 | 228 | Presents all metrics in a text format compatible with Prometheus. This can be 229 | called either by `http.server` callback or by 230 | [Tarantool nginx_upstream_module](https://github.com/tarantool/nginx_upstream_module). 231 | 232 | ### collect_http() 233 | 234 | Convenience function, specially for one-line registration in the Tarantool 235 | `http.server`, as follows: 236 | 237 | ```lua 238 | httpd:route( { path = '/metrics' }, prometheus.collect_http) 239 | ``` 240 | 241 | ## Development 242 | 243 | Contributions are welcome. Report issues and feature requests at 244 | https://github.com/tarantool/prometheus/issues 245 | 246 | To run tests, do: 247 | 248 | ```bash 249 | $ tarantool test.lua 250 | ``` 251 | 252 | NB: Tests require `luaunit` library. 253 | 254 | ## Credits 255 | 256 | Loosely based on the implementation by @knyar: https://github.com/knyar/nginx-lua-prometheus 257 | 258 | ## License 259 | 260 | Licensed under the BSD license. See the LICENSE file. 261 | -------------------------------------------------------------------------------- /prometheus.lua: -------------------------------------------------------------------------------- 1 | -- vim: ts=2:sw=2:sts=2:expandtab 2 | -- luacheck: globals box 3 | 4 | local INF = math.huge 5 | local NAN = math.huge * 0 6 | local DEFAULT_BUCKETS = {.005, .01, .025, .05, .075, .1, .25, .5, 7 | .75, 1.0, 2.5, 5.0, 7.5, 10.0, INF} 8 | 9 | local REGISTRY = nil 10 | 11 | local Registry = {} 12 | Registry.__index = Registry 13 | 14 | function Registry.new() 15 | local obj = {} 16 | setmetatable(obj, Registry) 17 | obj.collectors = {} 18 | obj.callbacks = {} 19 | return obj 20 | end 21 | 22 | function Registry:register(collector) 23 | if self.collectors[collector.name]~=nil then 24 | return self.collectors[collector.name] 25 | end 26 | self.collectors[collector.name] = collector 27 | return collector 28 | end 29 | 30 | function Registry:unregister(collector) 31 | if self.collectors[collector.name]~=nil then 32 | table.remove(self.collectors, collector.name) 33 | end 34 | end 35 | 36 | function Registry:collect() 37 | for _, registered_callback in ipairs(self.callbacks) do 38 | registered_callback() 39 | end 40 | 41 | local result = {} 42 | for _, collector in pairs(self.collectors) do 43 | for _, metric in ipairs(collector:collect()) do 44 | table.insert(result, metric) 45 | end 46 | table.insert(result, '') 47 | end 48 | return result 49 | end 50 | 51 | function Registry:register_callback(callback) 52 | local found = false 53 | for _, registered_callback in ipairs(self.callbacks) do 54 | if registered_callback == callback then 55 | found = true 56 | end 57 | end 58 | if not found then 59 | table.insert(self.callbacks, callback) 60 | end 61 | end 62 | 63 | local function get_registry() 64 | if not REGISTRY then 65 | REGISTRY = Registry.new() 66 | end 67 | return REGISTRY 68 | end 69 | 70 | local function register(collector) 71 | local registry = get_registry() 72 | registry:register(collector) 73 | 74 | return collector 75 | end 76 | 77 | local function register_callback(callback) 78 | local registry = get_registry() 79 | registry:register_callback(callback) 80 | end 81 | 82 | local function zip(lhs, rhs) 83 | if lhs == nil or rhs == nil then 84 | return {} 85 | end 86 | 87 | local len = math.min(#lhs, #rhs) 88 | local result = {} 89 | for i=1,len do 90 | table.insert(result, {lhs[i], rhs[i]}) 91 | end 92 | return result 93 | end 94 | 95 | local function metric_to_string(value) 96 | if value == INF then 97 | return "+Inf" 98 | elseif value == -INF then 99 | return "-Inf" 100 | elseif value ~= value then 101 | return "Nan" 102 | else 103 | return tostring(value) 104 | end 105 | end 106 | 107 | local function escape_string(str) 108 | return str 109 | :gsub("\\", "\\\\") 110 | :gsub("\n", "\\n") 111 | :gsub('"', '\\"') 112 | end 113 | 114 | local function labels_to_string(label_pairs) 115 | if #label_pairs == 0 then 116 | return "" 117 | end 118 | local label_parts = {} 119 | for _, label in ipairs(label_pairs) do 120 | local label_name = label[1] 121 | local label_value = label[2] 122 | local label_value_escaped = escape_string(string.format("%s", label_value)) 123 | table.insert(label_parts, label_name .. '="' .. label_value_escaped .. '"') 124 | end 125 | return "{" .. table.concat(label_parts, ",") .. "}" 126 | end 127 | 128 | 129 | local Counter = {} 130 | Counter.__index = Counter 131 | 132 | function Counter.new(name, help, labels) 133 | local obj = {} 134 | setmetatable(obj, Counter) 135 | if not name then 136 | error("Name should be set for Counter") 137 | end 138 | obj.name = name 139 | obj.help = help or "" 140 | obj.labels = labels or {} 141 | obj.observations = {} 142 | obj.label_values = {} 143 | 144 | return obj 145 | end 146 | 147 | function Counter:inc(num, label_values) 148 | num = num or 1 149 | label_values = label_values or {} 150 | if num < 0 then 151 | error("Counter increment should not be negative") 152 | end 153 | local key = table.concat(label_values, '\0') 154 | local old_value = self.observations[key] or 0 155 | self.observations[key] = old_value + num 156 | self.label_values[key] = label_values 157 | end 158 | 159 | function Counter:collect() 160 | local result = {} 161 | 162 | if next(self.observations) == nil then 163 | return {} 164 | end 165 | 166 | table.insert(result, '# HELP '..self.name..' '..escape_string(self.help)) 167 | table.insert(result, "# TYPE "..self.name.." counter") 168 | 169 | for key, observation in pairs(self.observations) do 170 | local label_values = self.label_values[key] 171 | local prefix = self.name 172 | local labels = zip(self.labels, label_values) 173 | 174 | local str = prefix..labels_to_string(labels).. 175 | ' '..metric_to_string(observation) 176 | table.insert(result, str) 177 | end 178 | 179 | return result 180 | end 181 | 182 | 183 | local Gauge = {} 184 | Gauge.__index = Gauge 185 | 186 | function Gauge.new(name, help, labels) 187 | local obj = {} 188 | setmetatable(obj, Gauge) 189 | if not name then 190 | error("Name should be set for Gauge") 191 | end 192 | obj.name = name 193 | obj.help = help or "" 194 | obj.labels = labels or {} 195 | obj.observations = {} 196 | obj.label_values = {} 197 | 198 | return obj 199 | end 200 | 201 | function Gauge:inc(num, label_values) 202 | num = num or 1 203 | label_values = label_values or {} 204 | local key = table.concat(label_values, '\0') 205 | local old_value = self.observations[key] or 0 206 | self.observations[key] = old_value + num 207 | self.label_values[key] = label_values 208 | end 209 | 210 | function Gauge:dec(num, label_values) 211 | num = num or 1 212 | label_values = label_values or {} 213 | local key = table.concat(label_values, '\0') 214 | local old_value = self.observations[key] or 0 215 | self.observations[key] = old_value - num 216 | self.label_values[key] = label_values 217 | end 218 | 219 | function Gauge:set(num, label_values) 220 | num = num or 0 221 | label_values = label_values or {} 222 | local key = table.concat(label_values, '\0') 223 | self.observations[key] = num 224 | self.label_values[key] = label_values 225 | end 226 | 227 | function Gauge:collect() 228 | local result = {} 229 | 230 | if next(self.observations) == nil then 231 | return {} 232 | end 233 | 234 | table.insert(result, '# HELP '..self.name..' '..escape_string(self.help)) 235 | table.insert(result, "# TYPE "..self.name.." gauge") 236 | 237 | for key, observation in pairs(self.observations) do 238 | local label_values = self.label_values[key] 239 | local prefix = self.name 240 | local labels = zip(self.labels, label_values) 241 | 242 | local str = prefix..labels_to_string(labels).. 243 | ' '..metric_to_string(observation) 244 | table.insert(result, str) 245 | end 246 | 247 | return result 248 | end 249 | 250 | local Histogram = {} 251 | Histogram.__index = Histogram 252 | 253 | function Histogram.new(name, help, labels, 254 | buckets) 255 | local obj = {} 256 | setmetatable(obj, Histogram) 257 | if not name then 258 | error("Name should be set for Histogram") 259 | end 260 | obj.name = name 261 | obj.help = help or "" 262 | obj.labels = labels or {} 263 | obj.buckets = buckets or DEFAULT_BUCKETS 264 | table.sort(obj.buckets) 265 | if obj.buckets[#obj.buckets] ~= INF then 266 | obj.buckets[#obj.buckets+1] = INF 267 | end 268 | obj.observations = {} 269 | obj.label_values = {} 270 | obj.counts = {} 271 | obj.sums = {} 272 | 273 | return obj 274 | end 275 | 276 | function Histogram:observe(num, label_values) 277 | num = num or 0 278 | label_values = label_values or {} 279 | local key = table.concat(label_values, '\0') 280 | 281 | local obs 282 | if self.observations[key] == nil then 283 | obs = {} 284 | for i=1, #self.buckets do 285 | obs[i] = 0 286 | end 287 | self.observations[key] = obs 288 | self.label_values[key] = label_values 289 | self.counts[key] = 0 290 | self.sums[key] = 0 291 | else 292 | obs = self.observations[key] 293 | end 294 | 295 | self.counts[key] = self.counts[key] + 1 296 | self.sums[key] = self.sums[key] + num 297 | for i, bucket in ipairs(self.buckets) do 298 | if num <= bucket then 299 | obs[i] = obs[i] + 1 300 | end 301 | end 302 | end 303 | 304 | 305 | function Histogram:collect() 306 | local result = {} 307 | 308 | if next(self.observations) == nil then 309 | return {} 310 | end 311 | 312 | table.insert(result, '# HELP '..self.name..' '..escape_string(self.help)) 313 | table.insert(result, "# TYPE "..self.name.." histogram") 314 | 315 | for key, observation in pairs(self.observations) do 316 | local label_values = self.label_values[key] 317 | local prefix = self.name 318 | local labels = zip(self.labels, label_values) 319 | labels[#labels+1] = {le="0"} 320 | for i, bucket in ipairs(self.buckets) do 321 | labels[#labels] = {"le", metric_to_string(bucket)} 322 | local str = prefix.."_bucket"..labels_to_string(labels).. 323 | ' '..metric_to_string(observation[i]) 324 | table.insert(result, str) 325 | end 326 | table.remove(labels, #labels) 327 | 328 | table.insert(result, 329 | prefix.."_sum"..labels_to_string(labels)..' '..tostring(self.sums[key]):gsub('ULL$', '')) 330 | table.insert(result, 331 | prefix.."_count"..labels_to_string(labels)..' '..tostring(self.counts[key]):gsub('ULL$', '')) 332 | end 333 | 334 | return result 335 | end 336 | 337 | 338 | -- #################### Public API #################### 339 | 340 | 341 | local function counter(name, help, labels) 342 | local obj = Counter.new(name, help, labels) 343 | obj = register(obj) 344 | return obj 345 | end 346 | 347 | local function gauge(name, help, labels) 348 | local obj = Gauge.new(name, help, labels) 349 | obj = register(obj) 350 | return obj 351 | end 352 | 353 | local function histogram(name, help, labels, buckets) 354 | local obj = Histogram.new(name, help, labels, buckets) 355 | obj = register(obj) 356 | return obj 357 | end 358 | 359 | local function collect() 360 | local registry = get_registry() 361 | 362 | return table.concat(registry:collect(), '\n')..'\n' 363 | end 364 | 365 | local function collect_http() 366 | return { 367 | status = 200, 368 | headers = { ['content-type'] = 'text/plain; charset=utf8' }, 369 | body = collect() 370 | } 371 | end 372 | 373 | local function clear() 374 | local registry = get_registry() 375 | registry.collectors = {} 376 | registry.callbacks = {} 377 | end 378 | 379 | local function init() 380 | local registry = get_registry() 381 | local tarantool_metrics = require('prometheus.tarantool-metrics') 382 | registry:register_callback(tarantool_metrics.measure_tarantool_metrics) 383 | end 384 | 385 | return {counter=counter, 386 | gauge=gauge, 387 | histogram=histogram, 388 | collect=collect, 389 | collect_http=collect_http, 390 | clear=clear, 391 | init=init} 392 | --------------------------------------------------------------------------------