Admin Lua Extensions

We support users to write Lua extensions to perform some custom functions, which can be triggered by cron or events.

For example, we can query the database every minute for nodes with high cpu and report them to the user’s own monitoring system via HTTP requests.

Here is an practical example of how to create a Lua extension.


We create a cron extension with the following Lua code:

local sql = [[
select node_id, avg("system_CPU_percent") from monitor where created > now() - INTERVAL '1 hour' group by node_id limit 1
local res = sql_query(sql, 120, 2000, "log_server")

local params = {
    body = res
res = http_query('POST', "", params)


Then we can click the execute button to see the results immediately.


The execution results of the extension can be seen in the history page.



Lua extensions can also be triggered by events. When the specified event occurs, it will trigger the extension to run and the event details will be placed in the Lua variable trigger_event.

Here is an example of printing events.



Click the execute button to see the results immediately. A node offline test event is provided here by default.



We currently support these events:

Nodes Heartbeat Offline

Triggered when the nodes do not send heartbeats to Admin and are set to offline.

    "type": "nodes_heartbeat_offline",
    "from": "log-server",
    "message": "Gateway nodes [59] offline",
    "level": "ERROR"

Nodes Heartbeat Online

Triggered when the nodes send heartbeats to Admin and are set to online.

    "type": "nodes_heartbeat_online",
    "from": "log-server",
    "message": "Gateway nodes [59] online",
    "level": "WARNING"

Nodes Offline

Triggered when the node’s health check fails and the nodes are set to offline.

    "from": "admin",
    "level": "ERROR",
    "message": "gateway node [78] is offline since failed to connect to connection refused, time: 1634629224;;failed to connect to connection refused, time: 1634629254;;failed to connect to connection refused, time: 1634629284",
    "type": "node_offline"

Nodes Online

Triggered when the node’s health check is successful and the nodes are set to online.

    "from": "admin",
    "level": "WARNING",
    "message": "gateway node [78] is online since health check success",
    "type": "node_online"


Triggered when the application is released.

    "type": "release",
    "uid": 2,
    "http_app_id": 786

WAF Event

Triggered when the request hit WAF rules and the score is greater than the threshold value.

The requests of the same client IP will only trigger a WAF event once in a second.

    "app_id": "1033",
    "type": "waf_event",
    "score": "3",
    "threshold": "3",
    "action": "block",
    "matches": [
            "matches": [
            "begin_line": 1,
            "version": "d04fb751526fc85a172475a71f19cf53",
            "rule_id": "0",
            "rule_set_id": 10025,
            "msg": "test",
            "group": "test",
            "end_line": 2
    "header": "User-Agent: curl/7.29.0\r\nHost:\r\nAccept: */*\r\nProxy-Connection: Keep-Alive\r\n\r\n",
    "timestamp": "1634629481",
    "request_id": "0000270010243927eb48000d",
    "client_country": "",
    "client_province": "unknown",
    "client_city": "unknown",
    "from": "log_server",
    "client_isp": "",
    "body": "",
    "remote_addr": "",
    "host": "",
    "request": "GET HTTP:// HTTP/1.1"

Builtin API

Most of the regular Lua code is supported in the extensions, and we also provide a built-in lua api for writing extensions.


syntax: output(msg)

The output messages will be displayed in the Lua extension histories.


syntax: res = http_query(method, url, params, retries)

Send the HTTP request.

  • method The HTTP method, like GET, POST, PUT.
  • url The HTTP url string.
  • retries Number of retries after request failed, default retries is 1.

The params table accepts the following fields:

  • timeout Sets the timeout (in millisecond), default value is 300 seconds.
  • headers A table of request headers.
  • body The request body as a string, or an iterator function (see get_client_body_reader).
  • ssl_verify Verify SSL cert matches hostname

When the request is successful, res will contain the following fields:

  • status The status code.
  • headers A table of headers. Multiple headers with the same field name will be presented as a table of values.
  • body The response body.


Query the admin or log-server database.

syntax: res = sql_query(sql, timeout, limit, destination, retries)

  • sql Query SQL, only select statements are supported.
  • timeout Sets the timeout (in second), default value is 120 seconds.
  • limit Sets the limit of query result sets, default value is 20000.
  • destination Query destination: admin and log-server.
  • retries Number of retries after query failed, default retries is 1.


Sending custom alarm events

syntax: res = send_alarm_event(alarm_type, alarm_level, alarm_message)

  • alarm_type Customized alarm types
  • alarm_level Alarm levels, there are three levels: CRITICAL, ERROR, WARNING
  • alarm_message Alarm text content

More Examples

Add IP data blocked by the WAF to the application’s IP list

This extension queries the database for the IP data blocked by the WAF and adds them to the application’s IP list.

Here we need to enable WAF and IP list in the application first.

-- In this example we assume that the application id and IP list id are both 1.
local app_id = "1"
local ip_list_id = "1"

local str_fmt = string.format
local api_put = require "Lua.SchemaDB" .update

-- Get the IP addresses blocked by WAF in the last 24 hours
local sql = [[
SELECT DISTINCT remote_addr as ip
FROM waf_request_tsdb
WHERE action='block'
        AND score >= threshold
        AND created >= now() - interval '24 hours'
        AND app_id='%s'

sql = str_fmt(sql, app_id)
local res = sql_query(sql, 120, 2000, "log_server")
local ip_list = { items = res }

local uri = { "applications", app_id, "ip_list", ip_list_id }
res, err = api_put(uri, ip_list)

if res then
    output("updated ip blacklist successfully!")
    output("failed to update ip blacklist: " .. tostring(err))

Verify the HTTPS certificate of the application specified with the APP_ID

-- UPDATE-ME: please specify app_id as you want
local app_id = nil

-- uncomment following lines if you want enable event trigger
-- local app_id = trigger_event.http_app_id

if not app_id then
    return output("WARN: app_id is required")

local ngx = ngx
local substr = string.sub
local str_fmt = string.format
local re_find =
local httpc = require "resty.http".new()

local function ssl_handshake(ip, port, domain)
    local c, err = httpc:connect(ip, port)
    if not c then
        return nil, err

    return httpc:ssl_handshake(nil, domain, true)

local function is_wildcard_domain(domain)
    if string.sub(domain, 1, 2) == '*.' then
        return true

    return false

local app_domains_sql = [[
select applications_domains.domain "domain",
    applications_domains.is_wildcard is_wildcard,
    from applications
left join applications_domains on = applications_domains._applications_id
where = %d

local cert_domains_sql = [[
select applications_phases_ssl_cert_certs_acme_host.item acme_host
    from applications_phases_ssl_cert_certs
join applications_phases_ssl_cert_certs_acme_host on
where global_cert is null and applications_phases_ssl_cert_certs._applications_id = %d
select global_certs.acme_host acme_host
    from applications_phases_ssl_cert_certs
join global_certs on applications_phases_ssl_cert_certs.global_cert =
where global_cert is not null and applications_phases_ssl_cert_certs._applications_id = %d

local gateway_nodes_sql = [[
select gateway_nodes.external_ip external_ip,
    gateway_nodes.external_ipv6 external_ipv6
    from applications
left join applications_partitions on = applications_partitions._applications_id
left join gateway on applications_partitions.item = gateway.partition
left join gateway_nodes on = gateway_nodes._gateway_id
where offline_enabled is not true
    and (gateway_nodes.external_ip is not null or gateway_nodes.external_ipv6 is not null)
    and = %d

local ok_tbl = {}
local err_tbl = {}
local check_list = {}
local app_domains_hash = {}

local app_domains, err = sql_query(str_fmt(app_domains_sql, tonumber(app_id)))
if not app_domains then
    output(str_fmt("failed to verify TLS certificate for app, app_id: %d"), tostring(app_id))

local domain_list = {}
for _, app_domain in ipairs(app_domains) do
    domain_list[#domain_list + 1] = app_domain.domain

    app_domains_hash[app_domain.domain] = app_domain

local domain_str = table.concat(domain_list, ', ')

local cert_domains, err = sql_query(str_fmt(cert_domains_sql, tonumber(app_id), tonumber(app_id)))
if not cert_domains then
    output("failed to verify TLS certificate for app, domain: %s, err: no certificate found", domain_str)

local gateway_nodes, err = sql_query(str_fmt(gateway_nodes_sql, tonumber(app_id)))
if not gateway_nodes then
    output("failed to verify TLS certificate for app, domain: %s, err: no gateway nodes found", domain_str)

for _, cert_domain in ipairs(cert_domains) do
    local acme_host = cert_domain.acme_host

    if is_wildcard_domain(acme_host) and app_domains_hash[acme_host] then
        err_tbl[#err_tbl + 1] = "wildcard app with wildcard cert is not supported yet: " .. tostring(acme_host)
        goto _next_

    if is_wildcard_domain(acme_host) then
        for _, app_domain in ipairs(app_domains) do
            local domain = app_domain.domain
            local base_acme_host = substr(acme_host, 2)

            if re_find(domain, [[\A(?:\Q\E.*?\Q]] .. base_acme_host .. [[\E)]], 'josm') then
                check_list[#check_list + 1] = app_domains_hash[app_domain]

        goto _next_

    local hit = false

    for _, app_domain in ipairs(app_domains) do
        local domain = app_domain.domain

        if domain == acme_host then
            check_list[#check_list + 1] = app_domains_hash[acme_host]

            goto _next_

        if is_wildcard_domain(domain) then
            local base_domain = substr(domain, 2)

            if re_find(acme_host, [[\A(?:\Q\E.*?\Q]] .. base_domain .. [[\E)]], 'josm') then
                local app_obj = app_domains_hash[domain]

                check_list[#check_list + 1] = {
                    domain = acme_host,
                    is_wildcard = app_obj.is_wildcard,
                    https_ports = app_obj.https_ports
                hit = true

    if hit then
        goto _next_

    err_tbl[#err_tbl + 1] = str_fmt(
        "certificate with Common Name '%s' not found matched host in current application '%s'",
        acme_host, domain_str)


local check_domain_list = {}

for _, check_obj in pairs(check_list) do
    local check_domain = check_obj.domain
    local https_ports = check_obj.https_ports

    check_domain_list[#check_domain_list + 1] = check_domain

    for _, gateway_node in ipairs(gateway_nodes) do
        local ip = gateway_node.external_ip or gateway_node.external_ipv6
        if not ip then
            goto _next_node_

        for _, https_port in ipairs(https_ports) do
            local ok, err = ssl_handshake(ip, https_port, check_domain)
            if not ok then
                err_tbl[#err_tbl + 1] = str_fmt("domain '%s' on ip '%s': '%s'",
                    check_domain, ip, err)
                goto _next_node_

            ok_tbl[#ok_tbl + 1] = str_fmt("domain '%s' on ip '%s'", check_domain, ip)


if #err_tbl == 0 then
    output("OK: all TLS certificates are verified: " .. table.concat(check_domain_list, ','))

    output("ERR: following TLS certificates are failed: " .. table.concat(err_tbl, "\n === \n"))

    if #ok_tbl > 0 then
        output("INFO: following TLS certificates are verified: " .. table.concat(ok_tbl, "\n === \n"))

        output("ERR: no TLS certificate is verified successfully")