Simplify telemetry systems
parent
02f50f0bcd
commit
e58d33243a
|
@ -1,94 +1,154 @@
|
|||
defmodule FarmbotTelemetry do
|
||||
@moduledoc """
|
||||
Interface for farmbot system introspection
|
||||
Interface for farmbot system introspection and metrics
|
||||
"""
|
||||
alias FarmbotTelemetry.EventClass
|
||||
|
||||
@typedoc "Module that implements the FarmbotTelemetry.EventClass behaviour"
|
||||
@type class() :: module()
|
||||
@typedoc "Classification of telemetry event"
|
||||
@type class() :: event_class() | metric_class()
|
||||
|
||||
@typedoc "First argument to the handler"
|
||||
@type event() :: nonempty_maybe_improper_list(class(), EventClass.type())
|
||||
@typedoc "Event classes are events that have no measurable value"
|
||||
@type event_class() :: atom()
|
||||
|
||||
@typedoc "Second argument to the handler"
|
||||
@type data() :: %{
|
||||
required(:action) => EventClass.action(),
|
||||
@typedoc "Metric classes are events that have a measurable value"
|
||||
@type metric_class() :: atom()
|
||||
|
||||
@typedoc "Type within an event"
|
||||
@type event_type() :: atom()
|
||||
|
||||
@typedoc "Action withing a type"
|
||||
@type event_action() :: atom()
|
||||
|
||||
@typedoc "Value of a metric event"
|
||||
@type metric_value() :: term()
|
||||
|
||||
@typedoc """
|
||||
1st arg passed to a `handler` if the type was an event
|
||||
[:farmbot_telemetry, :event, t/event_class()]
|
||||
"""
|
||||
@type event_class_path() :: [atom()]
|
||||
|
||||
@typedoc "2nd arg passed to a `handler` if the type was an event"
|
||||
@type event_class_data() :: %{
|
||||
required(:type) => event_type(),
|
||||
required(:action) => event_action(),
|
||||
required(:timestamp) => DateTime.t()
|
||||
}
|
||||
|
||||
@type meta() :: map()
|
||||
@typedoc """
|
||||
1st arg passed to a `handler` if the type was a metric
|
||||
[:farmbot_telemetry, :metric, t/metric_class()]
|
||||
"""
|
||||
@type metric_class_path() :: [atom()]
|
||||
|
||||
@typedoc "Term that will be passed to the handler."
|
||||
@type config() :: any()
|
||||
@typedoc "2nd arg passed to a `handler` if the type was a metric"
|
||||
@type metric_class_data() :: %{
|
||||
required(:value) => metric_value(),
|
||||
required(:timestamp) => DateTime.t()
|
||||
}
|
||||
|
||||
@typedoc "Module that implements the "
|
||||
@type handler() :: (event(), data(), meta(), config() -> any())
|
||||
@typedoc "3rd arg passed to a `handler`"
|
||||
@type meta() :: %{
|
||||
required(:module) => module() | nil,
|
||||
required(:file) => Path.t() | nil,
|
||||
required(:line) => pos_integer() | nil,
|
||||
required(:function) => {atom, 0 | pos_integer()} | nil
|
||||
}
|
||||
|
||||
@typedoc "required options for FarmbotTelemetry"
|
||||
@type opt() ::
|
||||
{:class, class()}
|
||||
| {:handler, handler()}
|
||||
| {:config, config()}
|
||||
@typedoc "4th arg passed to a `handler`"
|
||||
@type config() :: term()
|
||||
|
||||
@typedoc false
|
||||
@type opts :: [opts]
|
||||
@typedoc "Function that handles telemetry data"
|
||||
@type handler() ::
|
||||
(event_class_path(), event_class_data(), meta(), config() -> any())
|
||||
| (metric_class_path(), metric_class_data(), meta(), config() -> any())
|
||||
|
||||
@doc false
|
||||
@spec child_spec(opts) :: Supervisor.child_spec()
|
||||
def child_spec(opts) do
|
||||
%{
|
||||
id: opts[:class],
|
||||
start: {__MODULE__, :attach, [opts]}
|
||||
}
|
||||
end
|
||||
@doc "Execute a telemetry event"
|
||||
defmacro event(class, type, action, meta \\ %{}) do
|
||||
meta =
|
||||
Map.merge(meta, %{
|
||||
module: __ENV__.module,
|
||||
file: __ENV__.file,
|
||||
line: __ENV__.line,
|
||||
function: __ENV__.function
|
||||
})
|
||||
|
||||
@doc false
|
||||
@spec attach(opts) :: GenServer.on_start()
|
||||
def attach(opts) do
|
||||
class = opts[:class]
|
||||
handler_id = opts[:handler_id]
|
||||
handler = opts[:handler]
|
||||
config = opts[:config]
|
||||
|
||||
for {type, _actions} <- class.matrix() do
|
||||
_ = :telemetry.attach(handler_id, [class, type], handler, config)
|
||||
end
|
||||
|
||||
:ignore
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __using__(_opts) do
|
||||
quote do
|
||||
alias FarmbotTelemetry.{
|
||||
AMQPEventClass,
|
||||
DNSEventClass,
|
||||
HTTPEventClass,
|
||||
NetworkEventClass
|
||||
},
|
||||
warn: false
|
||||
|
||||
require FarmbotTelemetry
|
||||
quote location: :keep do
|
||||
:telemetry.execute(
|
||||
[:farmbot_telemetry, :event, unquote(class)],
|
||||
%{type: unquote(type), action: unquote(action), timestamp: DateTime.utc_now()},
|
||||
unquote(Macro.escape(meta))
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Execute a telemetry event. `type` and `action` will be validated"
|
||||
def execute(class, type, action, meta \\ %{}) do
|
||||
:telemetry.execute([class, type], %{action: action, timestamp: DateTime.utc_now()}, meta)
|
||||
# quote location: :keep,
|
||||
# bind_quoted: [class: class, type: type, action: action, meta: Macro.escape(meta)] do
|
||||
# # unless {type, action} in class.matrix() do
|
||||
# # raise """
|
||||
# # #{type}.#{action} is unknown for #{class}
|
||||
# # """
|
||||
# # end
|
||||
@doc "Execute a telemetry metric"
|
||||
defmacro metric(class, value, meta \\ %{}) do
|
||||
meta =
|
||||
Map.merge(meta, %{
|
||||
module: __ENV__.module,
|
||||
file: __ENV__.file,
|
||||
line: __ENV__.line,
|
||||
function: __ENV__.function
|
||||
})
|
||||
|
||||
# _ = FarmbotTelemetry.unsafe_execute(class, type, action, meta)
|
||||
# end
|
||||
quote location: :keep do
|
||||
:telemetry.execute(
|
||||
[:farmbot_telemetry, :metric, unquote(class)],
|
||||
%{value: unquote(value), timestamp: DateTime.utc_now()},
|
||||
unquote(Macro.escape(meta))
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Attach a handler to an event"
|
||||
@spec attach(String.t(), event_class_path() | metric_class_path(), handler(), config()) :: any()
|
||||
def attach(handler_id, event, handler, config \\ []) do
|
||||
:telemetry.attach(handler_id, event, handler, config)
|
||||
end
|
||||
|
||||
@doc "Helper to attach the log handler to an event"
|
||||
@spec attach_logger(event_class_path() | metric_class_path(), config()) :: any()
|
||||
def attach_logger(event, config \\ [level: :info]) do
|
||||
attach(
|
||||
"logger.#{:erlang.phash2({node(), :erlang.now()})}",
|
||||
event,
|
||||
&FarmbotTelemetry.log_handler/4,
|
||||
config
|
||||
)
|
||||
end
|
||||
|
||||
@doc "Helper to send a message to the current processes when a matching event is dispatched"
|
||||
@spec attach_recv(event_class_path() | metric_class_path(), config()) :: any()
|
||||
def attach_recv(event, config \\ [pid: self()]) do
|
||||
attach(
|
||||
"recv.#{:erlang.phash2({node(), :erlang.now()})}",
|
||||
event,
|
||||
&Kernel.send(&4[:pid], {&1, &2, &3, &4}),
|
||||
config
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unsafe_execute(class, type, action, meta \\ %{}) do
|
||||
:telemetry.execute([class, type], %{action: action, timestamp: DateTime.utc_now()}, meta)
|
||||
def log_handler(event, data, meta, config) do
|
||||
msg =
|
||||
case event do
|
||||
[:farmbot_telemetry, :event, class] ->
|
||||
%{type: type, action: action} = data
|
||||
"#{class}.#{type}.#{action}"
|
||||
|
||||
[:farmbot_telemetry, :metric, class] ->
|
||||
%{value: value} = data
|
||||
"#{class}.#{value}=#{value}"
|
||||
end
|
||||
|
||||
Logger.bare_log(config[:level] || :debug, msg, Map.to_list(meta))
|
||||
end
|
||||
|
||||
@doc "Helper to generate a path for event names"
|
||||
@spec event_class(event_class()) :: event_class_path()
|
||||
def event_class(class), do: [:farmbot_telemetry, :event, class]
|
||||
|
||||
@doc "Helper to generate a path for metric names"
|
||||
@spec metric_class(metric_class()) :: metric_class_path()
|
||||
def metric_class(class), do: [:farmbot_telemetry, :metric, class]
|
||||
end
|
||||
|
|
|
@ -2,28 +2,11 @@ defmodule FarmbotTelemetry.Application do
|
|||
# See https://hexdocs.pm/elixir/Application.html
|
||||
# for more information on OTP Applications
|
||||
@moduledoc false
|
||||
alias FarmbotTelemetry.LogHandler
|
||||
|
||||
alias FarmbotTelemetry.{
|
||||
AMQPEventClass,
|
||||
DNSEventClass,
|
||||
HTTPEventClass,
|
||||
NetworkEventClass
|
||||
}
|
||||
|
||||
use Application
|
||||
|
||||
def start(_type, _args) do
|
||||
children =
|
||||
for class <- [AMQPEventClass, DNSEventClass, HTTPEventClass, NetworkEventClass] do
|
||||
{FarmbotTelemetry,
|
||||
[
|
||||
class: class,
|
||||
handler_id: "#{class}-LogHandler",
|
||||
handler: &LogHandler.handle_event/4,
|
||||
config: [level: :info]
|
||||
]}
|
||||
end
|
||||
children = []
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
defprotocol FarmbotTelemetry.EventClass do
|
||||
@moduledoc """
|
||||
Classificaiton of a telemetry event
|
||||
"""
|
||||
|
||||
@typedoc "Type of event in relation to the class"
|
||||
@type type() :: atom()
|
||||
|
||||
@typedoc "Action in relation to the type"
|
||||
@type action() :: atom()
|
||||
|
||||
@doc "mapping of `type` to `action`"
|
||||
@callback matrix() :: [{type(), action()}]
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
defmodule FarmbotTelemetry.AMQPEventClass do
|
||||
@moduledoc """
|
||||
Classification of events pertaining to amqp channels including:
|
||||
|
||||
* channel connections
|
||||
* channel disconnects
|
||||
* channel errors
|
||||
"""
|
||||
|
||||
@behaviour FarmbotTelemetry.EventClass
|
||||
|
||||
@impl FarmbotTelemetry.EventClass
|
||||
def matrix() do
|
||||
channel_list = [
|
||||
:auto_sync,
|
||||
:celery_script,
|
||||
:bot_state,
|
||||
:logs,
|
||||
:nerves_hub
|
||||
]
|
||||
|
||||
[
|
||||
channel_connect: channel_list,
|
||||
channel_disconnect: channel_list,
|
||||
channel_error: channel_list
|
||||
]
|
||||
end
|
||||
end
|
|
@ -1,22 +0,0 @@
|
|||
defmodule FarmbotTelemetry.DNSEventClass do
|
||||
@moduledoc """
|
||||
Classification of events pertaining to dns resolution accross the various
|
||||
networked systems in the application including:
|
||||
|
||||
* ntp
|
||||
* Farmbot http token fetching
|
||||
* Farmbot http rest interface
|
||||
* Farmbot AMQP interface
|
||||
"""
|
||||
|
||||
@behaviour FarmbotTelemetry.EventClass
|
||||
|
||||
@impl FarmbotTelemetry.EventClass
|
||||
def matrix(),
|
||||
do: [
|
||||
ntp: [:nxdomain],
|
||||
http: [:nxdomain],
|
||||
token: [:nxdomain],
|
||||
amqp: [:nxdomain]
|
||||
]
|
||||
end
|
|
@ -1,36 +0,0 @@
|
|||
defmodule FarmbotTelemetry.HTTPEventClass do
|
||||
@moduledoc """
|
||||
Classification of events pertaining to the Farmbot REST interface including:
|
||||
|
||||
* sequences
|
||||
* farm events
|
||||
* regimens
|
||||
* etc
|
||||
"""
|
||||
|
||||
@behaviour FarmbotTelemetry.EventClass
|
||||
|
||||
@impl FarmbotTelemetry.EventClass
|
||||
def matrix() do
|
||||
[
|
||||
farm_events: [:http_error, :http_timeout],
|
||||
farmware_installations: [:http_error, :http_timeout],
|
||||
regimens: [:http_error, :http_timeout],
|
||||
device: [:http_error, :http_timeout],
|
||||
diagnostic_dumps: [:http_error, :http_timeout],
|
||||
farm_events: [:http_error, :http_timeout],
|
||||
farmware_envs: [:http_error, :http_timeout],
|
||||
fbos_config: [:http_error, :http_timeout],
|
||||
firmware_config: [:http_error, :http_timeout],
|
||||
peripherals: [:http_error, :http_timeout],
|
||||
pin_bindings: [:http_error, :http_timeout],
|
||||
points: [:http_error, :http_timeout],
|
||||
public_keys: [:http_error, :http_timeout],
|
||||
sensor_readings: [:http_error, :http_timeout],
|
||||
sensors: [:http_error, :http_timeout],
|
||||
sequences: [:http_error, :http_timeout],
|
||||
sync: [:http_error, :http_timeout],
|
||||
tools: [:http_error, :http_timeout]
|
||||
]
|
||||
end
|
||||
end
|
|
@ -1,18 +0,0 @@
|
|||
defmodule FarmbotTelemetry.NetworkEventClass do
|
||||
@moduledoc """
|
||||
Classification of events pertaining to a network interface, not network
|
||||
software errors. This includes:
|
||||
|
||||
* WiFi errors
|
||||
* ip address configuration errors
|
||||
"""
|
||||
|
||||
@behaviour FarmbotTelemetry.EventClass
|
||||
|
||||
@impl FarmbotTelemetry.EventClass
|
||||
def matrix(),
|
||||
do: [
|
||||
access_point: [:disconnect, :connect, :eap_error, :assosiate_error, :assosiate_timeout],
|
||||
ip_address: [:dhcp_lease, :dhcp_renew, :dhcp_lease_fail, :dhcp_renew_fail]
|
||||
]
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
defmodule FarmbotTelemetry.LogHandler do
|
||||
@moduledoc false
|
||||
require Logger
|
||||
|
||||
def handle_event([class, type], %{action: action, timestamp: _timestamp}, meta, config) do
|
||||
message = "#{class}.#{type}.#{action}"
|
||||
Logger.bare_log(config[:level], message, Map.to_list(meta))
|
||||
end
|
||||
end
|
|
@ -1,54 +1,4 @@
|
|||
defmodule FarmbotTelemetryTest do
|
||||
use ExUnit.Case
|
||||
doctest FarmbotTelemetry
|
||||
use FarmbotTelemetry
|
||||
|
||||
defmodule TestHandler do
|
||||
def handle_event([class, type], %{action: action}, meta, config) do
|
||||
send(config[:test_pid], {class, type, action, meta, config})
|
||||
end
|
||||
end
|
||||
|
||||
describe "network" do
|
||||
setup do
|
||||
opts = [
|
||||
class: NetworkEventClass,
|
||||
handler_id: "#{inspect(self())}",
|
||||
handler: &TestHandler.handle_event/4,
|
||||
config: [test_pid: self()]
|
||||
]
|
||||
|
||||
:ignore = FarmbotTelemetry.attach(opts)
|
||||
:ok
|
||||
end
|
||||
|
||||
test "access_point.disconnect" do
|
||||
FarmbotTelemetry.execute(NetworkEventClass, :access_point, :disconnect, %{ssid: "test"})
|
||||
assert_receive {NetworkEventClass, :access_point, :disconnect, %{ssid: "test"}, _}
|
||||
end
|
||||
|
||||
test "access_point.connect" do
|
||||
FarmbotTelemetry.execute(NetworkEventClass, :access_point, :connect, %{ssid: "test"})
|
||||
assert_receive {NetworkEventClass, :access_point, :connect, %{ssid: "test"}, _}
|
||||
end
|
||||
|
||||
test "access_point.eap_error" do
|
||||
FarmbotTelemetry.execute(NetworkEventClass, :access_point, :eap_error, %{ssid: "test"})
|
||||
assert_receive {NetworkEventClass, :access_point, :eap_error, %{ssid: "test"}, _}
|
||||
end
|
||||
|
||||
test "access_point.assosiate_error" do
|
||||
FarmbotTelemetry.execute(NetworkEventClass, :access_point, :assosiate_error, %{ssid: "test"})
|
||||
|
||||
assert_receive {NetworkEventClass, :access_point, :assosiate_error, %{ssid: "test"}, _}
|
||||
end
|
||||
|
||||
test "access_point.assosiate_timeout" do
|
||||
FarmbotTelemetry.execute(NetworkEventClass, :access_point, :assosiate_timeout, %{
|
||||
ssid: "test"
|
||||
})
|
||||
|
||||
assert_receive {NetworkEventClass, :access_point, :assosiate_timeout, %{ssid: "test"}, _}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue