mirror of
https://github.com/mailscope/kumomta.git
synced 2026-08-18 18:38:18 +00:00
224 lines
7.7 KiB
Markdown
224 lines
7.7 KiB
Markdown
# Publishing Log Events Via Webhooks
|
|
|
|
While logs are an invaluable resource for monitoring and troubleshooting mail
|
|
flows, log rotating and parsing adds complexity and latency when the goal is
|
|
loading the email event data into an existing platform.
|
|
|
|
Webhooks are ideal for near real-time integration into existing platforms,
|
|
providing the ability to send message events to a user-defined HTTP endpoint,
|
|
with queuing out of the box to ensure durability in the event of an error on
|
|
the part of the HTTP receiving service.
|
|
|
|
Webhooks are implemented in KumoMTA by triggering a Lua hook on log events that
|
|
allows for a policy script to load the log events into their own message queue
|
|
within the KumoMTA queueing structure like any other message, ensuring
|
|
durability and performance for queued log events.
|
|
|
|
Webhook events are moved through the queues like SMTP messages, and when they
|
|
enter the Ready Queue they are set to deliver via an arbitrary Lua event rather
|
|
than SMTP, with the Lua script configured to issue an HTTP request to the
|
|
destination server.
|
|
|
|
## Using the log_hooks.lua Helper
|
|
|
|
We strongly recommend that all users make use of the `policy-extras.log_hooks`
|
|
module for their web (or other protocol) hooks. The module is much more
|
|
convenient to use than the underlying low level events, and handles some
|
|
subtle edge cases for you.
|
|
|
|
To implement the helper, add the following to your init.lua:
|
|
|
|
```lua
|
|
local log_hooks = require 'policy-extras.log_hooks'
|
|
|
|
-- Send a JSON webhook to a local network host.
|
|
-- See https://docs.kumomta.com/userguide/operation/webhooks/
|
|
log_hooks:new_json {
|
|
name = 'webhook',
|
|
url = 'http://10.0.0.1:4242/log',
|
|
log_parameters = {
|
|
headers = { 'Subject', 'X-Customer-ID' },
|
|
},
|
|
}
|
|
```
|
|
|
|
!!!Warning
|
|
The call to `new_json` must appear before the queues helper for it to work
|
|
properly. See the [Example Config](../configuration/example.md) to see a
|
|
working layout for the `init.lua` file.
|
|
|
|
!!!Warning
|
|
The default traffic shaping rules that are likely present in your [Shaping Helper](../trafficshaping/shapingfiles.md) configuration will also affect messages queued for delivery via webhooks. To avoid issues, add throttle configuration specific to your webhook queue (based on the name provided when creating a webhook):
|
|
|
|
```toml
|
|
['webhook.log_hook']
|
|
mx_rollup = false
|
|
connection_limit = 100
|
|
max_deliveries_per_connection = 100000
|
|
max_connection_rate = "1000/s"
|
|
```
|
|
|
|
More advanced usage is possible by implementing the full call to the
|
|
`log_hooks.lua` helper; the example below shows approximately
|
|
how you might define your own equivalent of `log_hooks:new_json`:
|
|
|
|
```lua
|
|
local log_hooks = require 'policy-extras.log_hooks'
|
|
log_hooks:new {
|
|
name = 'webhook',
|
|
-- log_parameters are combined with the name and
|
|
-- passed through to kumo.configure_log_hook
|
|
log_parameters = {
|
|
headers = { 'Subject', 'X-Customer-ID' },
|
|
},
|
|
-- queue config are passed to kumo.make_queue_config.
|
|
-- You can use these to override the retry parameters
|
|
-- if you wish.
|
|
-- The defaults are shown below.
|
|
queue_config = {
|
|
retry_interval = '1m',
|
|
max_retry_interval = '20m',
|
|
},
|
|
|
|
-- The constructor is called when kumod needs to initiate
|
|
-- a new connection to the log target. It must return
|
|
-- a connection object
|
|
constructor = function(domain, tenant, campaign)
|
|
-- Define the connection object
|
|
local connection = {}
|
|
|
|
-- Create an HTTP client
|
|
local client = kumo.http.build_client {}
|
|
|
|
-- The send method is called for each log event
|
|
function connection:send(message)
|
|
local response = client
|
|
:post('http://10.0.0.1:4242/log')
|
|
:header('Content-Type', 'application/json')
|
|
:body(message:get_data())
|
|
:send()
|
|
|
|
local disposition = string.format(
|
|
'%d %s: %s',
|
|
response:status_code(),
|
|
response:status_reason(),
|
|
response:text()
|
|
)
|
|
|
|
if response:status_is_success() then
|
|
return disposition
|
|
end
|
|
|
|
-- Signal that the webhook request failed.
|
|
-- In this case the 500 status prevents us from retrying
|
|
-- the webhook call again, but you could be more sophisticated
|
|
-- and analyze the disposition to determine if retrying it
|
|
-- would be useful and generate a 400 status instead.
|
|
-- In that case, the message will be retried later, until
|
|
-- it reached its expiration.
|
|
kumo.reject(500, disposition)
|
|
end
|
|
|
|
-- The close method is called when the connection needs
|
|
-- to be closed
|
|
function connection:close()
|
|
client:close()
|
|
end
|
|
|
|
return connection
|
|
end,
|
|
}
|
|
```
|
|
|
|
You can use the above to define logging that uses other protocols
|
|
than HTTP, such as AMQP or Kafka.
|
|
|
|
## batched hooks
|
|
|
|
{{since('2024.11.08-d383b033')}}
|
|
|
|
It can be desirable for log events to be delivered to the destination
|
|
system in a batch; the primary motivation for this is to amortize the
|
|
cost of a database transaction on the remote system by handling more than
|
|
one record per transaction.
|
|
|
|
You can implement batching by setting the `batch_size` parameter
|
|
to a value greater than 1. When you do this, the hook is run in a batch
|
|
mode and it is expected to return a `connection` object that has
|
|
a `send_batch` method rather than the `send` method shown in the example
|
|
above.
|
|
|
|
When in batch mode, the connection will receive a batch consisting of
|
|
1 or more messages, up to the `batch_size` that you configured. The batch
|
|
can be less than the `batch_size`; the connection will pop off up-to the
|
|
configured number of messages from the *ready queue*. That queue holds
|
|
only a finite number of messages that are immediately ready for delivery.
|
|
The popping process does not artificially delay to encourage a larger
|
|
batch size. It will grab whatever is immediately ready and send it
|
|
as a batch.
|
|
|
|
Here's how you would write something that is similar to the above example
|
|
using batching:
|
|
|
|
```lua
|
|
local log_hooks = require 'policy-extras.log_hooks'
|
|
log_hooks:new {
|
|
name = 'webhookbatch',
|
|
-- batches of up to 100 messages at a time
|
|
batch_size = 100,
|
|
constructor = function(domain, tenant, campaign)
|
|
local connection = {}
|
|
local client = kumo.http.build_client {}
|
|
|
|
-- This method must be named send_batch when batch_size > 1
|
|
function connection:send_batch(messages)
|
|
local payload = {}
|
|
for _, msg in ipairs(messages) do
|
|
-- Rather than collecting the pre-templated record as
|
|
-- a string, get it as an object. This makes it easier
|
|
-- to compose it as an array and json encode than doing
|
|
-- the string manipulation by-hand.
|
|
table.insert(payload, msg:get_meta 'log_record')
|
|
end
|
|
|
|
-- encode the array of objects as json
|
|
local data = kumo.serde.json_encode(payload)
|
|
|
|
local response = client
|
|
:post('http://10.0.0.1:4242/log')
|
|
:header('Content-Type', 'application/json')
|
|
:body(data)
|
|
:send()
|
|
|
|
local disposition = string.format(
|
|
'%d %s: %s',
|
|
response:status_code(),
|
|
response:status_reason(),
|
|
response:text()
|
|
)
|
|
|
|
if response:status_is_success() then
|
|
return disposition
|
|
end
|
|
kumo.reject(500, disposition)
|
|
end
|
|
|
|
function connection:close()
|
|
client:close()
|
|
end
|
|
|
|
return connection
|
|
end,
|
|
}
|
|
```
|
|
|
|
If your `send_batch` method returns a transient failure, either by allowing
|
|
errors to escape the function without being caught by `pcall`, or by
|
|
explicitly using `kumo.reject` with a `4xx` status code, then that
|
|
transient disposition applies to every message in the batch. Each
|
|
transiently failed message will have its own jittered retry time computed,
|
|
and it will be reattempted at a later time. This per-message jitter can
|
|
help to break out of a situation where one message in the batch is somehow
|
|
objectionable to the destination endpoint and continues to cause the
|
|
messages that get lumped into its batch to transiently fail.
|