DDTrace.Decorators (dd_trace_ex v0.1.0)

Copy Markdown View Source

Tracing a function by annotating its definition.

use DDTrace.Decorators brings in one decorator, trace, which wraps the decorated function in the span DDTrace.trace/2,3 would have opened around its body — a child of whatever span the process already has open, or the root of a new trace when it has none:

defmodule MyApp.Orders do
  use DDTrace.Decorators

  @decorate trace()
  def process(order), do: ...

  @decorate trace(service: "billing")
  defp charge(order), do: ...
end

The function's return value, signature, and visibility are untouched: defp stays private, guards still guard, and the decorator adds nothing a caller can see other than the span.

The span's name

A bare @decorate trace() names the span after the function it decorates — "MyApp.Orders.process/1" — and the span's resource defaults to the same, so a decorated function is findable in Datadog without naming anything. The arity is the definition's: a function with default arguments is one definition, so f(a, b \\ 1) spans as f/2 however many arguments a call passed.

name: overrides it:

@decorate trace(name: "orders.process")
def process(order), do: ...

Options

Every other option is DDTrace.start_span/2's, with the same meanings — service:, resource:, type: — and a misspelled one is a compile-time warning, as it is on the macro. Values are ordinary expressions evaluated at call time in the module's scope; a module attribute works, and the decorated function's own arguments are not in scope.

Tags are not an option, here or on the macro: they are written from inside the body, which is also where the data for them is.

@decorate trace()
def process(order) do
  DDTrace.set_tag("order.channel", order.channel)

  ...
end

Whole modules, and multiple clauses

@decorate_all trace() decorates every function defined after it, which is the whole instrumentation of a worker module:

defmodule MyApp.Worker do
  use DDTrace.Decorators
  @decorate_all trace()

  def perform(job), do: ...
  def retry(job), do: ...
end

Decorating a multi-clause function traces every clause of that name and arity, under the one name, one span per call — a function is traced as a unit rather than a clause at a time. Decorating the first clause is enough.

rescue, catch, and after

A decorated definition may not carry them:

@decorate trace()
def process(order) do
  ...
rescue                     # compile error
  e -> ...
end

The rewriting would wrap the body and each handler separately, which would open a second span for the handler and error-tag the first for an error you are handling. Put the try inside the body instead, where it means what it says:

@decorate trace()
def process(order) do
  try do
    ...
  rescue
    e -> ...
  end
end

An error that leaves a decorated function any other way — a raise you do not handle, a throw, an exit — tags the span and propagates unchanged, exactly as it does through DDTrace.trace/2,3.

When tracing is off

A decorated function runs and returns its value with the tracer disabled, or with the application not started, and produces no span. The decorator is DDTrace.trace/2,3, so it is a no-op wherever that is.