# Telemetry
PhxMediaLibrary emits [Telemetry](https://hexdocs.pm/telemetry) events for all
key operations, enabling monitoring, logging, and metrics collection in your
application. Every span follows the `:start` / `:stop` / `:exception`
convention.
## Events
All events are prefixed with `[:phx_media_library, ...]`.
### Media Addition
| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:phx_media_library, :add, :start]` | `%{system_time: integer()}` | `%{collection: atom(), source_type: atom(), model: struct()}` |
| `[:phx_media_library, :add, :stop]` | `%{duration: integer()}` | `%{collection: atom(), source_type: atom(), model: struct(), media: Media.t()}` |
| `[:phx_media_library, :add, :exception]` | `%{duration: integer()}` | `%{collection: atom(), source_type: atom(), model: struct(), kind: atom(), reason: term(), stacktrace: list()}` |
### Media Deletion
| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:phx_media_library, :delete, :start]` | `%{system_time: integer()}` | `%{media: Media.t()}` |
| `[:phx_media_library, :delete, :stop]` | `%{duration: integer()}` | `%{media: Media.t()}` |
| `[:phx_media_library, :delete, :exception]` | `%{duration: integer()}` | `%{media: Media.t(), kind: atom(), reason: term(), stacktrace: list()}` |
### Image Conversions
| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:phx_media_library, :conversion, :start]` | `%{system_time: integer()}` | `%{media: Media.t(), conversion: atom()}` |
| `[:phx_media_library, :conversion, :stop]` | `%{duration: integer()}` | `%{media: Media.t(), conversion: atom()}` |
| `[:phx_media_library, :conversion, :exception]` | `%{duration: integer()}` | `%{media: Media.t(), conversion: atom(), kind: atom(), reason: term(), stacktrace: list()}` |
### Storage Operations
| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:phx_media_library, :storage, :start]` | `%{system_time: integer()}` | `%{operation: atom(), path: String.t(), adapter: module()}` |
| `[:phx_media_library, :storage, :stop]` | `%{duration: integer()}` | `%{operation: atom(), path: String.t(), adapter: module()}` |
| `[:phx_media_library, :storage, :exception]` | `%{duration: integer()}` | `%{operation: atom(), path: String.t(), adapter: module(), kind: atom(), reason: term(), stacktrace: list()}` |
### Batch Operations
| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:phx_media_library, :batch, :start]` | `%{system_time: integer()}` | `%{operation: atom(), count: integer()}` |
| `[:phx_media_library, :batch, :stop]` | `%{duration: integer()}` | `%{operation: atom(), count: integer()}` |
| `[:phx_media_library, :batch, :exception]` | `%{duration: integer()}` | `%{operation: atom(), count: integer(), kind: atom(), reason: term(), stacktrace: list()}` |
### Remote Downloads
Emitted when media is added from a URL via `add_from_url/3`.
| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:phx_media_library, :download, :start]` | `%{system_time: integer()}` | `%{url: String.t()}` |
| `[:phx_media_library, :download, :stop]` | `%{duration: integer()}` | `%{url: String.t(), size: integer(), mime_type: String.t()}` |
| `[:phx_media_library, :download, :exception]` | `%{duration: integer()}` | `%{url: String.t(), error: term(), kind: atom(), reason: term(), stacktrace: list()}` |
### Reorder
| Event | Measurements | Metadata |
|-------|-------------|----------|
| `[:phx_media_library, :reorder]` | `%{count: integer()}` | `%{model: struct(), collection: atom()}` |
> **Note:** Duration values are in native time units. Use
> `System.convert_time_unit(duration, :native, :millisecond)` to convert.
## Attaching Handlers
Attach handlers in your application's `start/2` callback:
```elixir
# lib/my_app/application.ex
def start(_type, _args) do
:telemetry.attach_many(
"my-app-media-handler",
[
[:phx_media_library, :add, :stop],
[:phx_media_library, :add, :exception],
[:phx_media_library, :delete, :stop],
[:phx_media_library, :storage, :stop],
[:phx_media_library, :batch, :stop],
[:phx_media_library, :download, :stop],
[:phx_media_library, :download, :exception]
],
&MyApp.TelemetryHandler.handle_event/4,
nil
)
children = [
# ...
]
opts = [strategy: :one_for_one, name: MyApp.Supervisor]
Supervisor.start_link(children, opts)
end
```
## Example: Logging Handler
```elixir
defmodule MyApp.TelemetryHandler do
require Logger
def handle_event([:phx_media_library, :add, :stop], measurements, metadata, _config) do
ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.info(
"[Media] Added to #{metadata.collection} in #{ms}ms " <>
"(media_id=#{metadata.media.id})"
)
end
def handle_event([:phx_media_library, :add, :exception], measurements, metadata, _config) do
ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.error(
"[Media] Failed to add to #{metadata.collection} after #{ms}ms: " <>
"#{inspect(metadata.reason)}"
)
end
def handle_event([:phx_media_library, :delete, :stop], measurements, _metadata, _config) do
ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.info("[Media] Deleted in #{ms}ms")
end
def handle_event([:phx_media_library, :storage, :stop], measurements, metadata, _config) do
ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.debug(
"[Media] Storage #{metadata.operation} on #{metadata.path} " <>
"via #{inspect(metadata.adapter)} in #{ms}ms"
)
end
def handle_event([:phx_media_library, :batch, :stop], measurements, metadata, _config) do
ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.info("[Media] Batch #{metadata.operation} (#{metadata.count} items) in #{ms}ms")
end
def handle_event([:phx_media_library, :download, :stop], measurements, metadata, _config) do
ms = System.convert_time_unit(measurements.duration, :native, :millisecond)
Logger.info(
"[Media] Downloaded #{metadata.url} " <>
"(#{metadata.size} bytes, #{metadata.mime_type}) in #{ms}ms"
)
end
def handle_event([:phx_media_library, :download, :exception], _measurements, metadata, _config) do
Logger.error("[Media] Download failed for #{metadata.url}: #{inspect(metadata.error)}")
end
end
```
## Example: StatsD / Prometheus Metrics
If you use a metrics library like `telemetry_metrics`, you can define metrics
declaratively:
```elixir
defmodule MyAppWeb.Telemetry do
use Supervisor
import Telemetry.Metrics
def start_link(arg) do
Supervisor.start_link(__MODULE__, arg, name: __MODULE__)
end
@impl true
def init(_arg) do
children = [
{TelemetryMetricsStatsd, metrics: metrics()}
]
Supervisor.init(children, strategy: :one_for_one)
end
defp metrics do
[
# Timing
summary("phx_media_library.add.stop.duration",
unit: {:native, :millisecond},
tags: [:collection]
),
summary("phx_media_library.storage.stop.duration",
unit: {:native, :millisecond},
tags: [:operation, :adapter]
),
summary("phx_media_library.download.stop.duration",
unit: {:native, :millisecond}
),
# Counters
counter("phx_media_library.add.stop.duration",
tags: [:collection]
),
counter("phx_media_library.add.exception.duration",
tags: [:collection]
),
counter("phx_media_library.delete.stop.duration"),
counter("phx_media_library.download.stop.duration"),
counter("phx_media_library.download.exception.duration"),
# Batch sizes
summary("phx_media_library.batch.stop.duration",
unit: {:native, :millisecond},
tags: [:operation]
)
]
end
end
```
## Built-in Debug Logging
PhxMediaLibrary includes built-in debug-level Logger integration via
`PhxMediaLibrary.Telemetry`. When your application's log level is set to
`:debug`, you'll automatically see log lines for key operations without needing
to attach any custom handlers.
See the `PhxMediaLibrary.Telemetry` module documentation for full details.