---
title: Trace Decoration for Fun and Profit
description: Auto-instrumentation shows that something was slow. Decoration shows why. Learn to enrich OpenTelemetry spans with business context in Elixir.
---

[Gearflow Blog](https://blog.gearflow.com)

# [Trace Decoration for Fun and Profit](https://blog.gearflow.com/trace-decoration-for-fun-and-profit)

 Written by [Christian Koch](https://blog.gearflow.com/author/christian-koch) | Dec 22, 2025 4:18:24 PM

It's 11 PM. PagerDuty fires. Checkout latency is spiking.

You open a slow trace. Phoenix shows the route. Ecto shows 12 queries. You see *that* it was slow. You don't see *why*. Large order? Retry? Cold cache? The trace doesn't say.

This is the gap between infrastructure telemetry and business context. Decoration bridges it.

This is the third post in our observability series. [Part 1](https://blog.gearflow.com/why-you-need-telemetry) covered the three pillars. [Part 2](https://blog.gearflow.com/two-patterns-that-make-otel-actually-useful) introduced domain instrumentation and context propagation. This post is about enriching spans so they carry meaning, not just timing.

### Terms

**Span**: A named, timed operation. The atom of distributed tracing.

**Attributes**: Key-value metadata attached to a span. Static context: `user_id: 123`, `item_count: 47`, `tenant: "acme"`.

**Events**: Timestamped moments within a span. Dynamic markers: "cache miss", "retry attempted", "payment started".

**Status**: The outcome. OK or Error, with a description when things fail.

**Decoration**: Adding attributes, events, and status to spans. Taking "something happened" to "here's what, why, and for whom."

Trace: A collection of spans that represents a subset of the code executed for one request or action.

### The Abstraction Gap

Auto-instrumentation operates at the infrastructure layer. Phoenix knows routes. Ecto knows queries. Neither knows your domain.

Your business logic lives above: orders, shipments, invoices, tenants. The concepts that matter at 2 AM aren't HTTP verbs—they're "why did this customer's order fail?"

Decoration pushes domain context into spans so traces speak your language.

### Setup

The [open\_telemetry\_decorator](https://hex.pm/packages/open_telemetry_decorator) library wraps functions in spans with one line:

```
def deps do
  [
    {:opentelemetry, "~> 1.4"},
    {:opentelemetry_exporter, "~> 1.7"},
    {:open_telemetry_decorator, "~> 1.5"}
  ]
end
```

Configure a namespace to avoid collisions with OpenTelemetry's semantic conventions:

```
config :o11y, :attribute_namespace, "app"
```

### Decorating a Function

```
defmodule MyApp.Checkout do
  use OpenTelemetryDecorator

  @decorate with_span("checkout.submit", include: [:user_id, :item_count, :total])
  def submit(user, cart) do
    user_id = user.id
    item_count = length(cart.items)
    total = Cart.total(cart)

    O11y.add_event("inventory_check")
    :ok = Inventory.reserve(cart.items)

    O11y.add_event("payment_started")
    {:ok, charge} = Payments.charge(user, total)
    O11y.add_event("payment_complete", %{charge_id: charge.id})

    Orders.create(user, cart, charge)
  end
end
```

The `include` option captures local variables as span attributes. Events mark phases. When things fail:

```
case Payments.charge(user, total) do
  {:ok, charge} ->
    {:ok, charge}
  {:error, reason} ->
    O11y.set_error(inspect(reason))
    {:error, :payment_failed}
end
```

### What to Capture

**Identifiers**: `user_id`, `order_id`, `tenant_id`. The nouns.

**Quantities**: `item_count`, `batch_size`, `retry_count`. The scale.

**States**: `subscription_tier`, `feature_flag`, `cache_hit`. The context.

**Outcomes**: `result`, `error_type`, `fallback_used`. The answers.

Attributes describe the span as a whole. Events describe moments within it. Use attributes for "what was this request?" Use events for "where did the time go?"

### Back to 11 PM

Same alert. Same spike. Different trace.

Now you see: `app.item_count: 127`. Events show inventory check took 800ms—127 items, 127 stock lookups. Mystery solved in 10 seconds.

`GROUP BY app.item_count` confirms it: every slow checkout has 50+ items. You've found your optimization target without touching a log file.

### The Return

Decorated traces compound:

- **Search becomes useful.** Find spans where `app.tenant = "acme"` and `status = error`.
- **Aggregations become meaningful.** P99 latency for orders over $500. Retry rate by payment provider.
- **Debugging becomes fast.** Open a trace, see the order ID, customer tier, item count, and timeline. No log correlation.
- **Alerting becomes precise.** Fire when `app.retry_count > 3`, not just "errors happened."

Undecorated traces are timestamps. Decorated traces are answers.

[View full post](https://blog.gearflow.com/trace-decoration-for-fun-and-profit)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Christian Koch"
  },
  "dateModified" : "2025-12-22T16:18:24.495Z",
  "datePublished" : "2025-12-22T16:18:24Z",
  "headline" : "Trace Decoration for Fun and Profit",
  "image" : {
    "@type" : "ImageObject",
    "height" : 1024,
    "url" : "https://4339805.fs1.hubspotusercontent-na1.net/hubfs/4339805/AI-Generated%20Media/Images/improve%20tracing%20decoration%20observability-1.png",
    "width" : 1536
  },
  "mainEntityOfPage" : "https://blog.gearflow.com/trace-decoration-for-fun-and-profit",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60.0,
      "url" : "https://4339805.fs1.hubspotusercontent-na1.net/hubfs/4339805/New%20transparent%20Gearflow%20Logo%20HIGH%20RES%20-%20Compressed.png",
      "width" : 227.35536
    },
    "name" : "Gearflow Blog"
  }
}
```