Current section
Files
Jump to
Current section
Files
lib/smlr.ex
defmodule Smlr do
@behaviour Plug
@moduledoc ~S"""
Compresses the output of the plug with the correct compressor,
if enabled cache the compressed output so that when requested again we can return it instantly
rather than having to compress it again.
Add the plug at the bottom of one or more pipelines in `router.ex`:
pipeline "myapp" do
# ...
plug (Smlr)
end
"""
require Logger
alias Smlr.{Cache, Config}
alias Plug.Conn
@impl Plug
@doc ~S"""
Init function sets all default variables and .
"""
def init(opts) when is_list(opts) do
opts
|> Enum.chunk_every(2)
|> Enum.into(%{}, fn [key, val] -> {key, val} end)
|> init()
end
def init(opts) do
opts
end
defp parse_request_header([], _compressors, _ignore_client_weight) do
nil
end
defp parse_request_header([header], compressors, true) do
schemes =
String.downcase(header)
|> String.split(",")
|> Enum.map(fn scheme ->
scheme
|> String.split(";")
|> Enum.at(0)
|> String.trim()
end)
Enum.find(compressors, nil, fn compressor ->
compressor.name() in schemes
end)
end
defp parse_request_header([header], compressors, false) do
schemes = String.split(header, ",")
{choice, _weight} =
Enum.reduce(schemes, {nil, -1}, fn scheme, acc ->
split_scheme = String.split(scheme, ";")
case Enum.count(split_scheme) do
1 ->
Enum.at(split_scheme, 0)
|> String.trim()
|> String.downcase()
|> enabled_compressor(compressors)
|> choose_compressor(acc)
2 ->
new_weight = get_weight(Enum.at(split_scheme, 1))
Enum.at(split_scheme, 0)
|> String.trim()
|> String.downcase()
|> enabled_compressor(compressors, new_weight)
|> choose_compressor(acc)
end
end)
choice
end
defp get_weight(weight) do
split_string = String.split(weight, "=")
case Enum.count(split_string) do
2 ->
number =
Enum.at(split_string, 1)
|> String.trim()
case String.contains?(number, ".") do
true ->
Float.parse(number)
false ->
Integer.parse(number)
end
_ ->
-1
end
end
defp choose_compressor({new_choice, new_choice_weight}, {current_choice, current_choice_weight}) do
case current_choice_weight >= new_choice_weight do
true ->
{current_choice, current_choice_weight}
false ->
{new_choice, new_choice_weight}
end
end
defp enabled_compressor(compression, compressors, weight \\ 0) do
case Enum.find(compressors, nil, fn compressor ->
compressor.name() == compression
end) do
nil ->
{nil, -1}
compressor ->
{compressor, weight}
end
end
@impl Plug
@doc ~S"""
Call function. we check to see if the client has requested compression if it has, we register call back and compress before sending
"""
@spec call(Conn.t(), Keyword.t()) :: Conn.t()
def call(conn, opts) do
case Config.config(:enabled, opts) do
true ->
conn
|> Conn.get_req_header("accept-encoding")
|> parse_request_header(Config.config(:compressors, opts), Config.config(:ignore_client_weight, opts))
|> pass_or_compress(conn, opts)
false ->
conn
end
end
defp pass_or_compress(nil, conn, _opts) do
:telemetry.execute([:smlr, :request, :pass], %{}, %{path: conn.request_path})
conn
end
defp pass_or_compress(compressor, conn, opts) do
Conn.register_before_send(conn, fn conn ->
compress_response(conn, Map.put(opts, :compressor, compressor))
end)
end
defp compress_response(conn, opts) do
conn
|> Conn.put_resp_header("content-encoding", opts.compressor.name())
|> Map.put(:resp_body, compress(conn.resp_body, conn.request_path, opts))
end
defp compress(body, path, opts) do
# We do this because io lists are a pain and strings are easy
case Cache.get_from_cache(body, opts.compressor.name(), Config.config(:cache_opts, opts)) do
nil ->
:telemetry.execute([:smlr, :request, :compress], %{}, %{
path: path,
compressor: opts.compressor.name(),
level: opts.compressor.level(opts)
})
opts.compressor.compress(:erlang.iolist_to_binary(body), opts)
|> Cache.set_for_cache(body, opts.compressor.name(), Config.config(:cache, opts))
compressed ->
:telemetry.execute([:smlr, :request, :cache], %{}, %{
path: path,
compressor: opts.compressor.name(),
level: opts.compressor.level(opts)
})
compressed
end
end
end