--- Prometheus client skynet 实现.
--
-- 所有的 Metrics 应该在服务器启动的时候先全部创建好并创建。
--
--     local prometheus = require "prometheus"
--     prometheus.gauge('online_users', '在线玩家数量'):register()
--     prometheus.counter('exceptions_count', '异常数量'):register()
--     prometheus.histogram('request_duration_seconds', {'profile_id', 'request_status'}):register()
--
-- 在需要记录统计数值的服务中在去创建相同名字的 Metric，注意不要重复注册，会出错，而且只需要传 name 就可以了，其它都不用了
--
--     # in agent.lua
--     local prometheus = require "prometheus"
--     local metric_online_users = prometheus.gauge('online_users')
--
--     local init(...)
--       metric_online_users:inc()
--       ...
--     end
--
--     local on_exit()
--       metric_online_users:dec()
--     end
--
-- 如果要在 xpcall 中的 error handler 中使用，因为不能在 error handler 中调用一些 skynet 方法，最好在服务启动的地方先调用一次 `prometheus.default_registry()`

-- https://github.com/apache/apisix/blob/master/doc/zh-cn/plugins/prometheus.md
-- https://yunlzheng.gitbook.io/prometheus-book/part-ii-prometheus-jin-jie/exporter/what-is-prometheus-exporter
-- https://kalasearch.cn/blog/grafana-with-prometheus-tutorial/
-- https://www.cnblogs.com/chanshuyi/p/02_grafana_quick_start.html

-- https://prometheus.io/docs/instrumenting/writing_exporters/
-- https://github.com/pourer/pika_exporter
-- https://www.jianshu.com/p/9646ce49f722
-- https://github.com/prometheus/haproxy_exporter
-- https://github.com/yunlzheng/prometheus-book

-- https://yunlzheng.gitbook.io/prometheus-book/parti-prometheus-ji-chu/promql/prometheus-metrics-types
-- https://frezc.github.io/2019/08/03/prometheus-metrics/

-- Prometheus中主要使用的四类指标类型，如下所示
-- Counter (累加指标)
-- Gauge (测量指标)
-- Summary (概略图)
-- Histogram (直方图)

-- Counter 一个累加指标数据，这个值随着时间只会逐渐的增加，
-- 比如程序完成的总任务数量，运行错误发生的总次数。
-- 常见的还有交换机中snmp采集的数据流量也属于该类型，
-- 代表了持续增加的数据包或者传输字节累加值。

-- Gauge代表了采集的一个单一数据，这个数据可以增加也可以减少，
-- 比如CPU使用情况，内存使用量，硬盘当前的空间容量等等

-- Histogram和Summary使用的频率较少，两种都是基于采样的方式。
-- 另外有一些库对于这两个指标的使用和支持程度不同，有些仅仅实现了部分功能。
-- 这两个类型对于某一些业务需求可能比较常见，比如查询单位时间内：总的响应时间低于300ms的占比，或者查询95%用户查询的门限值对应的响应时间是多少。
--  使用Histogram和Summary指标的时候同时会产生多组数据，_count代表了采样的总数，_sum则代表采样值的和。 _bucket则代表了落入此范围的数据

local skynet = require "skynet"

local sgsub = string.gsub
local tsort = table.sort
local tinsert = table.insert

local prometheus = {}

local INF = math.huge
local DEFAULT_BUCKETS = {0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0, INF}

local METRIC_NAME_PATTERN = "[a-zA-Z_:][a-zA-Z0-9_:]*"
local METRIC_LABEL_NAME_PATTERN = "[a-zA-Z_][a-zA-Z0-9_]*"
local RESERVED_METRIC_LABEL_NAME_PATTERN = "__.*"

local function fully_match(s, pattern)
    return sgsub(s, pattern, "") == ""
end

local function is_valid_name(name)
    return fully_match(name, METRIC_NAME_PATTERN)
end

local function is_valid_label_name(name)
    return fully_match(name, METRIC_LABEL_NAME_PATTERN) and not fully_match(name, RESERVED_METRIC_LABEL_NAME_PATTERN)
end

local DEFAULT_REGISTRIES
local REGISTRY

--- 获得默认注册服务.
function prometheus.default_registry()
    if REGISTRY == nil then
        REGISTRY = skynet.uniqueservice("swt/prometheus")
    end
    return REGISTRY
end
local default_registry = prometheus.default_registry

--- 获得 Regstiry 所有的数据.
-- @treturn string metrics
function prometheus.collect(registry)
    return skynet.call(registry or default_registry(), "lua", "collect")
end

local function each_registry(collector)
    if next(collector._registries) then
        return pairs(collector._registries)
    else
        if DEFAULT_REGISTRIES == nil then
            local registry = default_registry()
            DEFAULT_REGISTRIES = {[registry] = registry}
        end
        return pairs(DEFAULT_REGISTRIES)
    end
end

local Collector = {}
Collector.__index = Collector

local Counter = setmetatable({_type = "counter"}, Collector)
Counter.__index = Counter

--- 创建一个 counter.
--
-- Counter 用来计算，比如请求数量，异常数量等等
--
-- @tparam string name Metric 名字
-- @tparam string help Metric 用途说明
-- @tparam[opt] tab labels 标签名字数组
-- @treturn Counter
function prometheus.counter(name, help, labels)
    return Counter:new(name, help, labels)
end

local Gauge = setmetatable({_type = "gauge"}, Collector)
Gauge.__index = Gauge

--- 创建一个 gauge.
--
-- Gauge 用来记录一个可以自由变化的数值，可以用来记录在线人数等等。
--
-- @tparam string name Metric 名字
-- @tparam string help Metric 用途说明
-- @tparam[opt] tab labels 标签名字数组
-- @treturn Gauge
function prometheus.gauge(name, help, labels)
    return Gauge:new(name, help, labels)
end

local Summary = setmetatable({_type = "summary"}, Collector)
Summary.__index = Summary

--- 创建一个 summary.
--
-- Summary 同时记录观测值的出现数量和总和。可以用来同时统计请求数量和总用时。
--
-- @tparam string name Metric 名字
-- @tparam string help Metric 用途说明
-- @tparam[opt] tab labels 标签名字数组
-- @treturn Gauge
function prometheus.summary(name, help, labels)
    return Summary:new(name, help, labels)
end

local Histogram = setmetatable({_type = "histogram"}, Collector)
Histogram.__index = Histogram

--- 创建一个 histogram.
--
-- Histogram 除了 summary 统计的信息，还会根据 buckets 分别统计落入不同区间的观测值的数量。
--
-- 每个 Bucket 指定了上限，所有观测值小于或者等于上限的 buckets 都会加 1。
--
-- @tparam string name Metric 名字
-- @tparam string help Metric 用途说明
-- @tparam[opt] tab labels 标签名字数组
-- @tparam[opt] tab buckets 每个区间的上限
-- @treturn Gauge
function prometheus.histogram(name, help, labels, buckets)
    return Histogram:new(name, help, labels):buckets(buckets)
end

--- Counter, Gauge, Summary, Histogram 共享的方法.
-- @type Collector

function Collector:new(name, help, labels)
    assert(name ~= nil and name ~= "", "Require metric name")
    assert(is_valid_name(name), "Invalid metric name: " .. name)
    labels = labels or {}
    return setmetatable(
        {
            _name = name,
            _help = help,
            _labels = labels,
            _registries = {},
            _type = self._type
        },
        self
    )
end

--- 设置标签名.
--
-- 只有在调用 register 注册之前设置才有用
--
-- @tparam tab labels 标签名数组
-- @return collector 返回自己
function Collector:labels(labels)
    labels = labels or {}
    for _, l in ipairs(labels) do
        assert(is_valid_label_name(l), "Invalid label name: " .. l)
    end
    self._labels = labels
    return self
end

--- 注册 metric.
--
-- 注册后才会把数据真正记录下来.
--
-- @param[opt] registry 注册服务，缺省使用默认的注册服务
-- @return collector 返回自己
function Collector:register(registry)
    assert(self._help ~= nil and self._help ~= "", "Require metric help")
    for _, l in ipairs(self._labels) do
        assert(is_valid_label_name(l), "Invalid label name: " .. l)
    end
    registry = registry or default_registry()
    if self._registries[registry] == nil then
        skynet.call(registry, "lua", "register", self._name, self._help, self._type, self._labels, self._buckets)
        self._registries[registry] = registry
    end

    return self
end

--- 取消注册 metric.
--
-- @param[opt] registry 注册服务，缺省使用默认的注册服务
-- @return collector 返回自己
function Collector:unregister(registry)
    registry = registry or default_registry()
    if self._registries[registry] ~= nil then
        skynet.call(registry, "lua", "unregister", self._name)
        self._registries[registry] = nil
    end

    return self
end

--- Counter metric.
-- @type Counter

--- 增长 Counter 的计数.
-- @tparam number num 非负数，可以是浮点数
-- @tparam tab labels_values 标签的值数组
function Counter:inc(num, labels_values)
    for _, registry in each_registry(self) do
        skynet.send(registry, "lua", "submit", self._name, "inc", num or 1, labels_values)
    end
    return self
end

--- Gauge metric.
-- @type Gauge

--- 增加 Gauge 的数量.
-- @tparam number num 变化数量
-- @tparam tab labels_values 标签的值数组
function Gauge:inc(num, labels_values)
    for _, registry in each_registry(self) do
        skynet.send(registry, "lua", "submit", self._name, "inc", num or 1, labels_values)
    end
    return self
end

--- 减少 Gauge 的数量.
-- @tparam number num 变化数量
-- @tparam tab labels_values 标签的值数组
function Gauge:dec(num, labels_values)
    for _, registry in each_registry(self) do
        skynet.send(registry, "lua", "submit", self._name, "dec", num or 1, labels_values)
    end
    return self
end

--- 将 Gauge 数量设置为指定值.
-- @tparam number num 指定 Gauge 的值
-- @tparam tab labels_values 标签的值数组
function Gauge:set(num, labels_values)
    for _, registry in each_registry(self) do
        skynet.send(registry, "lua", "submit", self._name, "set", num or 1, labels_values)
    end
    return self
end

--- Summary metric.
-- @type Summary

--- 记录一个 Summary 的观察值.
-- @tparam number num 观察值
-- @tparam tab labels_values 标签的值数组
function Summary:observe(num, labels_values)
    assert(num ~= nil, "Require num")
    for _, registry in each_registry(self) do
        skynet.send(registry, "lua", "submit", self._name, "observe", num, labels_values)
    end

    return self
end

--- Histogram metric.
-- @type Histogram

--- 自定义 Histogram 的 buckets.
-- 只有在调用 register 注册之前设置才有用
-- @tparam tab buckets 每个 buckets 的上限组成的数组.
-- @return histogram 返回自己
function Histogram:buckets(buckets)
    if buckets == nil then
        self._buckets = DEFAULT_BUCKETS
    else
        tsort(buckets)
        if buckets[#buckets] ~= INF then
            tinsert(buckets, INF)
        end
        self._buckets = buckets
    end

    return self
end

--- 记录一个 Histogram 的观察值.
-- @tparam number num 观察值
-- @tparam tab labels_values 标签的值数组
function Histogram:observe(num, labels_values)
    assert(num ~= nil, "Require num")
    for _, registry in each_registry(self) do
        skynet.send(registry, "lua", "submit", self._name, "observe", num, labels_values)
    end

    return self
end

return prometheus
