Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for dry-monitor events #924

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changesets/add-support-for-dry-monitor-events.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
bump: "patch"
type: "add"
---

`dry-monitor` events are now supported. There's also native support for `rom-sql` instrumentation events if they're configured.
54 changes: 54 additions & 0 deletions .semaphore/semaphore.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,24 @@ blocks:
value: latest
commands:
- "./support/bundler_wrapper exec rake test"
- name: Ruby 3.0.5 for dry-monitor
env_vars:
- *2
- *3
- *4
- *5
- name: RUBY_VERSION
value: 3.0.5
- name: GEMSET
value: dry-monitor
- name: BUNDLE_GEMFILE
value: gemfiles/dry-monitor.gemfile
- name: _RUBYGEMS_VERSION
value: latest
- name: _BUNDLER_VERSION
value: latest
commands:
- "./support/bundler_wrapper exec rake test"
- name: Ruby 3.0.5 for grape
env_vars:
- *2
Expand Down Expand Up @@ -1701,6 +1719,24 @@ blocks:
value: latest
commands:
- "./support/bundler_wrapper exec rake test"
- name: Ruby 3.1.3 for dry-monitor
env_vars:
- *2
- *3
- *4
- *5
- name: RUBY_VERSION
value: 3.1.3
- name: GEMSET
value: dry-monitor
- name: BUNDLE_GEMFILE
value: gemfiles/dry-monitor.gemfile
- name: _RUBYGEMS_VERSION
value: latest
- name: _BUNDLER_VERSION
value: latest
commands:
- "./support/bundler_wrapper exec rake test"
- name: Ruby 3.1.3 for grape
env_vars:
- *2
Expand Down Expand Up @@ -2040,6 +2076,24 @@ blocks:
value: latest
commands:
- "./support/bundler_wrapper exec rake test"
- name: Ruby 3.2.1 for dry-monitor
env_vars:
- *2
- *3
- *4
- *5
- name: RUBY_VERSION
value: 3.2.1
- name: GEMSET
value: dry-monitor
- name: BUNDLE_GEMFILE
value: gemfiles/dry-monitor.gemfile
- name: _RUBYGEMS_VERSION
value: latest
- name: _BUNDLER_VERSION
value: latest
commands:
- "./support/bundler_wrapper exec rake test"
- name: Ruby 3.2.1 for grape
env_vars:
- *2
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ configurations you need to run the spec suite with a specific Gemfile.
```
BUNDLE_GEMFILE=gemfiles/capistrano2.gemfile bundle exec rspec
BUNDLE_GEMFILE=gemfiles/capistrano3.gemfile bundle exec rspec
BUNDLE_GEMFILE=gemfiles/dry-monitor.gemfile bundle exec rspec
BUNDLE_GEMFILE=gemfiles/grape.gemfile bundle exec rspec
BUNDLE_GEMFILE=gemfiles/hanami.gemfile bundle exec rspec
BUNDLE_GEMFILE=gemfiles/http5.gemfile bundle exec rspec
Expand Down
6 changes: 6 additions & 0 deletions build_matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ matrix:
- gem: "no_dependencies"
- gem: "capistrano2"
- gem: "capistrano3"
- gem: "dry-monitor"
only:
ruby:
- "3.0.5"
- "3.1.3"
- "3.2.1"
- gem: "grape"
- gem: "hanami"
only:
Expand Down
5 changes: 5 additions & 0 deletions gemfiles/dry-monitor.gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
source 'https://rubygems.org'

gem "dry-monitor", "~> 1.0.1"

gemspec :path => '../'
18 changes: 18 additions & 0 deletions lib/appsignal/event_formatter/rom/sql_formatter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

module Appsignal
class EventFormatter
module Rom
class SqlFormatter
def format(payload)
["query.#{payload[:name]}", payload[:query], SQL_BODY_FORMAT]
end
end
end
end
end

Appsignal::EventFormatter.register(
"sql.dry",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"sql.dry",
"sql.rom",

Let's name it for the gem it's formatting the events for.

We have a bunch of sql formatters that do very similar things and we can combine them one day, but this is a safer approach right now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this has to match the prefix given to look for an event formatter when the dry-monitor instrumentation receives the event -- and this instrumentation is generic to all dry-monitor events (just like the activesupport notifications one is generic to all activesupport events)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do so, there's no way of automatically formatting the events rom-sql emits because they're called just sql. The suffix is added in the dry-monitor to add a minimum granularity.

I'm going to propose a change upstream to make rom-sql emit sql.rom events instead. As ActiveRecord does.

Appsignal::EventFormatter::Rom::SqlFormatter
)
1 change: 1 addition & 0 deletions lib/appsignal/hooks.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ def self.const_missing(name)
require "appsignal/hooks/active_support_notifications"
require "appsignal/hooks/celluloid"
require "appsignal/hooks/delayed_job"
require "appsignal/hooks/dry_monitor"
require "appsignal/hooks/http"
require "appsignal/hooks/mri"
require "appsignal/hooks/net_http"
Expand Down
20 changes: 20 additions & 0 deletions lib/appsignal/hooks/dry_monitor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Appsignal
class Hooks
# @api private
class DryMonitorHook < Appsignal::Hooks::Hook
register :dry_monitor

def dependencies_present?
defined?(::Dry::Monitor::Notifications)
end

def install
require "appsignal/integrations/dry_monitor"

::Dry::Monitor::Notifications.send(:prepend, Appsignal::Integrations::DryMonitorIntegration)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know we went this route for the active support notifications integration because of performance issues, but I don't know if that's a problem here. We can instead subscribe to the wildcard * event name instead to receive events from the instrumentation and use the built-in method of listening for events.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I recall correctly (I was taking a look at this yesterday with Luismi) users can create several Dry::Monitor::Notifications instances, each being its own "notification hub" of sorts -- it's not a singleton like ActiveSupport::Notifications. This allows us to instrument all instances.

end
end
end
end
22 changes: 22 additions & 0 deletions lib/appsignal/integrations/dry_monitor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module Appsignal
module Integrations
module DryMonitorIntegration
def instrument(event_id, payload = {}, &block)
Appsignal::Transaction.current.start_event

super
ensure
title, body, body_format = Appsignal::EventFormatter.format("#{event_id}.dry", payload)

Appsignal::Transaction.current.finish_event(
title || event_id.to_s,
title,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the formatted event test, both title and name are set to "query.postgres", but in the test for the generic event one of them is left empty. Is that the intended behaviour? If not:

Suggested change
title,
title || event_id.to_s,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might also want to provide a .dry suffix for the resulting event category -- although we don't currently do this for ActiveSupport notifications.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't add dry to the event category. It's the thing that reports it, but no one cares about that detail.

body,
body_format
)
end
end
end
end
22 changes: 22 additions & 0 deletions spec/lib/appsignal/event_formatter/rom/sql_formatter_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

describe Appsignal::EventFormatter::Rom::SqlFormatter do
let(:klass) { described_class }
let(:formatter) { klass.new }

it "registers the sql event formatter" do
expect(Appsignal::EventFormatter.registered?("sql.dry", klass)).to be_truthy
end

describe "#format" do
let(:payload) do
{
:name => "postgres",
:query => "SELECT * FROM users"
}
end
subject { formatter.format(payload) }

it { is_expected.to eq ["query.postgres", "SELECT * FROM users", 1] }
end
end
104 changes: 104 additions & 0 deletions spec/lib/appsignal/hooks/dry_monitor_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# frozen_string_literal: true

if DependencyHelper.dry_monitor_present?
require "dry-monitor"

describe Appsignal::Hooks::DryMonitorHook do
describe "#dependencies_present?" do
subject { described_class.new.dependencies_present? }

context "when Dry::Monitor::Notifications constant is found" do
before { stub_const "Dry::Monitor::Notifications", Class.new }

it { is_expected.to be_truthy }
end

context "when Dry::Monitor::Notifications constant is not found" do
before { hide_const "Dry::Monitor::Notifications" }

it { is_expected.to be_falsy }
end
end
end

describe "#install" do
it "installs the dry-monitor hook" do
start_agent

expect(Dry::Monitor::Notifications.included_modules).to include(
Appsignal::Integrations::DryMonitorIntegration
)
end
end

describe "Dry Monitor Integration" do
before :context do
start_agent
end

let!(:transaction) do
Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
end

let(:notifications) { Dry::Monitor::Notifications.new(:test) }

context "when is a dry-sql event" do
let(:event_id) { :sql }
let(:payload) do
{
:name => "postgres",
:query => "SELECT * FROM users"
}
end

it "creates an sql event" do
notifications.instrument(event_id, payload)
expect(transaction.to_h["events"]).to match([
{
"allocation_count" => kind_of(Integer),
"body" => "SELECT * FROM users",
"body_format" => Appsignal::EventFormatter::SQL_BODY_FORMAT,
"child_allocation_count" => kind_of(Integer),
"child_duration" => kind_of(Float),
"child_gc_duration" => kind_of(Float),
"count" => 1,
"duration" => kind_of(Float),
"gc_duration" => kind_of(Float),
"name" => "query.postgres",
"start" => kind_of(Float),
"title" => "query.postgres"
}
])
end
end

context "when is an unregistered formatter event" do
let(:event_id) { :foo }
let(:payload) do
{
:name => "foo"
}
end

it "creates a generic event" do
notifications.instrument(event_id, payload)
expect(transaction.to_h["events"]).to match([
{
"allocation_count" => kind_of(Integer),
"body" => "",
"body_format" => Appsignal::EventFormatter::DEFAULT,
"child_allocation_count" => kind_of(Integer),
"child_duration" => kind_of(Float),
"child_gc_duration" => kind_of(Float),
"count" => 1,
"duration" => kind_of(Float),
"gc_duration" => kind_of(Float),
"name" => "foo",
"start" => kind_of(Float),
"title" => ""
}
])
end
end
end
end
6 changes: 5 additions & 1 deletion spec/support/helpers/dependency_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module DependencyHelper
module DependencyHelper # rubocop:disable Metrics/ModuleLength
module_function

def ruby_version
Expand Down Expand Up @@ -123,6 +123,10 @@ def hanami_present?
dependency_present? "hanami"
end

def dry_monitor_present?
dependency_present? "dry-monitor"
end

def hanami2_present?
ruby_3_0_or_newer? && hanami_present? && Gem.loaded_specs["hanami"].version >= Gem::Version.new("2.0")
end
Expand Down