Skip to content

Commit

Permalink
api: migrate to httpd role server
Browse files Browse the repository at this point in the history
Since the `httpd` role was released this role has been needed support
for this feature.

After the patch metrics-export-role supports an `httpd` role. It is
possible to determine a list of servers that role will reuse in the
following configuration.

Closes #15
  • Loading branch information
themilchenko committed Sep 30, 2024
1 parent 4c7758c commit 525b74a
Show file tree
Hide file tree
Showing 9 changed files with 1,370 additions and 293 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- Introduce latency observation for http endpoint (#17).
- Migration to HTTP role server (#15).

### Fixed

Expand Down
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,46 @@ set `metrics.enabled` to `true`:

For now only `json` and `prometheus` formats are supported.

### Integration with httpd role

It is also possible to use [httpd role](https://github.com/tarantool/http?tab=readme-ov-file#roles).
To enable it, you need to fill `server` field with name that was configured in `roles.httpd` block
instead of `listen` like it was earlier. To configure `httpd` role you need to write block in roles_cfg
section:

```yaml
roles_cfg:
roles.httpd:
default:
- listen: 8081
additional:
- listen: '127.0.0.1:8082'
```

After it you can use `server` name in `roles.metrics-export` block. If `server` and `listen` names
wasn't provided, the default one will be used:

```yaml
roles.metrics-export:
http:
- server: 'additional'
endpoints:
...
- endpoints:
...
```

So now it is possible to mix `server` and `listen` parameteres.

Let's put it all together now:

```yaml
roles_cfg:
roles.httpd:
default:
- listen: 8081
additional:
- listen: '127.0.0.1:8082'
roles.metrics-export:
http:
- listen: 8081
Expand All @@ -121,6 +157,15 @@ roles_cfg:
format: json
metrics:
enabled: true
- server: 'additional'
endpoints:
- path: /metrics
format: json
- endpoints:
- path: /metrics
format: prometheus
metrics:
enabled: true
```

With this configuration, metrics can be obtained on this machine with the
Expand Down
2 changes: 1 addition & 1 deletion metrics-export-role-scm-1.rockspec
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ description = {
dependencies = {
"lua >= 5.1",
"tarantool >= 3.0",
"http >= 1.5.0",
"http >= 1.6.0",
}

build = {
Expand Down
112 changes: 91 additions & 21 deletions roles/metrics-export.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local urilib = require("uri")
local http_server = require('http.server')
local httpd_role = require('roles.httpd')

local M = {}

Expand Down Expand Up @@ -48,9 +49,6 @@ local function remove_side_slashes(path)
end

local function parse_listen(listen)
if listen == nil then
return nil, nil, "must exist"
end
if type(listen) ~= "string" and type(listen) ~= "number" then
return nil, nil, "must be a string or a number, got " .. type(listen)
end
Expand Down Expand Up @@ -170,14 +168,48 @@ local function validate_http_endpoint(endpoint)
end
end

-- check_server_httpd_role validates that httpd configuration and provided name exists.
local function check_server_httpd_role(server)
local httpd_roles_cfg = require("config"):get("roles_cfg")['roles.httpd']
if httpd_roles_cfg == nil then
error("there is no configuration for httpd role")
end
if httpd_roles_cfg[server] == nil then
error(('server with name %s not found in httpd role config'):format(server))
end
end

-- get_server_from_httpd_role_cfg returns http server by provided server_name from httpd role.
-- If server_name not found returns server by DEFAULT_SERVER_NAME.
local function get_server_from_httpd_role_cfg(server_name)
local httpd_roles_cfg = require("config"):get("roles_cfg")['roles.httpd']
return httpd_roles_cfg[server_name] or
httpd_roles_cfg[httpd_role.DEFAULT_SERVER_NAME]
end


local function validate_http_node(node)
if type(node) ~= "table" then
error("http configuration node must be a table, got " .. type(node), 3)
end

local _, _, err = parse_listen(node.listen)
if err ~= nil then
error("failed to parse http 'listen' param: " .. err, 3)
if node.server ~= nil then
if type(node.server) ~= 'string' then
error("server configuration sould be a string, got " .. type(node.server))
end

if node.listen ~= nil then
error("it is not possible to provide 'server' and 'listen' blocks simultaneously")
end

check_server_httpd_role(node.server)
elseif node.listen ~= nil then
local _, _, err = parse_listen(node.listen)
if err ~= nil then
error("failed to parse http 'listen' param: " .. err, 3)
end
else
check_server_httpd_role(httpd_role.DEFAULT_SERVER_NAME)
end

node.endpoints = node.endpoints or {}
Expand Down Expand Up @@ -221,11 +253,19 @@ local function validate_http(conf)
end

for i, nodei in ipairs(conf) do
local hosti, porti, erri = parse_listen(nodei.listen)
local listen_address = nodei.listen
if listen_address == nil then
listen_address = get_server_from_httpd_role_cfg(nodei.server).listen
end
local hosti, porti, erri = parse_listen(listen_address)
assert(erri == nil) -- We should already successfully parse the URI.
for j, nodej in ipairs(conf) do
if i ~= j then
local hostj, portj, errj = parse_listen(nodej.listen)
listen_address = nodej.listen
if listen_address == nil then
listen_address = get_server_from_httpd_role_cfg(nodej.server).listen
end
local hostj, portj, errj = parse_listen(listen_address)
assert(errj == nil) -- The same.
if hosti == hostj and porti == portj then
error("http configuration nodes must have different listen targets", 2)
Expand Down Expand Up @@ -258,25 +298,47 @@ local function apply_http(conf)
local enabled = {}
for _, node in ipairs(conf) do
if #(node.endpoints or {}) > 0 then
local host, port, err = parse_listen(node.listen)
if err ~= nil then
error("failed to parse URI: " .. err, 2)
local host, port, target
if node.server ~= nil then
local server = httpd_role.get_server(node.server)
host, port = server.host, server.port
target = node.server
elseif node.listen ~= nil then
target = node.listen
local err
host, port, err = parse_listen(node.listen)
if err ~= nil then
error("failed to parse URI: " .. err, 2)
end
else
local server = httpd_role.get_server()
host, port = server.host, server.port
target = httpd_role.DEFAULT_SERVER_NAME
end
local listen = node.listen

http_servers = http_servers or {}
enabled[listen] = true
enabled[target] = true

if http_servers[target] == nil then
local httpd
local is_httpd_role = false
if node.listen ~= nil then
httpd = http_server.new(host, port)
httpd:start()
else
httpd = httpd_role.get_server(target)
is_httpd_role = true
end

if http_servers[listen] == nil then
local httpd = http_server.new(host, port)
httpd:start()
http_servers[listen] = {
http_servers[target] = {
httpd = httpd,
routes = {},
is_httpd_role = is_httpd_role,
}
end
local server = http_servers[listen]
local httpd = server.httpd

local server = http_servers[target]
local httpd = server.httpd or server
local old_routes = server.routes

local new_routes = {}
Expand Down Expand Up @@ -317,15 +379,23 @@ local function apply_http(conf)

for listen, server in pairs(http_servers) do
if not enabled[listen] then
server.httpd:stop()
if not server.is_httpd_role then
server.httpd:stop()
else
for path, _ in pairs(server.routes) do
server.httpd:delete(path)
end
end
http_servers[listen] = nil
end
end
end

local function stop_http()
for _, server in pairs(http_servers or {}) do
server.httpd:stop()
if not server.is_httpd_role then
server.httpd:stop()
end
end
http_servers = nil
end
Expand Down
20 changes: 19 additions & 1 deletion test/entrypoint/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,13 @@ groups:
replicaset-001:
instances:
master:
roles: [roles.metrics-export]
roles: [roles.httpd, roles.metrics-export]
roles_cfg:
roles.httpd:
default:
listen: 8083
additional:
listen: '127.0.0.1:8084'
roles.metrics-export:
http:
- listen: 8081
Expand All @@ -29,6 +34,19 @@ groups:
format: prometheus
metrics:
enabled: true
- endpoints:
- path: /metrics/prometheus
format: prometheus
- path: /metrics/json
format: json
- server: 'additional'
endpoints:
- path: /metrics/prometheus
format: prometheus
- path: /metrics/json
format: json
metrics:
enabled: true
iproto:
listen:
- uri: '127.0.0.1:3313'
Expand Down
55 changes: 55 additions & 0 deletions test/helpers/mocks.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
local M = {}

local validate = function (mocks)
if type(mocks) ~= "table" then
error("mocks should have a table type, got " .. type(mocks))
end

for _, mock in ipairs(mocks) do
if type(mock.module) ~= "string" then
error("module name should have a string type, got " .. type(mock.module))
end
local ok, _ = pcall(require, mock.module)
if not ok then
error("cannot require module " .. mock.module)
end

if type(mock.method) ~= "string" then
error("method name should have a string type, got " .. type(mock.method))
end
if require(mock.module)[mock.method] == nil then
error("there is no method called " .. mock.method .. " in " .. mock.module)
end

if type(mock.implementation) ~= "function" then
error("implementation type should be a function, got " .. mock.implementation)
end
end
end

-- M.init validates mocks and initializes it.
M.init = function (mocks)
validate(mocks)

M.mocks = {}
for _, mock in ipairs(mocks) do
table.insert(M.mocks, mock)
end
end

-- M.apply replaces methods from initialized list.
M.apply = function ()
for _, mock in ipairs(M.mocks) do
mock.original_implementation = require(mock.module)[mock.method]
require(mock.module)[mock.method] = mock.implementation
end
end

-- M.delete returns original implementation from mocked method.
M.delete = function ()
for _, mock in ipairs(M.mocks) do
require(mock.module)[mock.method] = mock.original_implementation
end
end

return M
Loading

0 comments on commit 525b74a

Please sign in to comment.