Initial pass at telelemtry event system

pull/1045/head
Connor Rigby 2019-10-20 13:36:11 -07:00 committed by Connor Rigby
parent 1539f45014
commit b581ebd8e7
9 changed files with 271 additions and 15 deletions

View File

@ -1,18 +1,91 @@
defmodule FarmbotTelemetry do
@moduledoc """
Documentation for FarmbotTelemetry.
Interface for farmbot system introspection
"""
alias FarmbotTelemetry.Class
@doc """
Hello world.
@typedoc "Module that implements the FarmbotTelemetry.Class behaviour"
@type class() :: module()
## Examples
@typedoc "First argument to the handler"
@type event() :: nonempty_maybe_improper_list(class(), Class.type())
iex> FarmbotTelemetry.hello()
:world
@typedoc "Second argument to the handler"
@type data() :: %{required(:action) => Class.action(), required(:timestamp) => DateTime.t()}
"""
def hello do
:world
@type meta() :: map()
@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

View File

@ -2,14 +2,28 @@ defmodule FarmbotTelemetry.Application do
# See https://hexdocs.pm/elixir/Application.html
# for more information on OTP Applications
@moduledoc false
alias FarmbotTelemetry.LogHandler
alias FarmbotTelemetry.{
AMQPClass,
DNSClass,
HTTPClass,
NetworkClass
}
use Application
def start(_type, _args) do
children = [
# Starts a worker by calling: FarmbotTelemetry.Worker.start_link(arg)
# {FarmbotTelemetry.Worker, arg}
]
children =
for class <- [AMQPClass, DNSClass, HTTPClass, NetworkClass] do
{FarmbotTelemetry,
[
class: class,
handler_id: "#{class}-LogHandler",
handler: &LogHandler.handle_event/4,
config: [level: :info]
]}
end
# See https://hexdocs.pm/elixir/Supervisor.html
# for other strategies and supported options

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,8 +1,50 @@
defmodule FarmbotTelemetryTest do
use ExUnit.Case
doctest FarmbotTelemetry
use FarmbotTelemetry
test "greets the world" do
assert FarmbotTelemetry.hello() == :world
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: 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