Skip to content

Commit c41fc31

Browse files
dingsdaxclaude
andcommitted
feat(yabeda): Add sentry-yabeda adapter gem
Introduces sentry-yabeda, a Yabeda adapter that forwards metrics to Sentry. Covers all four Yabeda metric types (counter, gauge, histogram, summary), a periodic collector to drive gauge collection in push-based environments, and a full spec suite including unit and integration tests. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 06b45a1 commit c41fc31

16 files changed

Lines changed: 1056 additions & 1 deletion

File tree

.gitignore

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,15 @@ Gemfile.lock
1818
node_modules
1919
.vite
2020

21+
.DS_Store
22+
23+
mise.toml
24+
2125
.devcontainer/.env
2226
vendor/gems
27+
2328
sentry-rails/Gemfile-*.lock
24-
mise.toml
29+
30+
sentry-yabeda/.DS_Store
31+
sentry-yabeda/.rspec_status
32+
sentry-yabeda/Gemfile-*.lock

sentry-yabeda/Gemfile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# frozen_string_literal: true
2+
3+
source "https://rubygems.org"
4+
git_source(:github) { |name| "https://github.com/#{name}.git" }
5+
6+
eval_gemfile "../Gemfile.dev"
7+
8+
# Specify your gem's dependencies in sentry-yabeda.gemspec
9+
gemspec
10+
11+
gem "sentry-ruby", path: "../sentry-ruby"
12+
13+
gem "timecop"

sentry-yabeda/LICENSE.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2020 Sentry
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

sentry-yabeda/README.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# sentry-yabeda
2+
3+
A [Yabeda](https://github.com/yabeda-rb/yabeda) adapter that forwards Ruby application metrics to [Sentry](https://sentry.io).
4+
5+
## Installation
6+
7+
Add this line to your application's Gemfile:
8+
9+
```ruby
10+
gem "sentry-yabeda"
11+
```
12+
13+
## Usage
14+
15+
Require `sentry-yabeda` in your application. If you're using Bundler (most cases), simply adding it to your Gemfile is enough.
16+
17+
```ruby
18+
# config/initializers/sentry.rb
19+
Sentry.init do |config|
20+
config.dsn = ENV["SENTRY_DSN"]
21+
config.enable_metrics = true
22+
end
23+
24+
# config/initializers/yabeda.rb (or wherever Yabeda is configured)
25+
require "sentry-yabeda"
26+
```
27+
28+
That's it! All Yabeda metrics will automatically flow to Sentry.
29+
30+
### Periodic Gauge Collection
31+
32+
Many Yabeda plugins (puma, gc, gvl\_metrics) measure process-level state using **gauge metrics** with `collect` blocks. These blocks are designed for Prometheus's pull model. A scrape request triggers `Yabeda.collect!`, which reads the current state and sets gauge values.
33+
34+
In a push-based system like Sentry, there's no scrape request. `sentry-yabeda` solves this with a built-in **periodic collector** that calls `Yabeda.collect!` on a background thread:
35+
36+
```ruby
37+
require "sentry-yabeda"
38+
39+
# Start the collector (default: every 15 seconds)
40+
Sentry::Yabeda.start_collector!
41+
42+
# Or with a custom interval
43+
Sentry::Yabeda.start_collector!(interval: 30)
44+
45+
# Stop the collector
46+
Sentry::Yabeda.stop_collector!
47+
```
48+
49+
Without starting the collector, only **event-driven metrics** (counters incremented on each request, histograms measured per-operation) will flow to Sentry. Gauges that depend on periodic collection (e.g. GC stats, GVL contention, and Puma thread pool utilization) require the collector.
50+
51+
** How it works **
52+
53+
Every 15s (or set interval)
54+
1. Collector calls Yabeda.collect!
55+
2. Plugin collect blocks fire (read GC.stat, fetch Puma /stats, etc.)
56+
3. gauge.set(value) calls flow through the adapter
57+
4. Sentry.metrics.gauge(name, value, attributes: tags)
58+
5. Sentry buffers and sends in the next envelope flush
59+
60+
### Metric Type Mapping
61+
62+
| Yabeda Type | Sentry Type |
63+
|-------------|-------------|
64+
| Counter | `Sentry.metrics.count` |
65+
| Gauge | `Sentry.metrics.gauge` |
66+
| Histogram | `Sentry.metrics.distribution` |
67+
| Summary | `Sentry.metrics.distribution` |
68+
69+
### Tags
70+
71+
Yabeda tags are passed directly as Sentry metric attributes, enabling filtering and grouping in the Sentry UI.
72+
73+
### Metric Naming
74+
75+
Metrics are named using the pattern `{group}.{name}` (e.g., `rails.request_duration`). Metrics without a group use just the name.
76+
77+
### Trace Integration
78+
79+
Since Sentry metrics carry trace context automatically, metrics emitted via the adapter are connected to active traces when `sentry-rails` or other Sentry integrations are active. This enables pivoting from metric spikes to relevant traces in the Sentry UI.

sentry-yabeda/Rakefile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
require "bundler/gem_tasks"
4+
require_relative "../lib/sentry/test/rake_tasks"
5+
6+
Sentry::Test::RakeTasks.define_spec_tasks(
7+
spec_pattern: "spec/sentry/**/*_spec.rb",
8+
spec_rspec_opts: "--order rand --format progress"
9+
)
10+
11+
task default: :spec

sentry-yabeda/lib/sentry-yabeda.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# frozen_string_literal: true
2+
3+
require "yabeda"
4+
require "sentry-ruby"
5+
require "sentry/integrable"
6+
require "sentry/yabeda/version"
7+
require "sentry/yabeda/adapter"
8+
require "sentry/yabeda/collector"
9+
10+
module Sentry
11+
module Yabeda
12+
extend Sentry::Integrable
13+
14+
register_integration name: "yabeda", version: Sentry::Yabeda::VERSION
15+
16+
class << self
17+
attr_accessor :collector
18+
19+
# Start periodic collection of Yabeda gauge metrics.
20+
# Call this after Sentry.init to begin pushing runtime metrics
21+
# (GC, GVL, Puma stats, etc.) to Sentry.
22+
def start_collector!(interval: Collector::DEFAULT_INTERVAL)
23+
raise ArgumentError, "call start_collector! after Sentry.init" unless Sentry.initialized?
24+
25+
@collector&.kill
26+
@collector = Collector.new(interval: interval)
27+
end
28+
29+
def stop_collector!
30+
@collector&.kill
31+
@collector = nil
32+
end
33+
end
34+
end
35+
end
36+
37+
::Yabeda.register_adapter(:sentry, Sentry::Yabeda::Adapter.new)
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# frozen_string_literal: true
2+
3+
require "yabeda/base_adapter"
4+
5+
module Sentry
6+
module Yabeda
7+
class Adapter < ::Yabeda::BaseAdapter
8+
# Sentry does not require pre-registration of metrics
9+
def register_counter!(_metric) = nil
10+
def register_gauge!(_metric) = nil
11+
def register_histogram!(_metric) = nil
12+
def register_summary!(_metric) = nil
13+
14+
def perform_counter_increment!(counter, tags, increment)
15+
return unless enabled?
16+
17+
Sentry.metrics.count(
18+
metric_name(counter),
19+
value: increment,
20+
attributes: attributes_for(tags)
21+
)
22+
end
23+
24+
def perform_gauge_set!(gauge, tags, value)
25+
return unless enabled?
26+
27+
Sentry.metrics.gauge(
28+
metric_name(gauge),
29+
value,
30+
unit: unit_for(gauge),
31+
attributes: attributes_for(tags)
32+
)
33+
end
34+
35+
def perform_histogram_measure!(histogram, tags, value)
36+
return unless enabled?
37+
38+
Sentry.metrics.distribution(
39+
metric_name(histogram),
40+
value,
41+
unit: unit_for(histogram),
42+
attributes: attributes_for(tags)
43+
)
44+
end
45+
46+
def perform_summary_observe!(summary, tags, value)
47+
return unless enabled?
48+
49+
Sentry.metrics.distribution(
50+
metric_name(summary),
51+
value,
52+
unit: unit_for(summary),
53+
attributes: attributes_for(tags)
54+
)
55+
end
56+
57+
private
58+
59+
def enabled?
60+
Sentry.initialized? && Sentry.configuration.enable_metrics
61+
end
62+
63+
def attributes_for(tags)
64+
tags.empty? ? nil : tags
65+
end
66+
67+
def metric_name(metric)
68+
[metric.group, metric.name].compact.join(".")
69+
end
70+
71+
# TODO: Normalize Yabeda unit symbols (e.g. :milliseconds) to Sentry's
72+
# canonical singular strings (e.g. "millisecond") once units are visible
73+
# in the Sentry product. See https://develop.sentry.dev/sdk/foundations/state-management/scopes/attributes/#units
74+
def unit_for(metric)
75+
metric.unit&.to_s
76+
end
77+
end
78+
end
79+
end
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# frozen_string_literal: true
2+
3+
require "sentry/threaded_periodic_worker"
4+
5+
module Sentry
6+
module Yabeda
7+
# Periodically calls Yabeda.collect! to trigger gauge collection blocks
8+
# registered by plugins like yabeda-puma-plugin, yabeda-gc, and
9+
# yabeda-gvl_metrics.
10+
#
11+
# In a pull-based system (Prometheus), the scrape request triggers
12+
# collection. In a push-based system (Sentry), we need this periodic
13+
# worker to drive the collect → gauge.set → adapter.perform_gauge_set!
14+
# pipeline.
15+
class Collector < Sentry::ThreadedPeriodicWorker
16+
DEFAULT_INTERVAL = 15 # seconds
17+
18+
def initialize(interval: DEFAULT_INTERVAL)
19+
super(Sentry.sdk_logger, interval)
20+
ensure_thread
21+
end
22+
23+
def run
24+
::Yabeda.collect!
25+
rescue => e
26+
log_warn("[Sentry::Yabeda::Collector] collection failed: #{e.message}")
27+
end
28+
end
29+
end
30+
end
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# frozen_string_literal: true
2+
3+
module Sentry
4+
module Yabeda
5+
VERSION = "6.5.0"
6+
end
7+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
require_relative "lib/sentry/yabeda/version"
4+
5+
Gem::Specification.new do |spec|
6+
spec.name = "sentry-yabeda"
7+
spec.version = Sentry::Yabeda::VERSION
8+
spec.authors = ["Sentry Team"]
9+
spec.description = spec.summary = "A gem that provides Yabeda integration for the Sentry error logger"
10+
spec.email = "accounts@sentry.io"
11+
spec.license = 'MIT'
12+
13+
spec.platform = Gem::Platform::RUBY
14+
spec.required_ruby_version = '>= 2.7'
15+
spec.extra_rdoc_files = ["README.md", "LICENSE.txt"]
16+
spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n")
17+
18+
github_root_uri = 'https://github.com/getsentry/sentry-ruby'
19+
spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}"
20+
21+
spec.metadata = {
22+
"homepage_uri" => spec.homepage,
23+
"source_code_uri" => spec.homepage,
24+
"changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md",
25+
"bug_tracker_uri" => "#{github_root_uri}/issues",
26+
"documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}"
27+
}
28+
29+
spec.bindir = "exe"
30+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
31+
spec.require_paths = ["lib"]
32+
33+
spec.add_dependency "sentry-ruby", "~> 6.5"
34+
spec.add_dependency "yabeda", ">= 0.11"
35+
end

0 commit comments

Comments
 (0)