Concepts

# Observability

See locks working: fan every lock event out to your metrics and logs with reflections, add per-lock debug metadata, and browse live locks in the Web UI.

## Reflections

Fan every lock event out to StatsD, your logger, or an error tracker.

A lock is invisible by default — it acquires, holds, and releases without telling you anything. Reflections are the observability seam: the gem emits a named event at every interesting moment, and you attach whatever you want to it. Metrics, structured logs, error-tracker breadcrumbs — all of it hangs off one registration block.

Register handlers with `SidekiqUniqueJobs.reflect`. It yields an `on` object; you call one method per event and pass a block that runs when that event fires. **The block is required** — registering an event without one raises `NoBlockGiven`.

```ruby
# config/initializers/sidekiq_unique_jobs.rb
SidekiqUniqueJobs.reflect do |on|
  on.locked do |job_hash|
    StatsD.increment("uniquejobs.locked")
  end
end
```

Each handler receives the Sidekiq job hash (`job_hash["class"]`, `job_hash["args"]`, `job_hash["jid"]`, and so on). A few events pass a second argument — `execution_failed`, for instance, also hands you the exception that was raised.

There are fourteen events in all: `locked`, `lock_failed`, `unlocked`, `unlock_failed`, `timeout`, `duplicate`, `execution_failed`, `after_unlock_callback_failed`, `rescheduled`, `reschedule_failed`, `uniqueness_lapsed`, `unknown_sidekiq_worker`, `debug`, and `error`. The full list with every signature lives in the [Reflections](https://sidekiq-unique-jobs.zoolutions.llc/docs/reflections) reference.

## A worked example

Wire the four events you'll actually watch.

In practice you care about a handful of events: did the lock succeed, did it fail, did it release, and did the job blow up while holding it. Here's a complete initializer wiring those to StatsD counters and a logger.

```ruby
# config/initializers/sidekiq_unique_jobs.rb
SidekiqUniqueJobs.reflect do |on|
  on.locked do |job_hash|
    StatsD.increment("uniquejobs.locked", tags: ["worker:#{job_hash["class"]}"])
  end

  on.lock_failed do |job_hash|
    StatsD.increment("uniquejobs.lock_failed", tags: ["worker:#{job_hash["class"]}"])
    Sidekiq.logger.warn("lock failed for #{job_hash["class"]} (#{job_hash["jid"]})")
  end

  on.unlocked do |job_hash|
    StatsD.increment("uniquejobs.unlocked", tags: ["worker:#{job_hash["class"]}"])
  end

  on.execution_failed do |job_hash, exception|
    StatsD.increment("uniquejobs.execution_failed", tags: ["worker:#{job_hash["class"]}"])
    Sidekiq.logger.error("#{job_hash["class"]} raised #{exception.class}: #{exception.message}")
  end
end
```

You don't have to register every event — only the ones you register run. Everything else stays a no-op, so there's no cost to watching just four.

A `lock_failed` counter climbing steadily is your signal that real duplicates are being caught. A single spike after a deploy usually means something is enqueuing the same job in a loop.

> **Tip:** Reflection handlers run inline in the middleware. Keep them fast — increment a counter, write a log line. Push slow work (an HTTP call, a database write) onto a background thread or queue so a slow metrics sink never delays a job.

## lock_info: debug metadata

Store the why-and-when alongside each lock.

Reflections tell you what happened over time. When you need to inspect a *single* lock — why does this digest still exist, who took it, when — turn on `lock_info` for the worker.

```ruby
class ChargeCustomerJob
  include Sidekiq::Job

  sidekiq_options lock: :until_executed, lock_info: true

  def perform(customer_id)
    # ...
  end
end
```

With `lock_info: true`, the gem stores metadata about the lock in Redis — the worker, the queue, the arguments, the lock type, timeouts, and the timestamp it was taken. That metadata is what the Web UI renders when you click into a lock, and it's what makes an orphaned lock legible instead of an opaque digest.

It costs an extra write per lock, so it's off by default (`config.lock_info` is `false`). Turn it on per worker while you're debugging, or globally in your configuration block:

```ruby
SidekiqUniqueJobs.configure do |config|
  config.lock_info = true
end
```

> **Note:** lock_info is diagnostic metadata — it never changes which jobs are considered duplicates. Uniqueness is always decided by the digest (worker class, queue, and arguments), whether or not lock_info is on.

## The Web UI Locks tab

Browse, filter, and delete live locks.

For an at-a-glance view of every lock currently held, mount the Web UI extension. Requiring it adds a **Locks** tab to the standard Sidekiq Web UI.

```ruby
# config/routes.rb (or wherever you mount Sidekiq::Web)
require "sidekiq/web"
require "sidekiq_unique_jobs/web"

Rails.application.routes.draw do
  mount Sidekiq::Web, at: "/sidekiq"
end
```

The Locks tab lists every active digest. You can filter to find a specific lock, click through to see its details (richest when `lock_info` is on), and delete a lock by hand — the escape hatch for a lock that's genuinely stuck and shouldn't wait for the reaper.

Reaching for the delete button often means a process crashed without cleaning up. That's exactly what the reaper handles automatically — see [Orphaned locks and recovery](https://sidekiq-unique-jobs.zoolutions.llc/docs/orphaned-locks-and-recovery).