Initial pass at telelemtry event system
parent
1539f45014
commit
b581ebd8e7
|
@ -1,18 +1,91 @@
|
||||||
defmodule FarmbotTelemetry do
|
defmodule FarmbotTelemetry do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
Documentation for FarmbotTelemetry.
|
Interface for farmbot system introspection
|
||||||
"""
|
"""
|
||||||
|
alias FarmbotTelemetry.Class
|
||||||
|
|
||||||
@doc """
|
@typedoc "Module that implements the FarmbotTelemetry.Class behaviour"
|
||||||
Hello world.
|
@type class() :: module()
|
||||||
|
|
||||||
## Examples
|
@typedoc "First argument to the handler"
|
||||||
|
@type event() :: nonempty_maybe_improper_list(class(), Class.type())
|
||||||
|
|
||||||
iex> FarmbotTelemetry.hello()
|
@typedoc "Second argument to the handler"
|
||||||
:world
|
@type data() :: %{required(:action) => Class.action(), required(:timestamp) => DateTime.t()}
|
||||||
|
|
||||||
"""
|
@type meta() :: map()
|
||||||
def hello do
|
|
||||||
:world
|
@typedoc "Term that will be passed to the handler."
|
||||||
|
@type config() :: any()
|
||||||
|
|
||||||
|
@typedoc "Module that implements the "
|
||||||
|
@type handler() :: (event(), data(), meta(), config() -> any())
|
||||||
|
|
||||||
|
@typedoc "required options for FarmbotTelemetry"
|
||||||
|
@type opt() ::
|
||||||
|
{:class, class()}
|
||||||
|
| {:handler, handler()}
|
||||||
|
| {:config, config()}
|
||||||
|
|
||||||
|
@typedoc false
|
||||||
|
@type opts :: [opts]
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
@spec child_spec(opts) :: Supervisor.child_spec()
|
||||||
|
def child_spec(opts) do
|
||||||
|
%{
|
||||||
|
id: opts[:class],
|
||||||
|
start: {__MODULE__, :attach, [opts]}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
@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.{
|
||||||
|
AMQPClass,
|
||||||
|
DNSClass,
|
||||||
|
HTTPClass,
|
||||||
|
NetworkClass
|
||||||
|
},
|
||||||
|
warn: false
|
||||||
|
|
||||||
|
require FarmbotTelemetry
|
||||||
|
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
|
||||||
|
|
||||||
|
# _ = FarmbotTelemetry.unsafe_execute(class, type, action, meta)
|
||||||
|
# end
|
||||||
|
end
|
||||||
|
|
||||||
|
@doc false
|
||||||
|
def unsafe_execute(class, type, action, meta \\ %{}) do
|
||||||
|
:telemetry.execute([class, type], %{action: action, timestamp: DateTime.utc_now()}, meta)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,14 +2,28 @@ defmodule FarmbotTelemetry.Application do
|
||||||
# See https://hexdocs.pm/elixir/Application.html
|
# See https://hexdocs.pm/elixir/Application.html
|
||||||
# for more information on OTP Applications
|
# for more information on OTP Applications
|
||||||
@moduledoc false
|
@moduledoc false
|
||||||
|
alias FarmbotTelemetry.LogHandler
|
||||||
|
|
||||||
|
alias FarmbotTelemetry.{
|
||||||
|
AMQPClass,
|
||||||
|
DNSClass,
|
||||||
|
HTTPClass,
|
||||||
|
NetworkClass
|
||||||
|
}
|
||||||
|
|
||||||
use Application
|
use Application
|
||||||
|
|
||||||
def start(_type, _args) do
|
def start(_type, _args) do
|
||||||
children = [
|
children =
|
||||||
# Starts a worker by calling: FarmbotTelemetry.Worker.start_link(arg)
|
for class <- [AMQPClass, DNSClass, HTTPClass, NetworkClass] do
|
||||||
# {FarmbotTelemetry.Worker, arg}
|
{FarmbotTelemetry,
|
||||||
]
|
[
|
||||||
|
class: class,
|
||||||
|
handler_id: "#{class}-LogHandler",
|
||||||
|
handler: &LogHandler.handle_event/4,
|
||||||
|
config: [level: :info]
|
||||||
|
]}
|
||||||
|
end
|
||||||
|
|
||||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||||
# for other strategies and supported options
|
# for other strategies and supported options
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
defprotocol FarmbotTelemetry.Class 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
|
|
@ -0,0 +1,28 @@
|
||||||
|
defmodule FarmbotTelemetry.AMQPClass do
|
||||||
|
@moduledoc """
|
||||||
|
Classification of events pertaining to amqp channels including:
|
||||||
|
|
||||||
|
* channel connections
|
||||||
|
* channel disconnects
|
||||||
|
* channel errors
|
||||||
|
"""
|
||||||
|
|
||||||
|
@behaviour FarmbotTelemetry.Class
|
||||||
|
|
||||||
|
@impl FarmbotTelemetry.Class
|
||||||
|
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
|
|
@ -0,0 +1,22 @@
|
||||||
|
defmodule FarmbotTelemetry.DNSClass 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.Class
|
||||||
|
|
||||||
|
@impl FarmbotTelemetry.Class
|
||||||
|
def matrix(),
|
||||||
|
do: [
|
||||||
|
ntp: [:nxdomain],
|
||||||
|
http: [:nxdomain],
|
||||||
|
token: [:nxdomain],
|
||||||
|
amqp: [:nxdomain]
|
||||||
|
]
|
||||||
|
end
|
|
@ -0,0 +1,36 @@
|
||||||
|
defmodule FarmbotTelemetry.HTTPClass do
|
||||||
|
@moduledoc """
|
||||||
|
Classification of events pertaining to the Farmbot REST interface including:
|
||||||
|
|
||||||
|
* sequences
|
||||||
|
* farm events
|
||||||
|
* regimens
|
||||||
|
* etc
|
||||||
|
"""
|
||||||
|
|
||||||
|
@behaviour FarmbotTelemetry.Class
|
||||||
|
|
||||||
|
@impl FarmbotTelemetry.Class
|
||||||
|
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
|
|
@ -0,0 +1,18 @@
|
||||||
|
defmodule FarmbotTelemetry.NetworkClass do
|
||||||
|
@moduledoc """
|
||||||
|
Classification of events pertaining to a network interface, not network
|
||||||
|
software errors. This includes:
|
||||||
|
|
||||||
|
* WiFi errors
|
||||||
|
* ip address configuration errors
|
||||||
|
"""
|
||||||
|
|
||||||
|
@behaviour FarmbotTelemetry.Class
|
||||||
|
|
||||||
|
@impl FarmbotTelemetry.Class
|
||||||
|
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
|
|
@ -0,0 +1,9 @@
|
||||||
|
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,8 +1,50 @@
|
||||||
defmodule FarmbotTelemetryTest do
|
defmodule FarmbotTelemetryTest do
|
||||||
use ExUnit.Case
|
use ExUnit.Case
|
||||||
doctest FarmbotTelemetry
|
doctest FarmbotTelemetry
|
||||||
|
use FarmbotTelemetry
|
||||||
|
|
||||||
test "greets the world" do
|
defmodule TestHandler do
|
||||||
assert FarmbotTelemetry.hello() == :world
|
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: NetworkClass,
|
||||||
|
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(NetworkClass, :access_point, :disconnect, %{ssid: "test"})
|
||||||
|
assert_receive {NetworkClass, :access_point, :disconnect, %{ssid: "test"}, _}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "access_point.connect" do
|
||||||
|
FarmbotTelemetry.execute(NetworkClass, :access_point, :connect, %{ssid: "test"})
|
||||||
|
assert_receive {NetworkClass, :access_point, :connect, %{ssid: "test"}, _}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "access_point.eap_error" do
|
||||||
|
FarmbotTelemetry.execute(NetworkClass, :access_point, :eap_error, %{ssid: "test"})
|
||||||
|
assert_receive {NetworkClass, :access_point, :eap_error, %{ssid: "test"}, _}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "access_point.assosiate_error" do
|
||||||
|
FarmbotTelemetry.execute(NetworkClass, :access_point, :assosiate_error, %{ssid: "test"})
|
||||||
|
assert_receive {NetworkClass, :access_point, :assosiate_error, %{ssid: "test"}, _}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "access_point.assosiate_timeout" do
|
||||||
|
FarmbotTelemetry.execute(NetworkClass, :access_point, :assosiate_timeout, %{ssid: "test"})
|
||||||
|
assert_receive {NetworkClass, :access_point, :assosiate_timeout, %{ssid: "test"}, _}
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in New Issue