Class: Wurk::Metrics::Statsd

Inherits:
Object
  • Object
show all
Includes:
Wurk::Middleware::ServerMiddleware
Defined in:
lib/wurk/metrics/statsd.rb

Overview

Pro parity (§9): emits per-job timing + counters to a statsd / dogstatsd client. The client itself is plumbed in by the host app via:

Wurk.configure_server do |config|
config.dogstatsd = -> { Datadog::Statsd.new('metrics.example.com', 8125) }
config.server_middleware { |chain| chain.add Wurk::Metrics::Statsd }
end

The dogstatsd accessor is a callable — invoked once per process, memoized — so the client is built lazily AFTER fork. Sharing a UDP socket across forks is fine, but Datadog::Statsd keeps thread-locals that must be initialized inside the child.

Per-job tuning via Statsd.options = ->(klass, job, queue) { {tags:, sample_rate:} }. Default options: tags ["worker:<klass>", "queue:<q>"], sample_rate 1.0. The dd_rate job option, when present, overrides sample_rate.

Metric naming follows Sidekiq Pro 8+: every metric prefixed sidekiq. (the prefix is hardcoded, not configurable — third-party dashboards built for Sidekiq Pro work unchanged).

Statsd.increment(metric, tags:) is the class-level fast path used by other Wurk components (Buffered client, Expiry middleware, super_fetch recovery, Batch lifecycle). No-op when no client is configured so callers never have to guard.

Spec: docs/target/sidekiq-pro.md §9.

Constant Summary collapse

METRIC_PREFIX =
'sidekiq.'
DEFAULT_SAMPLE_RATE =
1.0

Class Attribute Summary collapse

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Class Attribute Details

.optionsObject

Returns the value of attribute options.



48
49
50
# File 'lib/wurk/metrics/statsd.rb', line 48

def options
  @options
end

Instance Attribute Details

#configObject Originally defined in module Wurk::Middleware::ServerMiddleware

Returns the value of attribute config.

Class Method Details

.clientObject

Resolves the live client: invokes the configured dogstatsd proc exactly once per process and memoizes. Returns nil when no proc is configured, so callers get a clean no-op without raising.

The unconfigured answer is memoized too, which is what makes the no-client path free: Client#emit_enqueued asks once per payload, so re-reading Wurk.configuration here would cost a config lookup per job on every bulk push. Both invalidation points call reset!.



108
109
110
111
112
113
114
# File 'lib/wurk/metrics/statsd.rb', line 108

def client
  memo = @client
  return memo unless UNSET.equal?(memo)

  builder = Wurk.configuration.respond_to?(:dogstatsd) ? Wurk.configuration.dogstatsd : nil
  @client = builder.respond_to?(:call) ? builder.call : builder
end

.distribution(metric, value, tags: nil, sample_rate: DEFAULT_SAMPLE_RATE) ⇒ Object

Distribution send. Some statsd clients lack distribution (vanilla statsd-ruby, for example) — fall back to histogram so the metric still lands somewhere. dogstatsd-ruby always has distribution.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/wurk/metrics/statsd.rb', line 82

def distribution(metric, value, tags: nil, sample_rate: DEFAULT_SAMPLE_RATE)
  client = self.client
  return nil unless client

  opts = sample_rate_kw(sample_rate)
  opts[:tags] = tags if tags
  name = "#{METRIC_PREFIX}#{metric}"
  if client.respond_to?(:distribution)
    client.distribution(name, value, **opts)
  elsif client.respond_to?(:histogram)
    client.histogram(name, value, **opts)
  end
  nil
rescue StandardError => e
  handle_error(e)
  nil
end

.gauge(metric, value, tags: nil, sample_rate: DEFAULT_SAMPLE_RATE) ⇒ Object



66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/wurk/metrics/statsd.rb', line 66

def gauge(metric, value, tags: nil, sample_rate: DEFAULT_SAMPLE_RATE)
  client = self.client
  return nil unless client

  opts = sample_rate_kw(sample_rate)
  opts[:tags] = tags if tags
  client.gauge("#{METRIC_PREFIX}#{metric}", value, **opts)
  nil
rescue StandardError => e
  handle_error(e)
  nil
end

.increment(metric, tags: nil, sample_rate: DEFAULT_SAMPLE_RATE) ⇒ Object

Counter shortcut used across the codebase. Tags are forwarded as given — caller's job to namespace them ("class:Foo", "queue:bar"). No-op when no client is wired up.



53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/wurk/metrics/statsd.rb', line 53

def increment(metric, tags: nil, sample_rate: DEFAULT_SAMPLE_RATE)
  client = self.client
  return nil unless client

  opts = sample_rate_kw(sample_rate)
  opts[:tags] = tags if tags
  client.increment("#{METRIC_PREFIX}#{metric}", **opts)
  nil
rescue StandardError => e
  handle_error(e)
  nil
end

.reset!Object

Test/lifecycle hook. Reset between specs, after fork so the parent's socket doesn't bleed into children, and on every config.dogstatsd= — a memoized nil would otherwise hide a client configured later.



130
131
132
# File 'lib/wurk/metrics/statsd.rb', line 130

def reset!
  @client = UNSET
end

.safe_clientObject

client without the raise: a builder proc that blows up is reported through the error handler and treated as "no client", so misconfigured metrics can never fail the work they were measuring. Callers on a hot path use this to skip building anything the emit would drop.



120
121
122
123
124
125
# File 'lib/wurk/metrics/statsd.rb', line 120

def safe_client
  client
rescue StandardError => e
  handle_error(e)
  nil
end

Instance Method Details

#call(_worker, job, queue) ⇒ Object

rubocop:disable Metrics/AbcSize



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
# File 'lib/wurk/metrics/statsd.rb', line 145

def call(_worker, job, queue) # rubocop:disable Metrics/AbcSize
  return yield if self.class.safe_client.nil?

  klass = job['class']
  opts  = per_job_options(klass, job, queue)
  tags  = opts[:tags]
  rate  = opts.fetch(:sample_rate, DEFAULT_SAMPLE_RATE)

  emit(:increment, 'jobs.count', tags: tags, sample_rate: rate)
  started = monotonic_ms
  success = false
  begin
    yield
    success = true
  ensure
    duration = monotonic_ms - started
    # Metrics are best-effort: an emit failure mid-finalize must not
    # corrupt the job result the caller already produced.
    begin
      finalize(success, duration, tags: tags, sample_rate: rate)
    rescue StandardError => e
      self.class.send(:handle_error, e)
    end
  end
end

#loggerObject Originally defined in module Wurk::Middleware::ServerMiddleware

#redis(idempotent: false) ⇒ Object Originally defined in module Wurk::Middleware::ServerMiddleware

#redis_poolObject Originally defined in module Wurk::Middleware::ServerMiddleware