Skip to content

Commit

Permalink
add simple_trace method and update readme/docs
Browse files Browse the repository at this point in the history
  • Loading branch information
marcdel committed Sep 27, 2020
1 parent 83175a6 commit bdcdaa5
Show file tree
Hide file tree
Showing 6 changed files with 205 additions and 96 deletions.
151 changes: 87 additions & 64 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,92 +7,115 @@
<!-- INCLUDE -->
A function decorator for OpenTelemetry traces.

## Installation

Add `open_telemetry_decorator` to your list of dependencies in `mix.exs`. We include the `opentelemetry_api` package, but you'll need to add `opentelemetry` yourself in order to report spans and traces.

```elixir
def deps do
[
{:open_telemetry_decorator, "~> 0.4.0"},
{:opentelemetry, "~> 0.4.0"}
]
end
```

Then follow the directions for the exporter of your choice to send traces to to zipkin, honeycomb, etc.

https://github.com/garthk/opentelemetry_honeycomb

https://github.com/opentelemetry-beam/opentelemetry_zipkin

## Usage

The span name can be any string.
Add `use OpenTelemetryDecorator` to the module, and decorate any methods you want to trace with `@decorate trace("span name")` or `@decorate simple_trace("span name")`.

defmodule MyApp.Worker do
use OpenTelemetryDecorator
The `simple_trace` decorator will automatically add your input parameters and the function result to the span attributes. If you omit the span name, one will be generated based on the module, function, and arity. Specifying a name is helpful for `handle_info` type functions where the name/arity would be ambiguous.

@decorate trace("my_app.worker.do_work")
def do_work(arg1, arg2) do
...doing work
do_more_work(arg1)
end
```elixir
defmodule MyApp.Worker do
use OpenTelemetryDecorator

@decorate simple_trace() # Generates span name "MyApp.Worker.do_work/2". or...
@decorate simple_trace("worker.do_work")
def do_work(arg1, arg2) do
...doing work
end
end
```

@decorate trace("MyApp::Worker::do_more_work")
def do_more_work(arg1) do
...doing more work
end
end
The `trace` decorator allows you to specify an `includes` option which gives you more flexibility with what you can include in the span attributes. Omitting the `includes` option with `trace` means no attributes will be added to the span.

This decorator inserts all of code to add a span to the registered tracer into your method at compile time. In the example above, the `do_work` method would become something like this:
```elixir
defmodule MyApp.Worker do
use OpenTelemetryDecorator

def do_work(arg1, arg2) do
require OpenTelemetry.Span
require OpenTelemetry.Tracer
@decorate trace("worker.do_work", include: [:arg1, :arg2])
def do_work(arg1, arg2) do
...doing work
end
end
```

parent_ctx = OpenTelemetry.Tracer.current_span_ctx()
The decorator uses a macro to insert code into your function at compile time to wrap the body in a new span and link it to the currently active span. In the example above, the `do_work` method would become something like this:

OpenTelemetry.Tracer.with_span "my_app.worker.do_work", %{parent: parent_ctx} do
...doing work
do_more_work(arg1)
end
end
```elixir
def do_work(arg1, arg2) do
require OpenTelemetry.Span
require OpenTelemetry.Tracer

We use `OpenTelemetry.Tracer.current_span_ctx()` to automatically link new spans to the current trace (if it exists and is in the same process). So the above example will link the `do_work` and `do_more_work` spans for you by default.
parent_ctx = OpenTelemetry.Tracer.current_span_ctx()

OpenTelemetry.Tracer.with_span "my_app.worker.do_work", %{parent: parent_ctx} do
OpenTelemetry.Span.set_attributes(arg1: arg1, arg2: arg2)
...doing work
end
end
```

You can provide span attributes by specifying a list of variable names as atoms.

This list can include...

Any variables (in the top level closure) available when the function exits:

defmodule MyApp.Math do
use OpenTelemetryDecorator

@decorate trace("my_app.math.add", include: [:a, :b, :sum])
def add(a, b) do
sum = a + b
{:ok, thing1}
end
end


The result of the function by including the atom `:result`:

defmodule MyApp.Math do
use OpenTelemetryDecorator

@decorate trace("my_app.math.add", include: [:result])
def add(a, b) do
sum = a + b
{:ok, thing1}
end
end


Map/struct properties using nested lists of atoms:

defmodule MyApp.Worker do
use OpenTelemetryDecorator
```elixir
defmodule MyApp.Math do
use OpenTelemetryDecorator

@decorate trace("my_app.math.add", include: [:a, :b, :sum])
def add(a, b) do
sum = a + b
{:ok, thing1}
end
end
```

@decorate trace("my_app.worker.do_work", include: [[:arg1, :count], [:arg2, :count], :total])
def do_work(arg1, arg2) do
total = arg1.count + arg2.count
{:ok, total}
end
end
The result of the function by including the atom `:result`:

## Installation
```elixir
defmodule MyApp.Math do
use OpenTelemetryDecorator

@decorate trace("my_app.math.add", include: [:result])
def add(a, b) do
sum = a + b
{:ok, thing1}
end
end
```

Add `open_telemetry_decorator` to your list of dependencies in `mix.exs` and do a `mix deps.get`:
Map/struct properties using nested lists of atoms:

```elixir
def deps do
[
{:open_telemetry_decorator, "~> 0.3.0"}
]
defmodule MyApp.Worker do
use OpenTelemetryDecorator

@decorate trace("my_app.worker.do_work", include: [[:arg1, :count], [:arg2, :count], :total])
def do_work(arg1, arg2) do
total = arg1.count + arg2.count
{:ok, total}
end
end
```

Expand Down
4 changes: 0 additions & 4 deletions lib/attributes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,6 @@ defmodule Attributes do
@moduledoc false

def get(bound_variables, reportable_attr_keys, result \\ nil) do
get_reportable_attrs(bound_variables, reportable_attr_keys, result)
end

defp get_reportable_attrs(bound_variables, reportable_attr_keys, result) do
bound_variables
|> take_attrs(reportable_attr_keys)
|> maybe_add_result(reportable_attr_keys, result)
Expand Down
58 changes: 54 additions & 4 deletions lib/open_telemetry_decorator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,18 @@ defmodule OpenTelemetryDecorator do
# compensate for anchor id differences between ExDoc and GitHub
|> (&Regex.replace(~R{\(\#\K(?=[a-z][a-z0-9-]+\))}, &1, "module-")).()

use Decorator.Define, trace: 1, trace: 2
use Decorator.Define, trace: 1, trace: 2, simple_trace: 0, simple_trace: 1

@doc """
Decorate a function to add an OpenTelemetry trace with a named span.
Decorate a function to add an OpenTelemetry trace with a named span. You can provide span attributes by specifying a list of variable names as atoms.
You can provide span attributes by specifying a list of variable names as atoms. This list can include:
This list can include:
- any variables (in the top level closure) available when the function exits,
- the result of the function by including the atom `:result`,
- map/struct properties using nested lists of atoms.
```
```elixir
defmodule MyApp.Worker do
use OpenTelemetryDecorator
Expand Down Expand Up @@ -57,4 +57,54 @@ defmodule OpenTelemetryDecorator do
target = "#{inspect(context.module)}.#{context.name}/#{context.arity} @decorate telemetry"
reraise %ArgumentError{message: "#{target} #{e.message}"}, __STACKTRACE__
end

@doc """
Decorate a function to add an OpenTelemetry trace with a named span. The input parameters and result are automatically added to the span attributes.
You can specify a span name or one will be generated based on the module name, function name, and arity.
```elixir
defmodule MyApp.Worker do
use OpenTelemetryDecorator
@decorate simple_trace()
def do_work(arg1, arg2) do
total = arg1.count + arg2.count
{:ok, total}
end
@decorate simple_trace("worker.do_more_work")
def handle_call({:do_more_work, args}, _from, state) do
{:reply, {:ok, args}, state}
end
end
```
"""
def simple_trace(body, context) do
context
|> SpanName.from_context()
|> simple_trace(body, context)
end

def simple_trace(span_name, body, context) do
quote location: :keep do
require OpenTelemetry.Span
require OpenTelemetry.Tracer

parent_ctx = OpenTelemetry.Tracer.current_span_ctx()

OpenTelemetry.Tracer.with_span unquote(span_name), %{parent: parent_ctx} do
OpenTelemetry.Span.set_attributes(Kernel.binding())

result = unquote(body)

OpenTelemetry.Span.set_attributes(result: result)

result
end
end
rescue
e in ArgumentError ->
target = "#{inspect(context.module)}.#{context.name}/#{context.arity} @decorate telemetry"
reraise %ArgumentError{message: "#{target} #{e.message}"}, __STACKTRACE__
end
end
8 changes: 8 additions & 0 deletions lib/span_name.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule SpanName do
@moduledoc false

def from_context(%{module: m, name: f, arity: a}), do: "#{trim(m)}.#{f}/#{a}"

# "Elixir module names are just atoms prefixed with 'Elixir.'"
defp trim(m), do: m |> Atom.to_string() |> String.trim_leading("Elixir.")
end
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
defmodule OpenTelemetryDecorator.MixProject do
use Mix.Project

@version "0.3.0"
@version "0.4.0"
@github_page "https://github.com/marcdel/open_telemetry_decorator"

def project do
Expand Down
78 changes: 55 additions & 23 deletions test/open_telemetry_decorator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,46 @@ defmodule OpenTelemetryDecoratorTest do
require OpenTelemetry.Tracer
require OpenTelemetry.Span

# Make span methods available
require Record

# Make span methods available
for {name, spec} <- Record.extract_all(from_lib: "opentelemetry/include/ot_span.hrl") do
Record.defrecord(name, spec)
end

setup [:telemetry_pid_reporter]

defmodule Example do
use OpenTelemetryDecorator
describe "trace" do
defmodule Example do
use OpenTelemetryDecorator

@decorate trace("Example.step", include: [:id, :result])
def step(id), do: {:ok, id}
@decorate trace("Example.step", include: [:id, :result])
def step(id), do: {:ok, id}

@decorate trace("Example.workflow", include: [:count, :result])
def workflow(count), do: Enum.map(1..count, fn id -> step(id) end)
@decorate trace("Example.workflow", include: [:count, :result])
def workflow(count), do: Enum.map(1..count, fn id -> step(id) end)

@decorate trace("Example.numbers", include: [:up_to])
def numbers(up_to), do: [1..up_to]
@decorate trace("Example.numbers", include: [:up_to])
def numbers(up_to), do: [1..up_to]

@decorate trace("Example.find", include: [:id, [:user, :name], :error, :_even, :result])
def find(id) do
_even = rem(id, 2) == 0
user = %{id: id, name: "my user"}
@decorate trace("Example.find", include: [:id, [:user, :name], :error, :_even, :result])
def find(id) do
_even = rem(id, 2) == 0
user = %{id: id, name: "my user"}

case id do
1 ->
{:ok, user}
case id do
1 ->
{:ok, user}

error ->
{:error, error}
error ->
{:error, error}
end
end
end

@decorate trace("Example.no_include")
def no_include(opts), do: {:ok, opts}
end
@decorate trace("Example.no_include")
def no_include(opts), do: {:ok, opts}
end

describe "trace" do
test "does not modify inputs or function result" do
assert Example.step(1) == {:ok, 1}
end
Expand Down Expand Up @@ -110,6 +110,38 @@ defmodule OpenTelemetryDecoratorTest do
end
end

describe "simple_trace" do
defmodule Math do
use OpenTelemetryDecorator

@decorate simple_trace()
def add(a, b), do: a + b

@decorate simple_trace("math.subtraction")
def subtract(a, b), do: a - b
end

test "automatically adds inputs, outputs, and generates span name" do
Math.add(2, 3)

assert_receive {:span,
span(
name: "OpenTelemetryDecoratorTest.Math.add/2",
attributes: [a: 2, b: 3, result: 5]
)}
end

test "span name can be specified" do
Math.subtract(3, 2)

assert_receive {:span,
span(
name: "math.subtraction",
attributes: [a: 3, b: 2, result: 1]
)}
end
end

def telemetry_pid_reporter(_) do
ExUnit.CaptureLog.capture_log(fn -> :application.stop(:opentelemetry) end)

Expand Down

0 comments on commit bdcdaa5

Please sign in to comment.