From b581ebd8e7feca0049b11700ceeb11ea029f5482 Mon Sep 17 00:00:00 2001 From: Connor Rigby Date: Sun, 20 Oct 2019 13:36:11 -0700 Subject: [PATCH] Initial pass at telelemtry event system --- farmbot_telemetry/lib/farmbot_telemetry.ex | 91 +++++++++++++++++-- .../lib/farmbot_telemetry/application.ex | 22 ++++- .../lib/farmbot_telemetry/class.ex | 14 +++ .../farmbot_telemetry/classes/amqp_class.ex | 28 ++++++ .../farmbot_telemetry/classes/dns_class.ex | 22 +++++ .../farmbot_telemetry/classes/http_class.ex | 36 ++++++++ .../classes/network_class.ex | 18 ++++ .../lib/farmbot_telemetry/log_handler.ex | 9 ++ .../test/farmbot_telemetry_test.exs | 46 +++++++++- 9 files changed, 271 insertions(+), 15 deletions(-) create mode 100644 farmbot_telemetry/lib/farmbot_telemetry/class.ex create mode 100644 farmbot_telemetry/lib/farmbot_telemetry/classes/amqp_class.ex create mode 100644 farmbot_telemetry/lib/farmbot_telemetry/classes/dns_class.ex create mode 100644 farmbot_telemetry/lib/farmbot_telemetry/classes/http_class.ex create mode 100644 farmbot_telemetry/lib/farmbot_telemetry/classes/network_class.ex create mode 100644 farmbot_telemetry/lib/farmbot_telemetry/log_handler.ex diff --git a/farmbot_telemetry/lib/farmbot_telemetry.ex b/farmbot_telemetry/lib/farmbot_telemetry.ex index dce95f0c..9bd69cbb 100644 --- a/farmbot_telemetry/lib/farmbot_telemetry.ex +++ b/farmbot_telemetry/lib/farmbot_telemetry.ex @@ -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 diff --git a/farmbot_telemetry/lib/farmbot_telemetry/application.ex b/farmbot_telemetry/lib/farmbot_telemetry/application.ex index ce3fe81a..157b72aa 100644 --- a/farmbot_telemetry/lib/farmbot_telemetry/application.ex +++ b/farmbot_telemetry/lib/farmbot_telemetry/application.ex @@ -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 diff --git a/farmbot_telemetry/lib/farmbot_telemetry/class.ex b/farmbot_telemetry/lib/farmbot_telemetry/class.ex new file mode 100644 index 00000000..247bb756 --- /dev/null +++ b/farmbot_telemetry/lib/farmbot_telemetry/class.ex @@ -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 diff --git a/farmbot_telemetry/lib/farmbot_telemetry/classes/amqp_class.ex b/farmbot_telemetry/lib/farmbot_telemetry/classes/amqp_class.ex new file mode 100644 index 00000000..46b80f7a --- /dev/null +++ b/farmbot_telemetry/lib/farmbot_telemetry/classes/amqp_class.ex @@ -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 diff --git a/farmbot_telemetry/lib/farmbot_telemetry/classes/dns_class.ex b/farmbot_telemetry/lib/farmbot_telemetry/classes/dns_class.ex new file mode 100644 index 00000000..efb5a6cc --- /dev/null +++ b/farmbot_telemetry/lib/farmbot_telemetry/classes/dns_class.ex @@ -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 diff --git a/farmbot_telemetry/lib/farmbot_telemetry/classes/http_class.ex b/farmbot_telemetry/lib/farmbot_telemetry/classes/http_class.ex new file mode 100644 index 00000000..c25b8d81 --- /dev/null +++ b/farmbot_telemetry/lib/farmbot_telemetry/classes/http_class.ex @@ -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 diff --git a/farmbot_telemetry/lib/farmbot_telemetry/classes/network_class.ex b/farmbot_telemetry/lib/farmbot_telemetry/classes/network_class.ex new file mode 100644 index 00000000..f4b66819 --- /dev/null +++ b/farmbot_telemetry/lib/farmbot_telemetry/classes/network_class.ex @@ -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 diff --git a/farmbot_telemetry/lib/farmbot_telemetry/log_handler.ex b/farmbot_telemetry/lib/farmbot_telemetry/log_handler.ex new file mode 100644 index 00000000..e1e3c907 --- /dev/null +++ b/farmbot_telemetry/lib/farmbot_telemetry/log_handler.ex @@ -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 diff --git a/farmbot_telemetry/test/farmbot_telemetry_test.exs b/farmbot_telemetry/test/farmbot_telemetry_test.exs index 4e5a3b9a..c6afd507 100644 --- a/farmbot_telemetry/test/farmbot_telemetry_test.exs +++ b/farmbot_telemetry/test/farmbot_telemetry_test.exs @@ -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