defmodule PryIn do for file <- Path.wildcard("lib/proto/*.proto") do @external_resource file end use Protobuf, from: Path.wildcard("lib/proto/*.proto") use Application @moduledoc """ PryIn is a performance metrics platform for your Phoenix application. This is the main entry point for the client library. """ @doc false def start(_type, _args) do import Supervisor.Spec, warn: false children = [ :hackney_pool.child_spec(:pryin_pool, timeout: 60_000, max_connections: 5), worker(PryIn.InteractionStore, []), worker(PryIn.SystemMetricsCollector, []), worker(PryIn.MetricValueStore, []), worker(PryIn.DataForwarder, []) ] opts = [strategy: :rest_for_one, name: PryIn.Supervisor] Supervisor.start_link(children, opts) end @doc """ Join a process into a running trace. Use this to add metrics from a child process to a parent process. Example: ``` def index(conn, params) do ... parent_pid = self() task = Task.async(fn -> PryIn.join_trace(parent_pid, self()) Repo.all(User) ... end) Task.await(task) ... end ``` Without calling `join_trace` here, the `Repo.all` call would not be added to the trace of the `index` action, as it happens in a different process. """ def join_trace(parent_pid, child_pid) do PryIn.InteractionStore.add_child(parent_pid, child_pid) end @doc """ Drops a running trace. Use this if you don't want a trace being forwarded to PryIn. Must be called after the trace was started. Example: ``` def index(conn, params) do PryIn.drop_trace() ... end ``` """ def drop_trace(pid \\ self()) do PryIn.InteractionStore.drop_interaction(pid) end @doc """ Add context to a running trace. Both arguments need to implement the `String.Chars` protocol, so `to_string/1` can be called with them. Example: ``` def index(conn, params) do PryIn.put_context(:user_id, conn.assigns.user.id) ... end ``` """ def put_context(key, value, pid \\ self()) do if PryIn.InteractionStore.has_pid?(pid) do PryIn.InteractionStore.put_context(pid, key, value) end end @doc """ Collects metrics about custom code. Wrap any code in to have it's runtime reported to PryIn. The `key` argument will be present in the web ui, so you can identify the measurement. Note that you need to `require PryIn` before calling the `instrument` macro. Metrics are only collected inside of tracked interactions. Example: ``` def index(conn, params) do api_result = PryIn.instrument("foo_api_call") do FooApi.call(%{user_id: conn.assigns.user.id}) end ... end ``` """ defmacro instrument(key, opts \\ [], do: code) do compile_metadata = Macro.escape(__CALLER__) sample_rate = opts[:sample_rate] quote do require Logger should_sample = PryIn.SamplingHelper.should_sample(unquote(sample_rate)) if should_sample do start = :erlang.monotonic_time() data = PryIn.CustomInstrumentation.start(unquote(key), unquote(compile_metadata)) result = unquote(code) time_diff = :erlang.monotonic_time() - start try do PryIn.CustomInstrumentation.finish(time_diff, data) catch kind, error -> Logger.error( "[PryIn] Error finishing custom instrumentation: " <> Exception.format(kind, error) ) end result else unquote(code) end end end @doc """ Add the current value for a metric. This is independent of a running trace. The first argument is a (string) label that can be chosen freely. The second argument is the actual value. Possible keys for opts are currently: - `:context`: A map of additional metadata. - `:sample_rate`: If you don't want to include this metric every time, supply a sample rate between 0 and 1. Example: ``` PryIn.track_metric("genserver state length", length(state.some_list), context: %{some_key: "some_value"}) ``` """ def track_metric(label, value, opts \\ %{}) when is_binary(label) and is_number(value) do PryIn.MetricValueStore.add_metric_value( label, value, DateTime.to_unix(DateTime.utc_now(), :milliseconds), Enum.into(opts, %{}) ) end end