Packages
instrumentation
0.1.0
Simple instrumentation framework inspired by ActiveSupport::Notifications.
Current section
Files
Jump to
Current section
Files
lib/instrumentation.ex
defmodule Instrumentation do
@moduledoc ~S"""
Simple instrumentation framework inspired by ActiveSupport::Notifications.
It's basically syntactic sugar for a pubsub system based on Elixir's `Registry` module.
```
import Instrumentation
subscribe "foo", fn payload ->
IO.puts "foo took #{payload[:duration]} ms and said #{payload[:said]}!"
end
result = instrument "foo", fn ->
{some_function(), [said: "hiiii"]}
end
```
"""
@type namespace :: binary | atom
@type tag :: binary | atom | nil
@type target :: tag | Regex.t
@type payload :: map | Keyword.t
@type callback :: (payload -> term) | ((tag, payload) -> term)
@doc """
Instrument some code.
The return value of `f` must be a tuple `{result, payload}` where `result` _will
be the return value of this function_.
`payload` is of type `t:payload/0` and will be merged into the payload
that is delivered to the subscribers.
Subscribers use `namespace` and `tag` to match on and receive notifications.
```
# Without a tag
users = instrument "foo", fn ->
{Repo.all(User), some_metadata: "blah"}
end
# With a tag
value = instrument, "memcache", "get", fn ->
{Memcache.get("foo"), key: "foo"}
end
```
"""
@spec instrument(namespace, tag, f :: (() -> {term, payload})) :: term
def instrument(namespace, tag \\ nil, f) do
tag = normalize_tag(tag)
start_time = :os.system_time(:millisecond)
{value, payload} = try do
f.()
rescue
error ->
duration = :os.system_time(:millisecond) - start_time
[error: error, duration: duration]
|> Map.new
|> notify(namespace, tag)
reraise(error, System.stacktrace)
end
duration = :os.system_time(:millisecond) - start_time
payload
|> Map.new
|> Map.put_new(:duration, duration)
|> notify(namespace, tag)
value
end
@doc """
Simply time some code.
Use this function if you don't have any custom payload that you want to send
to subscribers; you just want to do simple timings.
Since there is no payload, the return value of `f` will be the return value
of this function.
```
"foobar" = time "foo", fn ->
"foobar"
end
value = time "memcache", "get", fn ->
Memcache.get(key)
end
""
```
"""
@spec time(namespace, tag, (() -> term)) :: term
def time(namespace, tag \\ nil, f) do
instrument(namespace, tag, fn -> {f.(), []} end)
end
@doc ~S"""
Subscribe to notifications generated by `instrument/3` and `time/3`.
If `tag` is `nil`, then the subscription will match _all_ notifications for
the given `namespace`.
If `tag` is a `t:Regex.t/0` and a notification's tag is a string, then the regex
will be run against the string to determine a match.
`f` can have arity of 1 or 2. If arity is 1, then `payload` is given to the
function. If arity is 2, then `tag, payload` is given to the function.
`payload` is a map so you can pattern match on it.
The `payload` will always have `:duration` in it.
If instrumentation fails, then `payload[:error]` will be an exception.
```
# Subscribe to all notifications on namespace "foo", regardless of tag.
subscribe "foo", fn tag, payload ->
Logger.debug("foo.#{tag} got payload #{inspect(payload)}")
end
# Subscribe to only certain tags.
subscribe "memcache", "get", fn payload ->
{tag, payload} = Map.pop(payload, :tag)
Logger.debug("memcache.#{tag} got payload #{inspect(payload)}")
end
# Subscribe to only certain tags, but don't put the tag in the payload.
subscribe "memcache", "get", fn tag, payload ->
Logger.debug("memcache.#{tag} got payload #{inspect(payload)}")
end
# Subscribe using a regex (this requires the notification emit a string tag).
subscribe "memcache", ~r/get/, fn tag, payload ->
Logger.debug("memcache.#{tag} got payload #{inspect(payload)}")
end
```
"""
@spec subscribe(namespace, target, callback) :: term
def subscribe(namespace, tag \\ nil, f) do
tag = normalize_tag(tag)
Registry.register(Instrumentation.Registry, namespace, {tag, f})
end
defp notify(payload, namespace, tag) do
Registry.dispatch(Instrumentation.Registry, namespace, fn entries ->
Enum.each(entries, ¬ify_payload(&1, payload, tag))
end)
end
defp notify_payload({_pid, {target, callback}}, payload, tag) do
if matches?(target, tag) do
case :erlang.fun_info(callback)[:arity] do
1 -> callback.(Map.put(payload, :tag, tag))
2 -> callback.(tag, payload)
_ -> raise(ArgumentError, "bad arity for subscribe function")
end
end
end
defp matches?(nil, _), do: true
defp matches?(target, tag) when is_binary(target), do: target == tag
defp matches?(target, tag) when is_atom(target), do: target == tag
defp matches?(target, tag) when is_binary(tag), do: tag =~ target
defp matches?(_, _), do: false
defp normalize_tag(nil), do: nil
defp normalize_tag(tag) when is_atom(tag), do: tag
defp normalize_tag(tag) when is_binary(tag), do: tag
defp normalize_tag(tag) do
if Regex.regex?(tag) do
tag
else
raise(ArgumentError, "invalid tag: #{inspect(tag)}")
end
end
end