miniapm 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +43 -0
  3. data/LICENSE +21 -0
  4. data/README.md +174 -0
  5. data/lib/generators/miniapm/install_generator.rb +27 -0
  6. data/lib/generators/miniapm/templates/README +19 -0
  7. data/lib/generators/miniapm/templates/initializer.rb +60 -0
  8. data/lib/miniapm/configuration.rb +176 -0
  9. data/lib/miniapm/context.rb +138 -0
  10. data/lib/miniapm/error_event.rb +130 -0
  11. data/lib/miniapm/exporters/errors.rb +67 -0
  12. data/lib/miniapm/exporters/otlp.rb +90 -0
  13. data/lib/miniapm/instrumentations/activejob.rb +271 -0
  14. data/lib/miniapm/instrumentations/activerecord.rb +123 -0
  15. data/lib/miniapm/instrumentations/base.rb +61 -0
  16. data/lib/miniapm/instrumentations/cache.rb +85 -0
  17. data/lib/miniapm/instrumentations/http/faraday.rb +112 -0
  18. data/lib/miniapm/instrumentations/http/httparty.rb +84 -0
  19. data/lib/miniapm/instrumentations/http/net_http.rb +99 -0
  20. data/lib/miniapm/instrumentations/rails/controller.rb +129 -0
  21. data/lib/miniapm/instrumentations/rails/railtie.rb +42 -0
  22. data/lib/miniapm/instrumentations/redis/redis.rb +135 -0
  23. data/lib/miniapm/instrumentations/redis/redis_client.rb +116 -0
  24. data/lib/miniapm/instrumentations/registry.rb +90 -0
  25. data/lib/miniapm/instrumentations/search/elasticsearch.rb +121 -0
  26. data/lib/miniapm/instrumentations/search/opensearch.rb +120 -0
  27. data/lib/miniapm/instrumentations/search/searchkick.rb +119 -0
  28. data/lib/miniapm/instrumentations/sidekiq.rb +185 -0
  29. data/lib/miniapm/middleware/error_handler.rb +120 -0
  30. data/lib/miniapm/middleware/rack.rb +103 -0
  31. data/lib/miniapm/span.rb +289 -0
  32. data/lib/miniapm/testing.rb +209 -0
  33. data/lib/miniapm/trace.rb +26 -0
  34. data/lib/miniapm/transport/batch_sender.rb +345 -0
  35. data/lib/miniapm/transport/http.rb +45 -0
  36. data/lib/miniapm/version.rb +5 -0
  37. data/lib/miniapm.rb +184 -0
  38. metadata +183 -0
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniAPM
4
+ module Instrumentations
5
+ class Base
6
+ class << self
7
+ def install!
8
+ raise NotImplementedError, "Subclass must implement .install!"
9
+ end
10
+
11
+ def installed?
12
+ @installed || false
13
+ end
14
+
15
+ protected
16
+
17
+ def mark_installed!
18
+ @installed = true
19
+ end
20
+
21
+ def subscribe(event_name, &block)
22
+ ActiveSupport::Notifications.subscribe(event_name) do |*args|
23
+ event = ActiveSupport::Notifications::Event.new(*args)
24
+ block.call(event)
25
+ rescue StandardError => e
26
+ MiniAPM.logger.debug { "MiniAPM instrumentation error in #{event_name}: #{e.message}" }
27
+ end
28
+ end
29
+
30
+ def create_span_from_event(event, name:, category:, attributes: {})
31
+ return unless MiniAPM.enabled?
32
+ return unless Context.current_trace
33
+
34
+ span = Span.new(
35
+ name: name,
36
+ category: category,
37
+ trace_id: Context.current_trace_id,
38
+ parent_span_id: Context.current_span&.span_id,
39
+ attributes: attributes
40
+ )
41
+
42
+ # Backfill timing from event
43
+ if event.time && event.end
44
+ span.instance_variable_set(:@start_time, (event.time.to_f * 1_000_000_000).to_i)
45
+ span.instance_variable_set(:@end_time, (event.end.to_f * 1_000_000_000).to_i)
46
+ else
47
+ span.finish
48
+ end
49
+
50
+ span
51
+ end
52
+
53
+ def record_span(span)
54
+ return unless span
55
+
56
+ MiniAPM.record_span(span)
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniAPM
4
+ module Instrumentations
5
+ class Cache < Base
6
+ EVENTS = %w[
7
+ cache_read.active_support
8
+ cache_write.active_support
9
+ cache_delete.active_support
10
+ cache_exist?.active_support
11
+ cache_fetch_hit.active_support
12
+ cache_generate.active_support
13
+ cache_increment.active_support
14
+ cache_decrement.active_support
15
+ ].freeze
16
+
17
+ # Minimum duration to record (skip very fast operations)
18
+ MIN_DURATION_MS = 0.5
19
+
20
+ class << self
21
+ def install!
22
+ return if installed?
23
+ mark_installed!
24
+
25
+ EVENTS.each do |event_name|
26
+ subscribe(event_name) do |event|
27
+ handle_cache_event(event)
28
+ end
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def handle_cache_event(event)
35
+ return unless MiniAPM.enabled?
36
+ return unless Context.current_trace
37
+
38
+ # Skip very fast cache operations to reduce noise
39
+ return if event.duration && event.duration < MIN_DURATION_MS
40
+
41
+ payload = event.payload
42
+ operation = event.name.sub("cache_", "").sub(".active_support", "")
43
+ key = payload[:key]
44
+
45
+ attributes = {
46
+ "cache.operation" => operation,
47
+ "cache.key" => truncate_key(key)
48
+ }
49
+
50
+ # Add hit/miss info
51
+ if payload.key?(:hit)
52
+ attributes["cache.hit"] = payload[:hit]
53
+ end
54
+
55
+ # Add store class if available
56
+ if payload[:store]
57
+ attributes["cache.store"] = payload[:store].to_s
58
+ end
59
+
60
+ # Add super_operation for fetch
61
+ if payload[:super_operation]
62
+ attributes["cache.super_operation"] = payload[:super_operation].to_s
63
+ end
64
+
65
+ span = create_span_from_event(
66
+ event,
67
+ name: "cache #{operation}",
68
+ category: :cache,
69
+ attributes: attributes
70
+ )
71
+
72
+ record_span(span)
73
+ end
74
+
75
+ def truncate_key(key)
76
+ key_str = key.to_s
77
+ key_str.length > 200 ? key_str[0, 200] + "..." : key_str
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ # Auto-install when loaded
85
+ MiniAPM::Instrumentations::Cache.install!
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniAPM
4
+ module Instrumentations
5
+ module HTTP
6
+ class Faraday
7
+ class << self
8
+ def install!
9
+ return if @installed
10
+ return unless defined?(::Faraday)
11
+
12
+ @installed = true
13
+
14
+ # Define the middleware class only when Faraday is available
15
+ define_middleware_class!
16
+
17
+ # Register our middleware
18
+ ::Faraday::Middleware.register_middleware(miniapm: middleware_class)
19
+
20
+ # Auto-inject into all connections by patching Connection
21
+ ::Faraday::Connection.prepend(ConnectionPatch)
22
+
23
+ MiniAPM.logger.debug { "MiniAPM: Faraday instrumentation installed" }
24
+ end
25
+
26
+ def installed?
27
+ @installed || false
28
+ end
29
+
30
+ def middleware_class
31
+ @middleware_class
32
+ end
33
+
34
+ private
35
+
36
+ def define_middleware_class!
37
+ @middleware_class = Class.new(::Faraday::Middleware) do
38
+ def call(env)
39
+ return @app.call(env) unless MiniAPM.enabled?
40
+ return @app.call(env) unless MiniAPM::Context.current_trace
41
+
42
+ uri = env.url
43
+ http_method = env.method.to_s.upcase
44
+
45
+ # Inject trace context
46
+ MiniAPM::Context.inject_into_headers(env.request_headers)
47
+
48
+ span = MiniAPM::Span.new(
49
+ name: "#{http_method} #{uri.host}#{uri.path}",
50
+ category: :http_client,
51
+ trace_id: MiniAPM::Context.current_trace_id,
52
+ parent_span_id: MiniAPM::Context.current_span&.span_id,
53
+ attributes: {
54
+ "http.method" => http_method,
55
+ "http.url" => sanitize_url(uri),
56
+ "http.host" => uri.host,
57
+ "net.peer.name" => uri.host,
58
+ "net.peer.port" => uri.port || (uri.scheme == "https" ? 443 : 80)
59
+ }
60
+ )
61
+
62
+ MiniAPM::Context.with_span(span) do
63
+ begin
64
+ response = @app.call(env)
65
+
66
+ span.add_attribute("http.status_code", response.status)
67
+
68
+ if response.status >= 400
69
+ span.set_error("HTTP #{response.status}")
70
+ else
71
+ span.set_ok
72
+ end
73
+
74
+ response
75
+ rescue StandardError => e
76
+ span.record_exception(e)
77
+ raise
78
+ ensure
79
+ span.finish
80
+ MiniAPM.record_span(span)
81
+ end
82
+ end
83
+ end
84
+
85
+ private
86
+
87
+ def sanitize_url(uri)
88
+ port = uri.port || (uri.scheme == "https" ? 443 : 80)
89
+ "#{uri.scheme}://#{uri.host}:#{port}#{uri.path}"
90
+ end
91
+ end
92
+ end
93
+ end
94
+
95
+ # Patch to auto-inject middleware
96
+ module ConnectionPatch
97
+ def initialize(url = nil, options = nil, &block)
98
+ super
99
+
100
+ middleware_class = MiniAPM::Instrumentations::HTTP::Faraday.middleware_class
101
+ # Add our middleware if not already present
102
+ unless @builder.handlers.any? { |h| h.klass == middleware_class }
103
+ @builder.insert(0, middleware_class)
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ # Installation is handled by the registry, not auto-install
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniAPM
4
+ module Instrumentations
5
+ module HTTP
6
+ class HTTParty
7
+ class << self
8
+ def install!
9
+ return if @installed
10
+ return unless defined?(::HTTParty)
11
+
12
+ @installed = true
13
+ ::HTTParty::Request.prepend(Patch)
14
+
15
+ MiniAPM.logger.debug { "MiniAPM: HTTParty instrumentation installed" }
16
+ end
17
+
18
+ def installed?
19
+ @installed || false
20
+ end
21
+ end
22
+
23
+ module Patch
24
+ def perform(&block)
25
+ return super unless MiniAPM.enabled?
26
+ return super unless MiniAPM::Context.current_trace
27
+
28
+ uri = self.uri
29
+ http_method = self.http_method.name.split("::").last.upcase
30
+
31
+ # Inject trace context into outgoing request
32
+ MiniAPM::Context.inject_into_headers(options[:headers] ||= {})
33
+
34
+ span = MiniAPM::Span.new(
35
+ name: "#{http_method} #{uri.host}#{uri.path}",
36
+ category: :http_client,
37
+ trace_id: MiniAPM::Context.current_trace_id,
38
+ parent_span_id: MiniAPM::Context.current_span&.span_id,
39
+ attributes: {
40
+ "http.method" => http_method,
41
+ "http.url" => sanitize_url(uri),
42
+ "http.host" => uri.host,
43
+ "net.peer.name" => uri.host,
44
+ "net.peer.port" => uri.port
45
+ }
46
+ )
47
+
48
+ MiniAPM::Context.with_span(span) do
49
+ begin
50
+ response = super
51
+
52
+ if response
53
+ span.add_attribute("http.status_code", response.code)
54
+
55
+ if response.code >= 400
56
+ span.set_error("HTTP #{response.code}")
57
+ else
58
+ span.set_ok
59
+ end
60
+ end
61
+
62
+ response
63
+ rescue StandardError => e
64
+ span.record_exception(e)
65
+ raise
66
+ ensure
67
+ span.finish
68
+ MiniAPM.record_span(span)
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def sanitize_url(uri)
76
+ "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}"
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+
84
+ # Installation is handled by the registry, not auto-install
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniAPM
4
+ module Instrumentations
5
+ module HTTP
6
+ class NetHTTP
7
+ class << self
8
+ def install!
9
+ return if @installed
10
+ return unless defined?(::Net::HTTP)
11
+
12
+ @installed = true
13
+ ::Net::HTTP.prepend(Patch)
14
+
15
+ MiniAPM.logger.debug { "MiniAPM: Net::HTTP instrumentation installed" }
16
+ end
17
+
18
+ def installed?
19
+ @installed || false
20
+ end
21
+ end
22
+
23
+ module Patch
24
+ def request(req, body = nil, &block)
25
+ return super unless MiniAPM.enabled?
26
+ return super unless MiniAPM::Context.current_trace
27
+
28
+ # Skip if this is MiniAPM's own request
29
+ return super if req["User-Agent"]&.include?("miniapm-ruby")
30
+
31
+ uri = build_uri(req)
32
+
33
+ # Inject trace context into outgoing request
34
+ MiniAPM::Context.inject_into_headers(req)
35
+
36
+ span = MiniAPM::Span.new(
37
+ name: "#{req.method} #{uri.host}#{uri.path}",
38
+ category: :http_client,
39
+ trace_id: MiniAPM::Context.current_trace_id,
40
+ parent_span_id: MiniAPM::Context.current_span&.span_id,
41
+ attributes: {
42
+ "http.method" => req.method,
43
+ "http.url" => sanitize_url(uri),
44
+ "http.host" => uri.host,
45
+ "net.peer.name" => uri.host,
46
+ "net.peer.port" => uri.port
47
+ }
48
+ )
49
+
50
+ MiniAPM::Context.with_span(span) do
51
+ begin
52
+ response = super
53
+
54
+ span.add_attribute("http.status_code", response.code.to_i)
55
+ span.add_attribute("http.response_content_length", response["content-length"].to_i) if response["content-length"]
56
+
57
+ if response.code.to_i >= 400
58
+ span.set_error("HTTP #{response.code}")
59
+ else
60
+ span.set_ok
61
+ end
62
+
63
+ response
64
+ rescue StandardError => e
65
+ span.record_exception(e)
66
+ raise
67
+ ensure
68
+ span.finish
69
+ MiniAPM.record_span(span)
70
+ end
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def build_uri(req)
77
+ scheme = use_ssl? ? "https" : "http"
78
+ host = address
79
+ port_str = (use_ssl? && port == 443) || (!use_ssl? && port == 80) ? "" : ":#{port}"
80
+
81
+ path = req.path || "/"
82
+ path = "/" + path unless path.start_with?("/")
83
+
84
+ URI.parse("#{scheme}://#{host}#{port_str}#{path}")
85
+ rescue StandardError
86
+ URI.parse("http://#{address}:#{port}#{req.path}")
87
+ end
88
+
89
+ def sanitize_url(uri)
90
+ # Remove query params for privacy
91
+ "#{uri.scheme}://#{uri.host}:#{uri.port}#{uri.path}"
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+
99
+ # Installation is handled by the registry, not auto-install
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MiniAPM
4
+ module Instrumentations
5
+ module Rails
6
+ class Controller < Base
7
+ class << self
8
+ def install!
9
+ return if installed?
10
+ mark_installed!
11
+
12
+ # Subscribe to controller processing
13
+ subscribe("process_action.action_controller") do |event|
14
+ handle_process_action(event)
15
+ end
16
+
17
+ # Subscribe to view rendering
18
+ subscribe("render_template.action_view") do |event|
19
+ handle_render_template(event)
20
+ end
21
+
22
+ subscribe("render_partial.action_view") do |event|
23
+ handle_render_partial(event)
24
+ end
25
+
26
+ subscribe("render_collection.action_view") do |event|
27
+ handle_render_collection(event)
28
+ end
29
+
30
+ subscribe("render_layout.action_view") do |event|
31
+ handle_render_layout(event)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def handle_process_action(event)
38
+ span = Context.current_span
39
+ return unless span&.root?
40
+
41
+ payload = event.payload
42
+
43
+ # Update root span with controller info
44
+ span.add_attribute("http.method", payload[:method])
45
+ span.add_attribute("http.route", "#{payload[:controller]}##{payload[:action]}")
46
+ span.add_attribute("rails.controller", payload[:controller])
47
+ span.add_attribute("rails.action", payload[:action])
48
+ span.add_attribute("rails.format", payload[:format]) if payload[:format]
49
+
50
+ if payload[:status]
51
+ span.add_attribute("http.status_code", payload[:status])
52
+ span.set_error("HTTP #{payload[:status]}") if payload[:status] >= 500
53
+ end
54
+
55
+ # Add timing breakdown
56
+ if payload[:db_runtime]
57
+ span.add_attribute("rails.db_runtime_ms", payload[:db_runtime].round(2))
58
+ end
59
+
60
+ if payload[:view_runtime]
61
+ span.add_attribute("rails.view_runtime_ms", payload[:view_runtime].round(2))
62
+ end
63
+
64
+ # Record exception if present
65
+ if payload[:exception_object]
66
+ span.record_exception(payload[:exception_object])
67
+ end
68
+ end
69
+
70
+ def handle_render_template(event)
71
+ record_view_span("render_template", event)
72
+ end
73
+
74
+ def handle_render_partial(event)
75
+ record_view_span("render_partial", event)
76
+ end
77
+
78
+ def handle_render_collection(event)
79
+ record_view_span("render_collection", event)
80
+ end
81
+
82
+ def handle_render_layout(event)
83
+ record_view_span("render_layout", event)
84
+ end
85
+
86
+ def record_view_span(type, event)
87
+ return unless MiniAPM.enabled?
88
+ return unless Context.current_trace
89
+
90
+ payload = event.payload
91
+ template = payload[:identifier] || payload[:virtual_path] || "unknown"
92
+
93
+ # Clean up template path
94
+ if defined?(::Rails.root) && ::Rails.root
95
+ template = template.sub(::Rails.root.to_s + "/", "")
96
+ end
97
+
98
+ template_name = File.basename(template)
99
+
100
+ attributes = {
101
+ "rails.template" => template,
102
+ "rails.template.type" => type
103
+ }
104
+
105
+ if payload[:layout]
106
+ attributes["rails.layout"] = payload[:layout]
107
+ end
108
+
109
+ if payload[:count]
110
+ attributes["rails.collection.count"] = payload[:count]
111
+ end
112
+
113
+ span = create_span_from_event(
114
+ event,
115
+ name: "#{type} #{template_name}",
116
+ category: :view,
117
+ attributes: attributes
118
+ )
119
+
120
+ record_span(span)
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ # Auto-install when loaded
129
+ MiniAPM::Instrumentations::Rails::Controller.install!
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+
5
+ module MiniAPM
6
+ module Instrumentations
7
+ module Rails
8
+ class Railtie < ::Rails::Railtie
9
+ initializer "miniapm.configure_rails_initialization" do |app|
10
+ # Insert middleware at the beginning of the stack
11
+ app.middleware.insert(0, MiniAPM::Middleware::Rack)
12
+ app.middleware.insert(1, MiniAPM::Middleware::ErrorHandler)
13
+ end
14
+
15
+ config.after_initialize do
16
+ # Auto-detect Rails version
17
+ MiniAPM.configuration.rails_version ||= ::Rails::VERSION::STRING
18
+
19
+ # Auto-detect environment
20
+ MiniAPM.configuration.environment = ::Rails.env.to_s
21
+
22
+ # Disable in test by default unless explicitly enabled
23
+ if ::Rails.env.test? && ENV["MINI_APM_ENABLED_IN_TEST"].nil?
24
+ MiniAPM.configuration.enabled = false
25
+ end
26
+
27
+ # Use Rails logger if available
28
+ if ::Rails.logger && MiniAPM.configuration.auto_start
29
+ MiniAPM.logger = ::Rails.logger
30
+ end
31
+
32
+ # Start MiniAPM if auto_start is enabled
33
+ MiniAPM.start! if MiniAPM.configuration.auto_start
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ # Require middleware
41
+ require_relative "../../middleware/rack"
42
+ require_relative "../../middleware/error_handler"