TeleDec

Zero-dependency Elixir library for declarative telemetry instrumentation using compile-time decorators.

TeleDec provides a clean, efficient way to add telemetry events to your functions with minimal boilerplate. Simply add @telemetry attributes to your functions, and the library handles all the instrumentation at compile-time with negligible runtime overhead.

Features

Installation

Add tele_dec to your list of dependencies in mix.exs:

def deps do
  [
    {:tele_dec, "~> 0.1.0"}
  ]
end

Quick Start

Basic Usage with Auto-Inferred Names

Option 1: Per-module app (recommended for umbrella projects)

defmodule MyApp.UserService do
  use TeleDec, app: :my_app  # App specified here

  @telemetry_service :user_service

  @telemetry true
  def create_user(attrs) do
    # Your implementation
    # Emits: [:my_app, :user_service, :create_user, :start/:stop/:exception]
  end

  @telemetry include: [:user_count]
  def list_users() do
    users = fetch_users()
    user_count = length(users)  # This will be included in telemetry metadata
    users
  end
end

Option 2: Global config (simpler for single-app projects)

# config/config.exs
config :tele_dec, app: :my_app

# lib/my_app/user_service.ex
defmodule MyApp.UserService do
  use TeleDec  # Uses config for app name

  @telemetry_service :user_service

  @telemetry true
  def create_user(attrs) do
    # Emits: [:my_app, :user_service, :create_user, :start/:stop/:exception]
  end
end

Explicit Event Names

defmodule MyApp.Service do
  use TeleDec

  @telemetry {[:my_app, :service, :process], []}
  def process(data) do
    # Implementation
    # Emits: [:my_app, :service, :process, :start/:stop/:exception]
  end
end

Configuration

Global Configuration

Set in config/config.exs:

config :tele_dec,
  app: :my_app,           # Application name prefix for auto-inferred events
  enabled: true            # Global enable/disable flag (default: true)

Module Configuration

defmodule MyApp.Service do
  use TeleDec

  # Set service name for auto-inferred event names
  @telemetry_service :service_name

  # Functions...
end

Per-Module App Configuration

For umbrella projects or multi-app monorepos, you can specify the app name per-module, overriding the global config:

# In app1/lib/my_app/user_service.ex
defmodule MyApp.UserService do
  use TeleDec, app: :my_app  # Explicit app for this module

  @telemetry_service :users

  @telemetry true
  def create_user(attrs) do
    # Emits: [:my_app, :users, :create_user, :start/:stop/:exception]
  end
end

# In app2/lib/other_app/order_service.ex
defmodule OtherApp.OrderService do
  use TeleDec, app: :other_app  # Different app!

  @telemetry_service :orders

  @telemetry true
  def process_order(id) do
    # Emits: [:other_app, :orders, :process_order, :start/:stop/:exception]
  end
end

App Name Priority:

  1. Explicit module option (use TeleDec, app: :my_app) - highest priority
  2. Global config (config :tele_dec, app: :my_app) - fallback
  3. No app prefix if neither is set

Function Options

# Auto-infer event name
@telemetry true

# Auto-infer with options
@telemetry include: [:computed_value]

# Explicit event name
@telemetry {[:custom, :event], [include: [:var1]]}

# Available options:
# - mode: :span (default) or :one_shot
# - include: list of variables to include in stop event metadata
# - args: list of specific arguments to capture (default: all)
# - metadata: set to false to skip all metadata capture
# - enabled: set to false to disable at compile-time

Performance Modes

Span Mode (default)

Full telemetry span with start, stop, and exception events:

@telemetry true
def process(data) do
  # ~700ns overhead
end

One-Shot Mode

Single event on completion, approximately 2x faster:

@telemetry mode: :one_shot
def fast_operation(x, y) do
  # ~300ns overhead
  # No start event, no exception tracking
end

Metadata-Free Mode

Maximum performance, no metadata capture:

@telemetry metadata: false
def high_frequency_operation() do
  # Minimal overhead
end

Performance Characteristics

TeleDec is designed for minimal overhead:

All metadata capture happens at compile-time via direct map construction, avoiding runtime reflection and Kernel.binding() calls.

Event Structure

Span Mode Events

One-Shot Mode Events

Attaching Handlers

Use standard :telemetry functions to attach handlers:

:telemetry.attach_many(
  "my-handler",
  [
    [:my_app, :user_service, :create_user, :start],
    [:my_app, :user_service, :create_user, :stop],
    [:my_app, :user_service, :create_user, :exception]
  ],
  fn event, measurements, metadata, config ->
    # Handle event
  end,
  %{some: :config}
)

Documentation

Full documentation is available on HexDocs.

License

MIT License - see LICENSE for details.

Contributing

Issues and pull requests are welcome on GitHub.