Skip to content

Commit

Permalink
messenger: :slack (#41)
Browse files Browse the repository at this point in the history
* `messenger: :slack`

* `messenger_channels`
  • Loading branch information
am-kantox authored Nov 30, 2024
1 parent 75ee790 commit 6425701
Show file tree
Hide file tree
Showing 18 changed files with 295 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .credo.exs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@
## Refactoring Opportunities
#
{Credo.Check.Refactor.CondStatements, []},
{Credo.Check.Refactor.CyclomaticComplexity, [max_complexity: 12]},
{Credo.Check.Refactor.CyclomaticComplexity, [max_complexity: 16]},
{Credo.Check.Refactor.FunctionArity, []},
{Credo.Check.Refactor.LongQuoteBlocks, []},
{Credo.Check.Refactor.MapInto, false},
Expand Down
5 changes: 5 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ config :telemetria,
[:test, :telemetria, :example, :sum_with_doubled],
[:test, :telemetria, :example, :half],
[:test, :telemetria, :example, :half_named, :foo],
[:test, :telemetria, :example, :third],
[:test, :telemetria, :example, :tmed],
[:test, :telemetria, :example, :tmed_do],
[:test, :telemetria, :example, :guarded],
Expand All @@ -21,3 +22,7 @@ config :telemetria,
# config :logger, :default_formatter,
# format: {Telemetria.Formatter, :format},
# metadata: :all

if Mix.env() == :test do
config :telemetria, :messenger_channels, %{mox: {:mox, []}}
end
5 changes: 5 additions & 0 deletions examples/otel/config/runtime.exs.stdout
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import Config

config :opentelemetry,
span_processor: :batch,
traces_exporter: {:otel_exporter_stdout, []}
4 changes: 3 additions & 1 deletion examples/tm/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ config :telemetria,
events: [
[:tm, :f_to_c]
],
throttle: %{some_group: {1_000, :last}}
throttle: %{some_group: {1_000, :last}},
# create a slack app and put URL here
messenger_channels: %{slack: {:slack, url: ""}}
6 changes: 4 additions & 2 deletions examples/tm/lib/tm.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ defmodule Tm do

use Telemetria

@telemetria level: :info, group: :weather_reports, locals: [:celsius]
@telemetria level: :info, group: :weather_reports, locals: [:celsius], messenger: :slack
def f_to_c(fahrenheit) do
celsius = (fahrenheit - 32) * 5 / 9
celsius = do_f_to_c(fahrenheit)
round(celsius)
end

defp do_f_to_c(fahrenheit), do: (fahrenheit - 32) * 5 / 9
end
2 changes: 1 addition & 1 deletion examples/tm/mix.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
%{
"doctest_formatter": {:hex, :doctest_formatter, "0.3.1", "a3fd87c1f75e8a78e7737ec4a4494800ddda705998a59320b87fe4c59c030794", [:mix], [], "hexpm", "3c092540d8b73ffc526a92daa2dc2ecd50714f14325eeacbc7b4e790f890443a"},
"estructura": {:hex, :estructura, "1.6.0", "951be10eb4ed1a7e8acc6c965835c3803f39fcb66669d19347d4ff3f212e9dbb", [:mix], [{:doctest_formatter, "~> 0.2", [hex: :doctest_formatter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}], "hexpm", "1d5d180b0174a8100c175f42d78aa0f6d3afe79dd16ae0d28591bb03d406369b"},
"estructura": {:hex, :estructura, "1.6.0", "c55ded89911301f9f965f9e7e319d8d2edebb001aa82619866b01adf474744c0", [:mix], [{:doctest_formatter, "~> 0.2", [hex: :doctest_formatter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}], "hexpm", "f374cf08158782a04819cd4f97ad1dfcf52512aaf664e467a524bf23ceb0da37"},
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"stream_data": {:hex, :stream_data, "1.1.2", "05499eaec0443349ff877aaabc6e194e82bda6799b9ce6aaa1aadac15a9fdb4d", [:mix], [], "hexpm", "129558d2c77cbc1eb2f4747acbbea79e181a5da51108457000020a906813a1a9"},
Expand Down
4 changes: 2 additions & 2 deletions examples/tm/test/tm_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ defmodule TmTest do
log =
capture_log(fn ->
assert Tm.f_to_c(451) == 233
Process.sleep(100)
Process.sleep(500)
end)

assert log =~ "[warning] Unexpected throttle setting for group `:weather_reports` → nil"
Expand All @@ -25,7 +25,7 @@ defmodule TmTest do
{:ok, log} =
with_log(fn ->
Tm.f_to_c(451)
Process.sleep(100)
Process.sleep(500)
end)

assert log =~
Expand Down
5 changes: 5 additions & 0 deletions lib/options.ex
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ defmodule Telemetria.Options do
doc: "The backend to be used as an actual implementation",
default: Telemetria.Backend.Telemetry
],
messenger_channels: [
type: :map,
doc: "The messenger channels as a map `%{name => {impl, opts}}`",
default: %{}
],
level: [
type: {:custom, Telemetria.Options, :log_level, []},
doc:
Expand Down
27 changes: 26 additions & 1 deletion lib/telemetria.ex
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ defmodule Telemetria do
- **`reshape: (map() -> map())`** — the function to be called on the resulting attributes
to reshape them before sending to the actual telemetry handler; the default application-wide
reshaper might be set in `:telemetria, :reshaper` config
- **`messenger_channels: %{optional(atom()) => {module, keyword()}`** — more handy messenger
management, several channels config with channels names associated with their
implementations and properties
### Example
Expand Down Expand Up @@ -135,7 +138,9 @@ defmodule Telemetria do
@type event_prefix :: [atom()]
@type handler_config :: term()

@default_level Application.compile_env(:telemetria, :level, :info)
@default_reshaper Application.compile_env(:telemetria, :reshaper)
@messenger_channels Application.compile_env(:telemetria, :messenger_channels, %{})

@doc false
defmacro __using__(opts) do
Expand Down Expand Up @@ -327,6 +332,8 @@ defmodule Telemetria do
"transform must be a tuple `{mod, fun}` or a function capture, #{inspect(weird)} given"
end

level = get_in(context, [:options, :level]) || @default_level

group = get_in(context, [:options, :group])

args_transform =
Expand All @@ -341,6 +348,16 @@ defmodule Telemetria do
reshape =
context |> get_in([:options, :reshape]) |> Kernel.||(@default_reshaper)

messenger =
context
|> get_in([:options, :messenger])
|> case do
nil -> nil
false -> false
{mod, opts} -> {mod, Keyword.put_new(opts, :level, level)}
channel -> get_channel_info(channel, level)
end

{clause_args, context} = Keyword.pop(context, :arguments, [])
args = Keyword.merge(args, clause_args)

Expand Down Expand Up @@ -373,7 +390,8 @@ defmodule Telemetria do

Telemetria.Throttler.execute(
unquote(group),
{block_ctx, %{system_time: now, consumed: benchmark}, attributes, unquote(reshape)}
{block_ctx, %{system_time: now, consumed: benchmark}, attributes, unquote(reshape),
unquote(messenger)}
)

Backend.exit(block_ctx)
Expand Down Expand Up @@ -409,6 +427,13 @@ defmodule Telemetria do
defp variablize({:%{}, _, elems}), do: {:map, Enum.map(elems, &variablize/1)}
defp variablize({var, _, _} = val), do: {var, val}

defp get_channel_info(channel, level) do
case Map.get(@messenger_channels, channel, {channel, []}) do
{mod, opts} -> {mod, Keyword.put(opts, :level, level)}
mod when is_atom(mod) -> {mod, level: level}
end
end

defp extract_guards([]), do: []

defp extract_guards([_ | _] = list) do
Expand Down
64 changes: 64 additions & 0 deletions lib/telemetria/messenger.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
defmodule Telemetria.Messenger do
@moduledoc """
The helper allowing quick sending of the telemetry events to the messenger.
"""

@typedoc "The type of the message the messenger is to process and send"
@type message :: %{
required(atom()) => term()
}

@doc "The formatter of the incoming message, producing the binary to be sent over the wire"
@callback format(message(), keyword()) :: message() | String.t()

@doc "The actual implementation of the message sending (`debug` level)"
@callback debug(message(), keyword()) :: {:ok, term()} | {:error, term()}

@doc "The actual implementation of the message sending (`info` level)"
@callback info(message(), keyword()) :: {:ok, term()} | {:error, term()}

@doc "The actual implementation of the message sending (`warning` level)"
@callback warning(message(), keyword()) :: {:ok, term()} | {:error, term()}

@doc "The actual implementation of the message sending (`error` level)"
@callback error(message(), keyword()) :: {:ok, term()} | {:error, term()}

@optional_callbacks format: 2

@implementation Telemetria.Messenger.Logger

@doc "Routes the message to the configured messenger(s)"
@spec post(message() | String.t(), impl :: atom() | module(), opts :: keyword()) ::
{:ok, term()} | {:error, term()}
def post(message, impl \\ @implementation, opts \\ [])

def post(%{} = message, impl, opts) do
impl = fix_impl_name(impl)

message =
if function_exported?(impl, :format, 2),
do: impl.format(message, opts),
else: inspect(message, opts)

do_post(message, impl, opts)
end

def post(message, impl, opts) when is_binary(message),
do: do_post(message, impl, opts)

defp do_post(message, impl, opts) do
impl = fix_impl_name(impl)
{level, opts} = Keyword.pop(opts, :level, :info)
apply(impl, level, [message, opts])
end

@spec fix_impl_name(atom()) :: module()
defp fix_impl_name(true), do: fix_impl_name(@implementation)

defp fix_impl_name(impl) do
case to_string(impl) do
"Elixir." <> _ -> impl
_ -> Module.concat([Telemetria, Messenger, impl |> to_string() |> Macro.camelize()])
end
end
end
18 changes: 18 additions & 0 deletions lib/telemetria/messenger/logger.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
defmodule Telemetria.Messenger.Logger do
@moduledoc false

require Logger

@behaviour Telemetria.Messenger

@impl true
def format(message, opts), do: inspect(message, opts)

Enum.each(~w|debug info warning error|a, fn level ->
@impl true
def unquote(level)(message, opts),
do: post(unquote(level), message, opts)
end)

defp post(level, message, opts), do: {Logger.log(level, message), opts}
end
111 changes: 111 additions & 0 deletions lib/telemetria/messenger/slack.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
defmodule Telemetria.Messenger.Slack do
@moduledoc false

@behaviour Telemetria.Messenger

@impl true
# %{
# args: [a: 42],
# env: %{
# function: {:half, 1},
# line: 23,
# module: Test.Telemetria.Example,
# file: "/home/am/Proyectos/Elixir/telemetria/test/support/telemetria_tester.ex"
# },
# context: [],
# result: 21.0,
# locals: [],
# measurements: %{
# system_time: [
# system: 1732885662078938716,
# monotonic: -576460750195561,
# utc: ~U[2024-11-29 13:07:42.078940Z]
# ],
# consumed: 3336
# },
# telemetria_group: :default
# }
def format(message, opts) do
with {event, message} <- Map.pop(message, :event),
{%{function: {f, a}} = env, message} <- Map.pop(message, :env),
{level, message} <-
Map.pop_lazy(message, :level, fn -> Keyword.get(opts, :level, :info) end),
{icon, message} <- Map.pop(message, :icon, slack_icon(level)) do
title = Enum.join(event, ".")

pretext =
env.module
|> Function.capture(f, a)
|> inspect()
|> Kernel.<>("\n#{env.file}:#{env.line}")

fields =
message
|> Estructura.Flattenable.flatten(jsonify: true)
|> Enum.map(fn {k, v} ->
%{
title: k,
value: v,
short: not is_binary(v) or String.length(v) < 32
}
end)

attachments =
%{
color: slack_color(level),
fields: fields,
mrkdwn_in: ["title", "text", "pretext"]
}
|> Map.merge(%{pretext: "```\n" <> pretext <> "\n```"})

fallback =
[title, pretext]
|> Enum.reject(&is_nil/1)
|> Enum.join("\n")

%{
description: title,
emoji_icon: icon,
fallback: fallback,
mrkdwn: true,
attachments: [attachments]
}
end
end

Enum.each(~w|debug info warning error|a, fn level ->
@impl true
def unquote(level)(message, opts),
do: post(unquote(level), message, opts)
end)

defp post(level, message, opts) do
json =
message
|> put_in([:emoji_icon], slack_icon(level))
|> put_in([:attachments, Access.all(), :color], slack_color(level))
|> Jason.encode!()
|> :erlang.binary_to_list()

url = Keyword.fetch!(opts, :url)

:httpc.request(:post, {to_charlist(url), [], ~c"application/json", json}, [], [])
end

defp slack_icon(:debug), do: ":speaker:"
defp slack_icon(:info), do: ":information_source:"
defp slack_icon(:warn), do: ":warning:"
defp slack_icon(:warning), do: slack_icon(:warn)
defp slack_icon(:error), do: ":exclamation:"

defp slack_icon(level) when is_binary(level),
do: level |> String.to_existing_atom() |> slack_icon()

defp slack_icon(_), do: slack_icon(:info)

defp slack_color(:debug), do: "#AAAAAA"
defp slack_color(:info), do: "good"
defp slack_color(:warn), do: "#FF9900"
defp slack_color(:warning), do: slack_color(:warn)
defp slack_color(:error), do: "danger"
end
10 changes: 8 additions & 2 deletions lib/telemetria/throttler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule Telemetria.Throttler do

def execute(group \\ nil, event), do: GenServer.cast(name(), {:event, group || :default, event})

defp name do
def name do
[Telemetria.otp_app(), :telemetria, :throttler]
|> Enum.map(&Atom.to_string/1)
|> Enum.map(&Macro.camelize/1)
Expand Down Expand Up @@ -67,13 +67,19 @@ defmodule Telemetria.Throttler do
Logger.warning("Wrong config for group: #{group}, skipping")
end

defp do_execute(group, {event, measurements, metadata, reshaper}) do
defp do_execute(group, {event, measurements, metadata, reshaper, messenger}) do
{context, updates} =
metadata
|> Map.put(:telemetria_group, group)
|> Map.put(:measurements, measurements)
|> Map.pop(:context, %{})

with {impl, opts} when is_atom(impl) and is_list(opts) <- messenger do
updates
|> Map.put(:event, event)
|> Telemetria.Messenger.post(impl, opts)
end

updates = if is_function(reshaper, 1), do: reshaper.(updates), else: updates

updates =
Expand Down
Loading

0 comments on commit 6425701

Please sign in to comment.