Packages
phoenix_gen_api
2.20.1
2.22.0
2.21.1
2.21.0
2.20.1
2.20.0
2.19.0
2.18.1
2.18.0
2.17.1
2.17.0
2.16.0
2.15.0
2.14.0
2.13.0
2.12.0
2.11.0
2.10.0
2.9.1
2.9.0
2.8.0
2.7.0
2.6.1
2.6.0
2.5.0
2.4.0
2.3.0
2.2.0
2.1.1
2.1.0
2.0.1
2.0.0
1.2.0
1.1.3
1.1.2
1.1.1
1.1.0
1.0.1
1.0.0
0.2.1
0.2.0
0.1.1
0.1.0
0.0.16
0.0.15
0.0.14
0.0.13
0.0.12
0.0.11
0.0.10
0.0.9
0.0.8
0.0.7
0.0.6
0.0.5
0.0.4
0.0.3
0.0.2
0.0.1
A library for fast develop APIs in Elixir cluster, using Phoenix Channels for transport data, auto pull api configs from service nodes.
Current section
Files
Jump to
Current section
Files
lib/phoenix_gen_api/hooks/hook.ex
defmodule PhoenixGenApi.Hooks do
@moduledoc """
Provides before/after execution hooks for `FunConfig`.
Hooks allow users to run custom code before and/or after a function is executed
through the API. They are configured per-function via the `before_execute` and
`after_execute` fields in `FunConfig`.
## Hook Signature
Hooks are specified as MFA tuples:
- `{module, function}` — Calls `module.function(request, fun_config)` for before,
or `module.function(request, fun_config, result)` for after.
- `{module, function, extra_args}` — Appends extra arguments after the standard ones.
### Before execute
Must return one of:
- `{:ok, request, fun_config}` — Proceed with (possibly modified) request/config.
- `{:error, reason}` — Abort execution and return an error response.
### After execute
Must return the (possibly modified) result. Any other return value is ignored and
the original result is preserved.
## Telemetry
Hooks emit telemetry events at:
- `[:phoenix_gen_api, :hook, :before, :start]`
- `[:phoenix_gen_api, :hook, :before, :stop]`
- `[:phoenix_gen_api, :hook, :before, :exception]`
- `[:phoenix_gen_api, :hook, :after, :start]`
- `[:phoenix_gen_api, :hook, :after, :stop]`
- `[:phoenix_gen_api, :hook, :after, :exception]`
## Timeout
Each hook execution is wrapped in a `Task` with a per-hook timeout
(default: 5 seconds). If a hook does not complete within the timeout,
the task is shut down and an error is returned. This prevents a
misbehaving hook from blocking the request indefinitely.
Hook crashes (exceptions) are also caught via `Task.yield/2` returning
`{:exit, reason}`, and are reported the same way as before.
## Examples
# In your FunConfig:
%FunConfig{
before_execute: {MyApp.Hooks, :validate_quota},
after_execute: {MyApp.Hooks, :log_response}
}
# Hook module:
defmodule MyApp.Hooks do
alias PhoenixGenApi.Structs.{Request, FunConfig}
def validate_quota(request, fun_config) do
# Check rate quota, enrich request, etc.
{:ok, request, fun_config}
end
def log_response(request, fun_config, result) do
# Log metrics, audit trail, etc.
result
end
end
"""
alias PhoenixGenApi.Structs.{Request, FunConfig}
require Logger
@default_hook_timeout 5_000
@doc """
Runs the before_execute hook if configured.
Returns `{:ok, request, fun_config}` to proceed, or `{:error, reason}` to abort.
"""
@spec run_before(
nil | {module(), atom()} | {module(), atom(), list()},
Request.t(),
FunConfig.t()
) :: {:ok, Request.t(), FunConfig.t()} | {:error, any()}
def run_before(nil, request, fun_config), do: {:ok, request, fun_config}
def run_before({mod, fun}, request, fun_config) do
case execute_hook(:before, mod, fun, [request, fun_config]) do
{:ok, {:ok, new_request, new_fun_config}} ->
{:ok, new_request, new_fun_config}
{:ok, {:error, reason}} ->
{:error, reason}
{:ok, _} ->
{:ok, request, fun_config}
{:error, reason} ->
{:error, reason}
end
end
def run_before({mod, fun, extra_args}, request, fun_config) do
case execute_hook(:before, mod, fun, [request, fun_config | extra_args]) do
{:ok, {:ok, new_request, new_fun_config}} ->
{:ok, new_request, new_fun_config}
{:ok, {:error, reason}} ->
{:error, reason}
{:ok, _} ->
{:ok, request, fun_config}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Runs the after_execute hook if configured.
Returns the (possibly modified) result. If the hook fails, the original result
is preserved.
"""
@spec run_after(
nil | {module(), atom()} | {module(), atom(), list()},
Request.t(),
FunConfig.t(),
any()
) :: any()
def run_after(nil, _request, _fun_config, result), do: result
def run_after({mod, fun}, request, fun_config, result) do
case execute_hook(:after, mod, fun, [request, fun_config, result]) do
{:ok, new_result} -> new_result
{:error, _} -> result
end
end
def run_after({mod, fun, extra_args}, request, fun_config, result) do
case execute_hook(:after, mod, fun, [request, fun_config, result | extra_args]) do
{:ok, new_result} -> new_result
{:error, _} -> result
end
end
defp execute_hook(type, mod, fun, args) do
start_time = System.monotonic_time(:microsecond)
:telemetry.execute(
[:phoenix_gen_api, :hook, type, :start],
%{system_time: System.system_time()},
%{module: mod, function: fun, type: type}
)
task =
Task.async(fn ->
try do
apply(mod, fun, args)
rescue
e -> {:__hook_exception, Exception.message(e)}
catch
kind, value -> {:__hook_exception, {kind, value}}
end
end)
result =
case Task.yield(task, @default_hook_timeout) do
{:ok, {:__hook_exception, reason}} ->
{:exit, reason}
{:ok, hook_result} ->
{:ok, hook_result}
nil ->
Task.shutdown(task, :brutal_kill)
{:timeout, "hook timed out after #{@default_hook_timeout}ms"}
{:exit, reason} ->
{:exit, reason}
end
duration = System.monotonic_time(:microsecond) - start_time
case result do
{:ok, hook_result} ->
:telemetry.execute(
[:phoenix_gen_api, :hook, type, :stop],
%{duration_us: duration},
%{module: mod, function: fun, type: type}
)
{:ok, hook_result}
{:timeout, reason} ->
:telemetry.execute(
[:phoenix_gen_api, :hook, type, :exception],
%{duration_us: duration},
%{
module: mod,
function: fun,
type: type,
kind: :timeout,
reason: reason,
stacktrace: nil
}
)
Logger.error(
"[Hooks] #{type} hook timed out after #{@default_hook_timeout}ms, module: #{inspect(mod)}, function: #{inspect(fun)}"
)
{:error, reason}
{:exit, reason} ->
:telemetry.execute(
[:phoenix_gen_api, :hook, type, :exception],
%{duration_us: duration},
%{
module: mod,
function: fun,
type: type,
kind: :error,
reason: inspect(reason),
stacktrace: nil
}
)
Logger.error(
"[Hooks] #{type} hook crashed: #{inspect(reason)}, module: #{inspect(mod)}, function: #{inspect(fun)}"
)
{:error, inspect(reason)}
end
end
end