plain_apm 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ef383306da20d00a0bc5c8b2072b1dfbb3d49eb7c5d1f0710cee6d6749b726d2
4
+ data.tar.gz: b5c174eb5869f5909695af612182f2e9e6fe960230ce4ecd2d94b4701c5856fc
5
+ SHA512:
6
+ metadata.gz: 86dc64bd65c57fc77f4713f10bf08beeff9f32c7f9e80e3fc454b4462ff14a4cfc0613bd24879d2ec08e2692e9e35eb3ccad49b015cbfc3e331e9bcbdf4e45b9
7
+ data.tar.gz: 6f79c406f73217c44c06a46359722fb1ecb8d8c72e1c6de8ed5846992cab12ba56dc4c36fac18919896ab1f002a98129301d28cefde8502ccd32d7f6319f82c6
data/CHANGELOG.md ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in plainapm.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 PlainAPM
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.
data/README.md ADDED
@@ -0,0 +1,37 @@
1
+ # PlainApm for Ruby
2
+
3
+ [PlainAPM][plainapm] monitors your Rails application and helps you
4
+ understand changes in its performance.
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'plain_apm'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle install
17
+
18
+ Or install it yourself as:
19
+
20
+ $ gem install plain_apm
21
+
22
+ ## Usage
23
+
24
+ To be able to use the gem, collect, and view your app's performance
25
+ metrics, request an APP key on [plainapm web site][plainapm] and follow
26
+ the installation instructions.
27
+
28
+ ## Contributing
29
+
30
+ Bug reports and pull requests are welcome on [GitHub][github-issues].
31
+
32
+ Alternatively, feel free to send questions, suggestions, and feedback
33
+ to [PlainAPM support][support-email].
34
+
35
+ [plainapm]: https://plainapm.com
36
+ [support-email]: mailto:support@plainapm.com
37
+ [github-issues]: https://github.com/plainapm/plainapm-ruby/issues
data/Rakefile ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "appraisal"
5
+ require "standard/rake"
6
+ require "rake/testtask"
7
+
8
+ Rake::TestTask.new(:test) do |t|
9
+ t.libs << "test"
10
+ t.libs << "lib"
11
+ t.test_files = FileList["test/**/*_test.rb"]
12
+ end
13
+
14
+ namespace :test do
15
+ task all: "appraisal:all"
16
+ end
17
+
18
+ task default: :test
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "singleton"
4
+ require "json"
5
+
6
+ module PlainApm
7
+ class Agent
8
+ include Singleton
9
+
10
+ def self.collect(event)
11
+ instance.collect(event)
12
+ end
13
+
14
+ def self.start
15
+ instance.start
16
+ end
17
+
18
+ def collect(event)
19
+ return unless @config.enabled
20
+
21
+ @events << event.merge(
22
+ "collected_at" => Time.now.utc.to_f,
23
+ "pid" => Process.pid,
24
+ "thread_id" => Thread.current.object_id.to_s
25
+ )
26
+ end
27
+
28
+ def start
29
+ return unless @publisher.nil?
30
+ return unless @config.enabled
31
+
32
+ # TODO: sized queue w/ a timeout.
33
+ @events = Thread::Queue.new
34
+
35
+ # TODO: Multiple threads
36
+ @publisher = Thread.new { publisher_loop }
37
+
38
+ install_hooks
39
+
40
+ # TODO: add a cleaner shutdown.
41
+ at_exit { shutdown }
42
+ end
43
+
44
+ private
45
+
46
+ def initialize
47
+ # TODO: validate config
48
+ @config = Config.new
49
+
50
+ super
51
+ end
52
+
53
+ def install_hooks
54
+ [
55
+ Hooks::Deploy,
56
+ Hooks::ActionMailer,
57
+ Hooks::ActionPack,
58
+ Hooks::ActionView,
59
+ Hooks::ActiveJob,
60
+ Hooks::ActiveRecord,
61
+ Hooks::ErrorReporter
62
+ ].map(&:new).each(&:install)
63
+ end
64
+
65
+ def shutdown
66
+ return if @publisher.nil?
67
+
68
+ @events << nil
69
+ @publisher.join
70
+ end
71
+
72
+ ##
73
+ # Run a background thread that pops events from the queue and posts them to
74
+ # the target server.
75
+ def publisher_loop
76
+ # Have the thread keep it's own connection.
77
+ transport = Transport.new(
78
+ endpoint: @config.endpoint,
79
+ app_key: @config.app_key
80
+ )
81
+
82
+ loop do
83
+ event = @events.pop
84
+
85
+ break if event.nil?
86
+
87
+ # TODO: event serialization
88
+ _response, _error, _retriable = transport.deliver(JSON.generate(event))
89
+
90
+ # TODO: retries / drops
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ class Backoff
5
+ ##
6
+ # Exponential backoff with jitter.
7
+
8
+ DEFAULT_BASE_SECONDS = 1.5
9
+ DEFAULT_MAX_RETRIES = 10 # sum_0^10 1.5 ** k ~ 170s
10
+ DEFAULT_JITTER_MULTIPLIER = 0.2 # % of the current retry interval
11
+
12
+ ##
13
+ # @param base_seconds [Integer] base of the exponential retry.
14
+ # @param max_retries [Integer] maximum retries to perform.
15
+ # @param jitter_multiplier [Float] % of the current retry interval to use for jitter.
16
+ def initialize(base_seconds: nil, max_retries: nil, jitter_multiplier: nil)
17
+ @base_seconds = base_seconds || DEFAULT_BASE_SECONDS
18
+ @max_retries = max_retries || DEFAULT_MAX_RETRIES
19
+ @jitter_multiplier = jitter_multiplier || DEFAULT_JITTER_MULTIPLIER
20
+ end
21
+
22
+ ##
23
+ # @param retries [Integer] Number of current retry attempts.
24
+ #
25
+ # @return [Integer|nil] Amount of time slept, or nil if out of retries.
26
+ def delay_time(retries:)
27
+ return if retries >= max_retries
28
+
29
+ base_interval = (base_seconds**retries)
30
+ jitter_interval = base_interval * jitter_multiplier
31
+
32
+ # The random factor is never -1, but that shouldn't be an issue.
33
+ base_interval + jitter_interval * (1.0 - 2.0 * rand)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :base_seconds, :max_retries, :jitter_multiplier
39
+ end
40
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ class Config
5
+ DEFAULT_EVENT_ENDPOINT = "https://ingest.plainapm.com/"
6
+
7
+ attr_accessor :endpoint, :app_key, :enabled
8
+
9
+ def initialize
10
+ @enabled = enabled?
11
+ @endpoint = ENV["PLAIN_APM_ENDPOINT"] || DEFAULT_EVENT_ENDPOINT
12
+ @app_key = ENV["PLAIN_APM_APP_KEY"] || warn("Missing PLAIN_APM_APP_KEY environment variable, plain_apm agent won't start.")
13
+ end
14
+
15
+ private
16
+
17
+ def enabled?
18
+ force = ENV["PLAIN_APM_ENABLED"] == "1"
19
+ key_present = ENV["PLAIN_APM_APP_KEY"] != ""
20
+ production = (ENV["RAILS_ENV"] || ENV["RACK_ENV"]) == "production"
21
+
22
+ key_present && (production || force)
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ module Extensions
5
+ module TraceId
6
+ module ActiveJob
7
+ attr_accessor :trace_id
8
+
9
+ def initialize(*arguments)
10
+ super(*arguments)
11
+
12
+ # Either from request headers / a previous job, or a new trace.
13
+ @trace_id = PlainApm::Extensions::TraceId.current || SecureRandom.uuid
14
+ end
15
+
16
+ def serialize
17
+ super.update("trace_id" => trace_id)
18
+ end
19
+
20
+ def deserialize(job)
21
+ PlainApm::Extensions::TraceId.current = job["trace_id"]
22
+
23
+ super(job)
24
+ end
25
+ end
26
+
27
+ ##
28
+ # Allow tracing request ID through jobs
29
+ ActiveSupport.on_load(:active_job) do |klass|
30
+ klass.prepend(ActiveJob)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ module Extensions
5
+ module Exceptions
6
+ class Rack
7
+ def initialize(app)
8
+ @app = app
9
+ end
10
+
11
+ def call(env)
12
+ response = @app.call(env)
13
+
14
+ e = env["action_dispatch.exception"]
15
+ report_exception(e, env) unless e.nil?
16
+
17
+ response
18
+ rescue Exception => e # standard:disable Lint/RescueException
19
+ report_exception(e, env)
20
+ raise
21
+ end
22
+
23
+ private
24
+
25
+ def report_exception(e, env)
26
+ event = {
27
+ "source" => "rack_exceptions",
28
+ "name" => "exception",
29
+ "class" => e.class.name,
30
+ "message" => e.message,
31
+ "backtrace" => e.backtrace,
32
+ "params" => env["action_dispatch.request.parameters"],
33
+ "trace_id" => PlainApm::Extensions::TraceId.current
34
+ }
35
+
36
+ PlainApm::Agent.collect(event)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ module Extensions
5
+ module Exceptions
6
+ class Railtie < Rails::Railtie
7
+ initializer "plain_apm.insert_exceptions_middleware" do |app|
8
+ app.config.middleware.insert(0, PlainApm::Extensions::Exceptions::Rack)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Steve Klabnik
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ module Extensions
5
+ module TraceId
6
+ module ActiveJob
7
+ def serialize
8
+ trace_id = PlainApm::Extensions::TraceId.current || SecureRandom.uuid
9
+ super.update("trace_id" => trace_id)
10
+ end
11
+
12
+ def deserialize(job)
13
+ PlainApm::Extensions::TraceId.current = job["trace_id"]
14
+
15
+ super(job)
16
+ end
17
+ end
18
+
19
+ ##
20
+ # Allow tracing request ID through jobs
21
+ ActiveSupport.on_load(:active_job) do |klass|
22
+ klass.prepend(ActiveJob)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ ##
6
+ # This code is inspired by request_store gem by Steve Klabnik:
7
+ #
8
+ # https://github.com/steveklabnik/request_store/
9
+ #
10
+ # See LICENSE.txt in the current directory for the license.
11
+ module PlainApm
12
+ module Extensions
13
+ module TraceId
14
+ class Middleware
15
+ def initialize(app)
16
+ @app = app
17
+ end
18
+
19
+ def call(env)
20
+ TraceId.current = trace_id(env)
21
+
22
+ status, headers, body = @app.call(env)
23
+
24
+ body = Rack::BodyProxy.new(body) do
25
+ TraceId.current = nil
26
+ end
27
+
28
+ processed = true
29
+
30
+ [status, headers, body]
31
+ ensure
32
+ TraceId.current = nil if !processed
33
+ end
34
+
35
+ private
36
+
37
+ def trace_id(env)
38
+ request_id(env) || SecureRandom.uuid
39
+ end
40
+
41
+ def request_id(env)
42
+ env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # This code is inspired by request_store gem by Steve Klabnik:
5
+ #
6
+ # https://github.com/steveklabnik/request_store/
7
+ #
8
+ # See LICENSE.txt in the current directory for the license.
9
+ module PlainApm
10
+ module Extensions
11
+ module TraceId
12
+ class Railtie < Rails::Railtie
13
+ initializer "plain_apm.insert_trace_id_middleware" do |app|
14
+ if defined?(ActionDispatch::RequestId)
15
+ app.config.middleware.insert_after ActionDispatch::RequestId, PlainApm::Extensions::TraceId::Middleware
16
+ else
17
+ app.config.middleware.insert_after Rack::MethodOverride, PlainApm::Extensions::TraceId::Middleware
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,15 @@
1
+ module PlainApm
2
+ module Extensions
3
+ module TraceId
4
+ TRACE_KEY = :plain_apm_extensions_trace_id
5
+
6
+ def self.current
7
+ Thread.current[TRACE_KEY]
8
+ end
9
+
10
+ def self.current=(id)
11
+ Thread.current[TRACE_KEY] = id
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ module Hooks
5
+ class ActionMailer < ActiveSupportSubscriber
6
+ NOTIFICATION_PATTERN = /\A[^!]\w+\.action_mailer\Z/.freeze
7
+
8
+ private
9
+
10
+ def notification_pattern
11
+ NOTIFICATION_PATTERN
12
+ end
13
+
14
+ def payload(event)
15
+ name, source = *event.name.split(".")
16
+ payload = event.payload
17
+
18
+ base = {
19
+ "source" => source,
20
+ "name" => name,
21
+ "backtrace" => filtered_backtrace,
22
+ "allocations" => event.allocations,
23
+ "started_at" => event.time,
24
+ "finished_at" => event.end,
25
+ "trace_id" => trace_id
26
+ }
27
+
28
+ case name
29
+ when "deliver"
30
+ base.merge({
31
+ "message_id" => payload[:message_id],
32
+ "mailer" => payload[:mailer],
33
+ "perform_deliveries" => payload[:perform_deliveries]
34
+ })
35
+ when "process"
36
+ base.merge({
37
+ "mailer" => payload[:mailer],
38
+ "action" => payload[:action]
39
+ })
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ module Hooks
5
+ class ActionPack < ActiveSupportSubscriber
6
+ NOTIFICATION_PATTERN = /\A[^!]\w+\.action_controller\Z/.freeze
7
+
8
+ private
9
+
10
+ def notification_pattern
11
+ NOTIFICATION_PATTERN
12
+ end
13
+
14
+ def payload(event)
15
+ name, source = *event.name.split(".")
16
+ payload = event.payload
17
+
18
+ base = {
19
+ "source" => source,
20
+ "name" => name,
21
+ "backtrace" => filtered_backtrace,
22
+ "allocations" => event.allocations,
23
+ "started_at" => event.time,
24
+ "finished_at" => event.end,
25
+ "trace_id" => trace_id
26
+ }
27
+
28
+ case name
29
+ when "process_action"
30
+ base.merge({
31
+ "controller" => payload[:controller],
32
+ "action" => payload[:action],
33
+ "params" => payload[:params],
34
+ "format" => payload[:format],
35
+ "method" => payload[:method],
36
+ "path" => payload[:path],
37
+ "status" => payload[:status]
38
+ })
39
+ when "redirect_to", "start_processing", "halted_callback", "send_file", "send_data"
40
+ nil
41
+ when "read_fragment", "write_fragment", "exist_fragment?", "expire_fragment"
42
+ # controller, action, key
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ module Hooks
5
+ class ActionView < ActiveSupportSubscriber
6
+ NOTIFICATION_PATTERN = /\A[^!]\w+\.action_view\Z/.freeze
7
+
8
+ private
9
+
10
+ def notification_pattern
11
+ NOTIFICATION_PATTERN
12
+ end
13
+
14
+ def payload(event)
15
+ name, source = *event.name.split(".")
16
+ payload = event.payload
17
+
18
+ base = {
19
+ "source" => source,
20
+ "name" => name,
21
+ "backtrace" => filtered_backtrace,
22
+ "started_at" => event.time,
23
+ "finished_at" => event.end,
24
+ "allocations" => event.allocations,
25
+ "trace_id" => trace_id
26
+ }
27
+
28
+ case name
29
+ when "render_collection"
30
+ base.merge({
31
+ "identifier" => identifier(payload[:identifier]),
32
+ "layout" => payload[:layout],
33
+ "count" => payload[:count],
34
+ "cache_hits" => payload[:cache_hits]
35
+ })
36
+ when "render_layout"
37
+ base.merge({
38
+ "identifier" => identifier(payload[:identifier])
39
+ })
40
+ when "render_template"
41
+ base.merge({
42
+ "identifier" => identifier(payload[:identifier]),
43
+ "layout" => payload[:layout]
44
+ })
45
+ when "render_partial"
46
+ base.merge({
47
+ "identifier" => identifier(payload[:identifier]),
48
+ "layout" => payload[:layout],
49
+ "cache_hit" => payload[:cache_hit]
50
+ })
51
+ end
52
+ end
53
+
54
+ # strip rails root
55
+ def identifier(id)
56
+ return id unless defined?(Rails) && Rails.root
57
+
58
+ root = Rails.root.to_s
59
+
60
+ id.start_with?(root) ? id[root.size + 1..-1] : id # standard:disable Style/SlicingWithRange
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ module Hooks
5
+ class ActiveJob < ActiveSupportSubscriber
6
+ NOTIFICATION_PATTERN = /\A[^!]\w+\.active_job\Z/.freeze
7
+
8
+ private
9
+
10
+ def notification_pattern
11
+ NOTIFICATION_PATTERN
12
+ end
13
+
14
+ def payload(event)
15
+ name, source = *event.name.split(".")
16
+ payload = event.payload
17
+ job = payload[:job]
18
+
19
+ base = {
20
+ "source" => source,
21
+ "name" => name,
22
+ "backtrace" => filtered_backtrace,
23
+ "started_at" => event.time,
24
+ "finished_at" => event.end,
25
+ "trace_id" => trace_id,
26
+ "allocations" => event.allocations,
27
+ "queue_name" => job.queue_name,
28
+ "job_id" => job.job_id,
29
+ "job_class" => job.class.name,
30
+ "job_arguments" => job.arguments,
31
+ "executions" => job.executions,
32
+ "scheduled_at" => job.scheduled_at,
33
+ "adapter" => payload[:adapter].class.name,
34
+ "aborted" => payload[:aborted]
35
+ }
36
+
37
+ case name
38
+ when "enqueue", "enqueue_at", "perform"
39
+ base
40
+ when "enqueue_retry"
41
+ base.merge({
42
+ "error" => payload[:error],
43
+ "wait" => payload[:wait]
44
+ })
45
+ when "retry_stopped", "discard"
46
+ base.merge({
47
+ "error" => payload[:error]
48
+ })
49
+ when "perform_start"
50
+ nil
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "active_support_subscriber"
4
+
5
+ module PlainApm
6
+ module Hooks
7
+ class ActiveRecord < ActiveSupportSubscriber
8
+ NOTIFICATION_PATTERN = /\A[^!]\w+\.active_record\Z/.freeze
9
+
10
+ private
11
+
12
+ def notification_pattern
13
+ NOTIFICATION_PATTERN
14
+ end
15
+
16
+ def payload(event)
17
+ name, source = *event.name.split(".")
18
+ payload = event.payload
19
+
20
+ base = {
21
+ "source" => source,
22
+ "name" => name,
23
+ "backtrace" => filtered_backtrace,
24
+ "started_at" => event.time,
25
+ "finished_at" => event.end,
26
+ "allocations" => event.allocations,
27
+ "trace_id" => trace_id
28
+ }
29
+
30
+ case name
31
+ when "sql"
32
+ base.merge({
33
+ "sql_name" => payload[:name],
34
+ "sql" => payload[:sql]
35
+ })
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ module Hooks
5
+ class ActiveSupportSubscriber
6
+ def install
7
+ begin
8
+ require "active_support/notifications"
9
+ rescue LoadError
10
+ return
11
+ end
12
+
13
+ asn = ActiveSupport::Notifications
14
+
15
+ # Rails >= 6.1
16
+ if asn.respond_to?(:monotonic_subscribe)
17
+ asn.monotonic_subscribe(notification_pattern, method(:collect))
18
+ else
19
+ asn.subscribe(notification_pattern, method(:collect))
20
+ end
21
+ end
22
+
23
+ def collect(event)
24
+ # id / transaction_id is by instrumenter and thread
25
+ payload = payload(event)
26
+
27
+ return if payload.nil?
28
+
29
+ Agent.instance.collect(payload)
30
+ end
31
+
32
+ private
33
+
34
+ def pattern
35
+ raise "Not implemented"
36
+ end
37
+
38
+ def payload(event)
39
+ raise "Not implemented"
40
+ end
41
+
42
+ def filtered_backtrace
43
+ if defined?(Rails) && defined?(Rails::BacktraceCleaner)
44
+ @cleaner ||= Rails::BacktraceCleaner.new
45
+ @cleaner.clean(caller)
46
+ end
47
+ end
48
+
49
+ ##
50
+ # The trace ID (comes from either HTTP_X_REQUEST_ID header, or is
51
+ # generated by the trace_id middleware)
52
+ def trace_id
53
+ PlainApm::Extensions::TraceId.current
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,47 @@
1
+ module PlainApm
2
+ ##
3
+ # Tracks current revision of the app.
4
+ #
5
+ # This enables per-deploy metrics segmentation and checking
6
+ # for performance regressions.
7
+ module Hooks
8
+ class Deploy
9
+ ##
10
+ # Collect once, immediately on install.
11
+ def install
12
+ collect
13
+ end
14
+
15
+ def collect
16
+ result = git_revision || hg_revision || return
17
+
18
+ tool, revision = *result
19
+
20
+ Agent.instance.collect(
21
+ {"source" => tool, "revision" => revision, "name" => "deploy"}
22
+ )
23
+ end
24
+
25
+ private
26
+
27
+ # TODO: other deploy mechanisms
28
+ #
29
+ # Also, we might not be in the app root.
30
+ def git_revision
31
+ return nil unless File.exist?(".git")
32
+
33
+ ["git", `git rev-parse --short HEAD`.strip]
34
+ rescue Error::ENOENT # No git installed
35
+ nil
36
+ end
37
+
38
+ def hg_revision
39
+ return nil unless File.exist?(".hg")
40
+
41
+ ["hg", `hg log -l 1 -r . -T '{node}'`]
42
+ rescue Error::ENOENT # No mercurial installed
43
+ nil
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,53 @@
1
+ module PlainApm
2
+ module Hooks
3
+ # Rails 7 error notification mechanism
4
+ class ErrorReporter
5
+ IGNORED_EXCEPTIONS = [
6
+ "Sidekiq::JobRetry::Skip" # Sidekiq uses exceptions for control flow.
7
+ ].freeze
8
+
9
+ def install
10
+ begin
11
+ require "active_support/error_reporter"
12
+ require "active_support/lazy_load_hooks"
13
+ rescue LoadError
14
+ return
15
+ end
16
+
17
+ # Install the hook when the app is up. This might miss errors that
18
+ # happen before that, but that's OK.
19
+ ActiveSupport.on_load(:after_initialize, yield: self, run_once: true) do
20
+ ::Rails.error.subscribe(self)
21
+ end
22
+ end
23
+
24
+ def collect(e, handled:, severity:, context: {})
25
+ return if IGNORED_EXCEPTIONS.include?(e.class.name)
26
+
27
+ event = {
28
+ "source" => "error_subscriber",
29
+ "name" => "exception",
30
+ "class" => e.class.name,
31
+ "message" => e.message,
32
+ "backtrace" => e.backtrace,
33
+ "handled" => handled,
34
+ "severity" => severity,
35
+ "context" => context,
36
+ "trace_id" => PlainApm::Extensions::TraceId.current
37
+ }
38
+
39
+ if e.cause
40
+ event.merge!({
41
+ "cause_class" => e.cause.class.name,
42
+ "cause_message" => e.cause.message,
43
+ "cause_backtrace" => e.cause.backtrace
44
+ })
45
+ end
46
+
47
+ PlainApm::Agent.collect(event)
48
+ end
49
+
50
+ alias_method :report, :collect
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+
6
+ module PlainApm
7
+ class Transport
8
+ ##
9
+ # HTTP transport class, mostly a wrapper for errors and timeout-handling.
10
+
11
+ # TODO: tune these.
12
+ HTTP_READ_TIMEOUT_SECONDS = 8 # default is 60
13
+ HTTP_WRITE_TIMEOUT_SECONDS = 8 # default is 60
14
+ HTTP_OPEN_TIMEOUT_SECONDS = 8 # default is 60
15
+
16
+ HTTP_TIMEOUTS = [
17
+ Net::OpenTimeout,
18
+ Net::ReadTimeout,
19
+ RUBY_VERSION >= "2.6.0" ? Net::WriteTimeout : nil
20
+ ].compact.freeze
21
+
22
+ ERRNO_ERRORS = [
23
+ Errno::ECONNABORTED,
24
+ Errno::ECONNREFUSED,
25
+ Errno::ECONNRESET,
26
+ Errno::EHOSTDOWN,
27
+ Errno::EHOSTUNREACH,
28
+ Errno::EINVAL,
29
+ Errno::ENETUNREACH,
30
+ Errno::EPIPE
31
+ ].freeze
32
+
33
+ ##
34
+ # @param endpoint [String] http URL to send the event to
35
+ # @param app_key [String] api key / token identifying this app
36
+ # @param http_client [Net::HTTP] for dependency injection in tests
37
+ def initialize(endpoint:, app_key:, http_client: Net::HTTP)
38
+ @uri = URI.parse(endpoint)
39
+ @app_key = app_key
40
+ @http = http_client.new(uri.host, uri.port)
41
+
42
+ # Forgotten /, e.g. https://example.com:3000
43
+ uri.path = "/" if uri.path.nil? || uri.path.empty?
44
+
45
+ # TODO: our own CA bundle?
46
+ http.use_ssl = uri.scheme == "https"
47
+
48
+ http.open_timeout = HTTP_OPEN_TIMEOUT_SECONDS
49
+ http.read_timeout = HTTP_READ_TIMEOUT_SECONDS
50
+
51
+ if RUBY_VERSION >= "2.6.0"
52
+ http.write_timeout = HTTP_WRITE_TIMEOUT_SECONDS
53
+ end
54
+
55
+ at_exit { shutdown }
56
+ end
57
+
58
+ ##
59
+ # Performs the actual HTTP request.
60
+ #
61
+ # @param data [String] serialized payload to POST
62
+ # @return [Array] [response, error, retriable]
63
+ def deliver(data)
64
+ http_response do
65
+ http_request(http, uri.path, data)
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ attr_reader :http, :uri, :app_key
72
+
73
+ ##
74
+ # Opens the underlying TCP connection.
75
+ def start
76
+ http.start unless http.started?
77
+ end
78
+
79
+ ##
80
+ # Close the connection at exit.
81
+ def shutdown
82
+ http.finish if http.started?
83
+ end
84
+
85
+ def http_request(http, path, body)
86
+ request = Net::HTTP::Post.new(path, http_headers)
87
+ http.request(request, body)
88
+ end
89
+
90
+ def http_headers
91
+ {
92
+ "Content-Type" => "application/json, charset=UTF-8",
93
+ "X-PlainApm-Key" => app_key
94
+ }
95
+ end
96
+
97
+ def http_response
98
+ # Opening the connection here allows us to rescue socket and SSL errors.
99
+ # It'll be a NO-OP if already connected.
100
+ start
101
+
102
+ response = yield
103
+
104
+ case response
105
+ when Net::HTTPSuccess
106
+ [response, nil, false]
107
+ when Net::HTTPServerError, Net::HTTPTooManyRequests
108
+ [response, nil, true]
109
+ when Net::HTTPBadRequest, Net::HTTPUnauthorized, Net::HTTPRequestEntityTooLarge
110
+ [response, nil, false]
111
+ else
112
+ # Caveat: this includes redirects.
113
+ [response, nil, false]
114
+ end
115
+ rescue *HTTP_TIMEOUTS, *ERRNO_ERRORS, IOError => e
116
+ # Lowlevel libc6, connection errors.
117
+ [nil, e, true]
118
+ rescue Exception => e # standard:disable Lint/RescueException
119
+ # SSL errors, socket errors, http protocol errors from Net. To avoid the
120
+ # caller thread loop dying.
121
+ [nil, e, false]
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PlainApm
4
+ VERSION = "0.2.6"
5
+ end
data/lib/plain_apm.rb ADDED
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "plain_apm/version"
4
+ require_relative "plain_apm/transport"
5
+ require_relative "plain_apm/config"
6
+ require_relative "plain_apm/agent"
7
+
8
+ require_relative "plain_apm/hooks/deploy"
9
+
10
+ # Rails extensions
11
+ begin
12
+ require "rack/body_proxy"
13
+
14
+ require_relative "plain_apm/extensions/trace_id"
15
+ require_relative "plain_apm/extensions/trace_id/middleware"
16
+ require_relative "plain_apm/extensions/trace_id/active_job" if defined?(ActiveSupport)
17
+ require_relative "plain_apm/extensions/trace_id/railtie" if defined?(Rails::Railtie)
18
+ rescue LoadError
19
+ nil
20
+ end
21
+
22
+ # Rack exceptions
23
+ require_relative "plain_apm/extensions/exceptions/rack"
24
+ require_relative "plain_apm/extensions/exceptions/railtie" if defined?(Rails::Railtie)
25
+
26
+ # Rails instrumentation
27
+ require_relative "plain_apm/hooks/active_support_subscriber"
28
+ require_relative "plain_apm/hooks/action_mailer"
29
+ require_relative "plain_apm/hooks/action_pack"
30
+ require_relative "plain_apm/hooks/action_view"
31
+ require_relative "plain_apm/hooks/active_job"
32
+ require_relative "plain_apm/hooks/active_record"
33
+ require_relative "plain_apm/hooks/error_reporter"
34
+
35
+ module PlainApm
36
+ Agent.start
37
+ end
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: plain_apm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.6
5
+ platform: ruby
6
+ authors:
7
+ - PlainAPM Team
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-10-05 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: standard
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: appraisal
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rake
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '13.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '13.0'
69
+ description: Ruby gem to collect events/metrics and send them to PlainAPM.
70
+ email:
71
+ - support@plainapm.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - CHANGELOG.md
77
+ - Gemfile
78
+ - LICENSE.txt
79
+ - README.md
80
+ - Rakefile
81
+ - lib/plain_apm.rb
82
+ - lib/plain_apm/agent.rb
83
+ - lib/plain_apm/backoff.rb
84
+ - lib/plain_apm/config.rb
85
+ - lib/plain_apm/extensions/exceptions/active_job.rb
86
+ - lib/plain_apm/extensions/exceptions/rack.rb
87
+ - lib/plain_apm/extensions/exceptions/railtie.rb
88
+ - lib/plain_apm/extensions/trace_id.rb
89
+ - lib/plain_apm/extensions/trace_id/LICENSE.txt
90
+ - lib/plain_apm/extensions/trace_id/active_job.rb
91
+ - lib/plain_apm/extensions/trace_id/middleware.rb
92
+ - lib/plain_apm/extensions/trace_id/railtie.rb
93
+ - lib/plain_apm/hooks/action_mailer.rb
94
+ - lib/plain_apm/hooks/action_pack.rb
95
+ - lib/plain_apm/hooks/action_view.rb
96
+ - lib/plain_apm/hooks/active_job.rb
97
+ - lib/plain_apm/hooks/active_record.rb
98
+ - lib/plain_apm/hooks/active_support_subscriber.rb
99
+ - lib/plain_apm/hooks/deploy.rb
100
+ - lib/plain_apm/hooks/error_reporter.rb
101
+ - lib/plain_apm/transport.rb
102
+ - lib/plain_apm/version.rb
103
+ homepage: https://plainapm.com
104
+ licenses:
105
+ - MIT
106
+ metadata:
107
+ homepage_uri: https://plainapm.com
108
+ source_code_uri: https://github.com/plainapm/plainapm-ruby
109
+ changelog_uri: https://github.com/plainapm/plainapm-ruby/blob/main/CHANGELOG.md
110
+ github_repo: git@github.com:plainapm/plainapm-ruby.git
111
+ post_install_message:
112
+ rdoc_options: []
113
+ require_paths:
114
+ - lib
115
+ required_ruby_version: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '2.0'
120
+ required_rubygems_version: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ requirements: []
126
+ rubygems_version: 3.3.7
127
+ signing_key:
128
+ specification_version: 4
129
+ summary: PlainAPM agent
130
+ test_files: []