Skip to content

Commit

Permalink
NEW: decorators - something works!
Browse files Browse the repository at this point in the history
  • Loading branch information
deadtrickster committed Sep 4, 2017
1 parent f6af2a6 commit a0cf61a
Show file tree
Hide file tree
Showing 8 changed files with 419 additions and 1 deletion.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ erl_crash.dump
*.ez

.\#*
.rebar3
83 changes: 83 additions & 0 deletions lib/prometheus/injector.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
defmodule Prometheus.Injector do

def inject(callback, env, ast) do
ast
|> Macro.prewalk(fn (thing) ->
case thing do
{:def, _, _} = defun -> defun
{:in, _, _} = arrow -> arrow #otherwise e in RuntimeError will be rewritten
_ -> Macro.expand(thing, env)
end
end)
|> inject_(callback)
end

# lambda
def inject_({:fn, fn_meta, [{:->, arrow_meta, [args, do_block]}]}, callback) do
{:fn, fn_meta, [{:->, arrow_meta, [args, callback.(do_block)]}]}
end

# do_blocks can be simple calls or defs
def inject_([{:do, {:__block__, [], do_blocks}}], callback) do
do_blocks = List.flatten(do_blocks)
if have_defs(do_blocks) do
Enum.map(do_blocks, &inject_to_def(&1, callback))
else
[{:do, callback.({:__block__, [], do_blocks})}]
end
end

# just do
def inject_([{:do, do_block}], callback) do
inject_([{:do, {:__block__, [], [do_block]}}], callback)
end

# implicit try
def inject_([{:do, _do_block} | rest] = all, callback) do
if is_try_unwrapped(rest) do
callback.(
quote do
try unquote(all)
end)
else
raise "Unexpected do block #{inspect rest}"
end
end

# single dos, or other non-block stuff like function calls
def inject_(thing, callback) do
inject_([{:do, {:__block__, [], [thing]}}], callback)
end

defp is_try_unwrapped(block) do
Keyword.has_key?(block, :catch)
|| Keyword.has_key?(block, :rescue)
|| Keyword.has_key?(block, :after)
|| Keyword.has_key?(block, :else)
end

defp have_defs(blocks) do
defs_count = Enum.count(blocks, fn
({:def, _, _}) -> true
(_) -> false
end)

blocks_count = Enum.count(blocks)

case defs_count do
0 -> false
^blocks_count -> true
_ -> raise "Mixing defs and other blocks isn't allowed"
end
end

defp inject_to_def({:def, def_meta, [head, [do: body]]}, callback) do
{:def, def_meta, [head, [do: callback.(body)]]}
end
defp inject_to_def({:def, def_meta, [head, [{:do, _do_block} | rest] = all]}, callback) do
{:def, def_meta, [head, [do: callback.(
quote do
try unquote(all)
end)]]}
end
end
1 change: 1 addition & 0 deletions lib/prometheus/metric.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ defmodule Prometheus.Metric do
quote do
alias Prometheus.Metric.{Counter,Gauge,Histogram,Summary,Boolean}
require Prometheus.Metric.{Counter,Gauge,Histogram,Summary,Boolean}
require Prometheus.Error

unquote_splicing(
for metric <- @metrics do
Expand Down
74 changes: 74 additions & 0 deletions lib/prometheus/metric/counter.ex
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,80 @@ defmodule Prometheus.Metric.Counter do
Erlang.metric_call(:inc, spec, [value])
end

@doc """
Increments the counter identified by `spec` by 1 when `body` executed.
Read more about bodies: `Prometheus.Injector`.
Raises `Prometheus.InvalidValueError` exception if `value` isn't a positive integer.<br>
Raises `Prometheus.UnknownMetricError` exception if a counter
for `spec` can't be found.<br>
Raises `Prometheus.InvalidMetricArityError` exception if labels count mismatch.
"""
defmacro count(spec, body) do
env = __CALLER__
Prometheus.Injector.inject(fn(block) ->
quote do
Prometheus.Metric.Counter.inc(unquote(spec), 1)
unquote(block)
end
end, env, body)
end

@doc """
Increments the counter identified by `spec` by 1 when `body` raises `exception`.
Read more about bodies: `Prometheus.Injector`.
Raises `Prometheus.InvalidValueError` exception if `value` isn't a positive integer.<br>
Raises `Prometheus.UnknownMetricError` exception if a counter
for `spec` can't be found.<br>
Raises `Prometheus.InvalidMetricArityError` exception if labels count mismatch.
"""
defmacro count_exceptions(spec, exception \\ :_, body) do
env = __CALLER__
Prometheus.Injector.inject(fn(block) ->
quote do
require Prometheus.Error
Prometheus.Error.with_prometheus_error(
try do
unquote(block)
rescue
e in unquote(exception) ->
stacktrace = System.stacktrace()
{registry, name, labels} = Prometheus.Metric.parse_spec(unquote(spec))
:prometheus_counter.inc(registry, name, labels, 1)
reraise(e, stacktrace)
end)
end
end, env, body)
end

@doc """
Increments the counter identified by `spec` by 1 when `body` raises no exceptions.
Read more about bodies: `Prometheus.Injector`.
Raises `Prometheus.InvalidValueError` exception if `value` isn't a positive integer.<br>
Raises `Prometheus.UnknownMetricError` exception if a counter
for `spec` can't be found.<br>
Raises `Prometheus.InvalidMetricArityError` exception if labels count mismatch.
"""
defmacro count_no_exceptions(spec, body) do
env = __CALLER__
Prometheus.Injector.inject(fn(block) ->
quote do
try do
unquote(block)
else
value ->
Prometheus.Metric.Counter.inc(unquote(spec), 1)
value
end
end
end, env, body)
end

@doc """
Increments the counter identified by `spec` by `value`.
If `value` happened to be a float number even one time(!) you
Expand Down
6 changes: 6 additions & 0 deletions lib/prometheus/metric/gauge.ex
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,12 @@ defmodule Prometheus.Metric.Gauge do
Erlang.metric_call(spec, [Erlang.ensure_fn(fun)])
end

defmacro track_inprogress(spec) do
quote do
@instrument {unquote(__MODULE__), :track_inprogress, unquote(spec)}
end
end

@doc """
Tracks the amount of time spent executing `fun`.
Expand Down
96 changes: 96 additions & 0 deletions test/metric/counter_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -213,4 +213,100 @@ defmodule Prometheus.CounterTest do
assert 0 == Counter.value(spec)
end

defmodule CounterInjectorsTest do

use Prometheus.Metric

@counter name: :calls_total, help: ""
@counter name: :sometimes_total, help: ""
@counter name: :exceptions_total, help: ""
@counter name: :no_exceptions_total, help: ""

Counter.count name: :calls_total do
def decorated_fun() do
IO.puts("I'm decorated fun")
end

def decorated_fun1() do
IO.puts("I'm decorated fun1")
IO.puts("I'm decorated fun1")
end
end

def sometimes_count(arg) do
if arg do
Counter.count [name: :sometimes_total], do: IO.puts "Called indeed!"
else
IO.puts("Not this time")
end
end

Counter.count_no_exceptions [name: :no_exceptions_total] do
Counter.count_exceptions [name: :exceptions_total], ArithmeticError do
def sometimes_raise(arg) do
5/arg
end
end

def sometimes_raise1(arg) when is_list(arg) do
5/arg
end
end

def qwe () do
Counter.count_no_exceptions [name: :no_exceptions_total], fn () ->
IO.puts 1
IO.puts 2
end
end

Counter.count_exceptions [name: :exceptions_total] do
def sometimes_raise_any(arg) do
5/arg
end
end
end

test "decorators test" do

CounterInjectorsTest.__declare_prometheus_metrics__()

assert 0 == Counter.value name: :calls_total
assert capture_io(fn -> CounterInjectorsTest.decorated_fun() end) ==
"I'm decorated fun\n"
assert 1 == Counter.value name: :calls_total
assert capture_io(fn -> CounterInjectorsTest.decorated_fun1() end) ==
"I'm decorated fun1\nI'm decorated fun1\n"
assert 2 == Counter.value name: :calls_total

assert 0 == Counter.value name: :sometimes_total
assert capture_io(fn -> CounterInjectorsTest.sometimes_count(true) end) ==
"Called indeed!\n"
assert capture_io(fn -> CounterInjectorsTest.sometimes_count(false) end) ==
"Not this time\n"
assert 1 == Counter.value name: :sometimes_total

assert 0 == Counter.value name: :exceptions_total
assert 0 == Counter.value name: :no_exceptions_total
assert 1 == CounterInjectorsTest.sometimes_raise(5)
assert 0 == Counter.value name: :exceptions_total
assert 1 == Counter.value name: :no_exceptions_total
assert_raise ArithmeticError,
fn ->
CounterInjectorsTest.sometimes_raise(0)
end
assert 1 == Counter.value name: :exceptions_total
assert 1 == Counter.value name: :no_exceptions_total

assert 1 == Counter.value name: :exceptions_total
assert 1 == CounterInjectorsTest.sometimes_raise(5)
assert 1 == Counter.value name: :exceptions_total
assert_raise ArithmeticError,
fn ->
CounterInjectorsTest.sometimes_raise(0)
end
assert 2 == Counter.value name: :exceptions_total

end

end
Loading

0 comments on commit a0cf61a

Please sign in to comment.