# `Telemetria`
[🔗](https://github.com/am-kantox/telemetria/blob/v0.25.0/lib/telemetria.ex#L1)

`Telemetría` is the opinionated wrapper for [`:telemetry`](https://hexdocs.pm/telemetry)
(started with `v0.19.0` it became agnostic to the actual telemetry backend and supports
`OpenTelemetry` out of the box, allowing for more custom implementations of the said backend.)
It provides handy macros to attach telemetry events to any function, private function,
anonymous functions (on per-clause basis) and just random set of expressions.

`Telemetría` exports three macros:

- `deft/2` which is wrapping `Kernel.def/2`
- `defpt/2` which is wrapping `Kernel.defp/2`
- `t/2` which is wrapping the expression passed as the first parameter
  and adds the options passed as a keyword to the second parameter to the
  context of the respective telemetry event

> ### Module attribute over macros {: .tip}
>
> Unless inevitably needed, one should prefer module attributes over explicit macros
> (see the section “Using Module Attribute” below.)
>
> Module attributes have a way richer customization abilities, including but 
> not limited to conditional wrapping, slack messaging etc. See options
> which are accepted by the module attribute below.

`Telemetría` allows compile-time telemetry events definition and provides
a compiler that is responsible for incremental builds and updates of the list of
events telemetry is aware about.

> ### Compile-time config {: .warning}
>
> `Telemetría` uses a compiler to wrap annotated functions with a telemetry calls.
> That means, that all the configuration must be placed into compile-time config files.

## Using Module Attribute

Besides the functions listed above, one might attach `Telemetría` to the function
by annotating it with `@telemetria` module attribute.

There are several options to pass to this attribute:

- **`true`** — attach the `telemetry` to the function
- **`if: boolean()`** — compile-time condition
- **`if: (result -> boolean())`** — runtime condition
- **`level: Logger,level()`** — specify a min logger level to attach telemetry
- **`group: atom()`** — the configured group to manage event throttling,
  see `:throttle` setting in `Telemetria.Options`
- **`locals: [atom()]`** — the list of names of local variables to be exported
  to the telemetry call
- **`transform: [{:args, (list() -> list())}, {:result, (any() -> any())}]`** — 
  the functions to be called on the incoming attributes and/or result to reshape them
- **`reshape: (map() -> map())`** — the function to be called on the resulting attributes
  to reshape them before sending to the actual telemetry handler; the default application-wide
  reshaper might be set in `:telemetria, :reshaper` config
- **`messenger_channels: %{optional(atom()) => {module, keyword()}`** -- more handy messenger
  management, several channels config with channels names associated with their
  implementations and properties

> ### Multi-clause functions {: .warning}
>
> When a function has multiple clauses, the `@telemetria` attribute **must** be placed
> on the bodyless head clause (the one declaring defaults), not on individual clauses.
> This single annotation will wrap **all** clauses with telemetry.
>
> ```elixir
> @telemetria level: :info, locals: [:i]
> def classify(i \ nil)       # <-- attribute goes here, on the head
> def classify(nil), do: :none
> def classify(i) when i > 0, do: :positive
> def classify(_i), do: :negative
> ```
>
> Placing `@telemetria` on individual clauses instead will annotate only those
> specific clauses. You may **not** mix head-level and clause-level annotations
> for the same function -- it will raise a compile-time error.

### Simple example

The following code would emit the telemetry event for the function `weather`,
  returning `result` in Celsius _and_ injecting `fahrenheit` value under `locals`

```elixir
defmodule Forecast do
  use Telemetria

  @telemetria level: :info, group: :weather_reports, locals: [:fahrenheit]
  def weather(city) do
    fahrenheit = ExternalService.retrieve(city)
    Converter.fahrenheit_to_celsius(fahrenheit)
  end
end
```

### Comprehensive example

The following example demonstrates all available `@telemetria` parameters
used together:

```elixir
defmodule MyApp.Payments do
  use Telemetria

  # Assuming config:
  #   config :telemetria,
  #     throttle: %{payments: {5_000, :last}},
  #     messenger_channels: %{slack: {:slack, url: "https://hooks.slack.com/..."}}

  @telemetria level: :warning,
              if: &match?({:ok, _}, &1),
              group: :payments,
              locals: [:amount_cents, :currency],
              transform: [
                args: fn args -> Keyword.drop(args, [:card_token]) end,
                result: &elem(&1, 1)
              ],
              reshape: fn meta ->
                Map.take(meta, [:result, :args, :locals, :measurements])
              end,
              messenger: :slack
  def charge(user_id, amount_cents, currency, card_token) do
    result = Gateway.charge(card_token, amount_cents, currency)
    {:ok, %{user_id: user_id, status: result}}
  end
end
```

This single annotation:

- **`level: :warning`** -- only emits when the configured purge level permits `:warning`
- **`if: &match?({:ok, _}, &1)`** -- emits only when the function returns `{:ok, _}`
- **`group: :payments`** -- throttles events in the `:payments` group (5s window, last event only)
- **`locals: [:amount_cents, :currency]`** -- captures the values of these local variables
- **`transform: [args: ..., result: ...]`** -- redacts `card_token` from args, extracts the value from the `{:ok, value}` tuple
- **`reshape: fn meta -> ... end`** -- strips the event payload down to essential fields
- **`messenger: :slack`** -- sends the event to the configured Slack channel

## Advantages

`Telemetría` takes care about managing events in the target application,
makes it a single-letter change to turn a function into a function wrapped
with telemetry call, measuring the execution time out of the box.

It also allows to easily convert expressions to be be telemetry-aware.

Besides that, `telemetry: false` flag allows to purge the calls in compile-time
resulting in zero overhead (useful for benchmark, or like.)

### Example

You need to include the compiler in `mix.exs`:

```elixir
defmodule MyApp.MixProject do
  def project do
    [
      # ...
      compilers: [:telemetria],
      # ...
    ]
  end
  # ...
end
```

## Enabling `Telemetría`

To enable `telemetría` for the project, you should add `:telemetria` compiler to the list
of `Mix.compilers/0` as shown below (`mix.exs`).

```elixir
def project do
  [
    ...
    compilers: [:telemetria],
    ...
  ]
end
```

Additional steps are described below for the different use-cases.

### Plain Macros

In the modules you want to add telemetry macros to, you should `require Telemetria` (or,
preferably, `import Telemetria` to make it available without FQN.) Once imported,
the macros are available and tracked by the compiler.

```elixir
defmodule MyMod do
  import Telemetria

  defpt pi, do: 3.14
  deft answer, do: 42 - pi()

  def inner do
    short_result = t(42 * 42)
    result =
      t do
        # long calculations
        :ok
      end
  end
end
```

### Module Attribute

Module attributes are processed by the compilation hooks. To enable `@telemetria`
module attributes, one should `use Telemetria`. Below is the example that would send
two telemetry events to the configured `Telemetria.Backend`.

```elixir
defmodule Otel do
  @moduledoc "`Telemetria` with :opentelemetry` example"

  use Telemetria

  @telemetria level: :info, group: :weather_reports, locals: [:celsius], messenger: :slack
  def f_to_c(fahrenheit) do
    celsius = do_f_to_c(fahrenheit)
    round(celsius)
  end

  @telemetria level: :info, group: :weather_reports
  defp do_f_to_c(fahrenheit), do: (fahrenheit - 32) * 5 / 9
end
```

### Typical Config

`Telemetría` requires an application-wide config to operate properly. Yes, I know
having a config in a library is discouraged by the core team. Unfortunately, for the
compiler to work properly, the static compile-time config is still required.

After all, even if running many OTP applications on the same node, one would barely
want to have a different telemetry config for them.

```elixir
import Config

config :telemetria,
  purge_level: :debug,
  level: :info,
  events: [
    [:tm, :f_to_c]
  ],
  throttle: %{some_group: {1_000, :last}}
# create a slack app and put URL here
# messenger_channels: %{slack: {:slack, url: ""}}
```

## Use in releases

`:telemetria` compiler keeps track of the events in the compiler manifest file
to support incremental builds. Also it spits out `config/.telemetria.config.json`
config for convenience. It might be used in in the release configuration as shown below.

```elixir
releases: [
  configured: [
    # ...,
    config_providers: [{Telemetria.ConfigProvider, "/etc/telemetria.json"}]
  ]
]
```

## Options

* `:otp_app` (`t:atom/0`) - OTP application this telemetry is attached to. The default value is `:telemetria`.

* `:enabled` (`t:boolean/0`) - Specifies whether telemetry should be enabled. The default value is `true`.

* `:backend` (`t:atom/0`) - The backend to be used as an actual implementation The default value is `Telemetria.Backend.Telemetry`.

* `:messenger_channels` (`t:map/0`) - The messenger channels as a map `%{name => {impl, opts}}` The default value is `%{}`.

* `:level` - Telemetria level to skip logging beyond, as in [Logger](https://hexdocs.pm/logger/Logger.html#module-application-configuration) The default value is `:debug`.

* `:purge_level` - Telemetria level to purge beyond, as in [Logger](https://hexdocs.pm/logger/Logger.html#module-application-configuration) The default value is `:debug`.

* `:throttle` - The throttling mechanism for throttling through too many events The default value is `:none`.

* `:strict` (`t:boolean/0`) - Ignore `@telemetria` tags without `if` clause The default value is `false`.

* `:smart_log` (`t:boolean/0`) - Log format to use; when true, custom json would be used The default value is `false`.

* `:applications` (`t:keyword/0`) - List the applications to enable Telemetria support for, with parameters The default value is `[]`.

* `:json_config_path` (`t:String.t/0`) - Relative path to JSON config The default value is `"config/.telemetria.config.json"`.

* `:events` - The application-specific events.

  See `t:Telemetria.event_prefix/0` and `t:Telemetria.event_name/0`.

  The default value is `[]`.

* `:handler` - Event handler for this application’s telemetry events. Arity must be 4. The default value is `{Telemetria.Handler.Default, :handle_event}`.

* `:polling` (`t:keyword/0`) - The default value is `[enabled: false, flush: 5000, poll: 5000]`.

  * `:enabled` (`t:boolean/0`) - Specifies whether polling should be enabled. The default value is `true`.

  * `:flush` (`t:non_neg_integer/0`) - Flush interval. The default value is `5000`.

  * `:poll` (`t:non_neg_integer/0`) - Poll interval. The default value is `5000`.

* `:process_info` (`t:boolean/0`) - Specifies whether each telemetry event should include process info. The default value is `false`.

# `event_measurements`

```elixir
@type event_measurements() :: map()
```

# `event_metadata`

```elixir
@type event_metadata() :: map()
```

# `event_name`

```elixir
@type event_name() :: [atom(), ...] | String.t()
```

# `event_prefix`

```elixir
@type event_prefix() :: [atom()]
```

# `event_value`

```elixir
@type event_value() :: number()
```

# `handler_config`

```elixir
@type handler_config() :: term()
```

# `defpt`
*macro* 

Declares a private function with a telemetry attached, measuring execution time

# `deft`
*macro* 

Declares a function with a telemetry attached, measuring execution time

# `t`
*macro* 

Attaches telemetry to anonymous function (per clause,) or to expression(s)

# `telemetry_prefix`

```elixir
@spec telemetry_prefix(
  Macro.Env.t(),
  {atom(), keyword(), tuple()} | nil | maybe_improper_list()
) :: [atom()]
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
