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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +43 -0
- data/LICENSE +21 -0
- data/README.md +174 -0
- data/lib/generators/miniapm/install_generator.rb +27 -0
- data/lib/generators/miniapm/templates/README +19 -0
- data/lib/generators/miniapm/templates/initializer.rb +60 -0
- data/lib/miniapm/configuration.rb +176 -0
- data/lib/miniapm/context.rb +138 -0
- data/lib/miniapm/error_event.rb +130 -0
- data/lib/miniapm/exporters/errors.rb +67 -0
- data/lib/miniapm/exporters/otlp.rb +90 -0
- data/lib/miniapm/instrumentations/activejob.rb +271 -0
- data/lib/miniapm/instrumentations/activerecord.rb +123 -0
- data/lib/miniapm/instrumentations/base.rb +61 -0
- data/lib/miniapm/instrumentations/cache.rb +85 -0
- data/lib/miniapm/instrumentations/http/faraday.rb +112 -0
- data/lib/miniapm/instrumentations/http/httparty.rb +84 -0
- data/lib/miniapm/instrumentations/http/net_http.rb +99 -0
- data/lib/miniapm/instrumentations/rails/controller.rb +129 -0
- data/lib/miniapm/instrumentations/rails/railtie.rb +42 -0
- data/lib/miniapm/instrumentations/redis/redis.rb +135 -0
- data/lib/miniapm/instrumentations/redis/redis_client.rb +116 -0
- data/lib/miniapm/instrumentations/registry.rb +90 -0
- data/lib/miniapm/instrumentations/search/elasticsearch.rb +121 -0
- data/lib/miniapm/instrumentations/search/opensearch.rb +120 -0
- data/lib/miniapm/instrumentations/search/searchkick.rb +119 -0
- data/lib/miniapm/instrumentations/sidekiq.rb +185 -0
- data/lib/miniapm/middleware/error_handler.rb +120 -0
- data/lib/miniapm/middleware/rack.rb +103 -0
- data/lib/miniapm/span.rb +289 -0
- data/lib/miniapm/testing.rb +209 -0
- data/lib/miniapm/trace.rb +26 -0
- data/lib/miniapm/transport/batch_sender.rb +345 -0
- data/lib/miniapm/transport/http.rb +45 -0
- data/lib/miniapm/version.rb +5 -0
- data/lib/miniapm.rb +184 -0
- metadata +183 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MiniAPM
|
|
4
|
+
module Instrumentations
|
|
5
|
+
module Redis
|
|
6
|
+
class Redis
|
|
7
|
+
class << self
|
|
8
|
+
def install!
|
|
9
|
+
return if @installed
|
|
10
|
+
return unless defined?(::Redis)
|
|
11
|
+
# Skip if redis-client is present (it's the modern replacement)
|
|
12
|
+
return if defined?(::RedisClient)
|
|
13
|
+
|
|
14
|
+
@installed = true
|
|
15
|
+
::Redis::Client.prepend(Patch)
|
|
16
|
+
|
|
17
|
+
MiniAPM.logger.debug { "MiniAPM: redis gem instrumentation installed" }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def installed?
|
|
21
|
+
@installed || false
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module Patch
|
|
26
|
+
def call(command)
|
|
27
|
+
return super unless MiniAPM.enabled?
|
|
28
|
+
return super unless MiniAPM::Context.current_trace
|
|
29
|
+
|
|
30
|
+
operation = command.first.to_s.upcase
|
|
31
|
+
conn_info = extract_connection_info
|
|
32
|
+
|
|
33
|
+
span = MiniAPM::Span.new(
|
|
34
|
+
name: "REDIS #{operation}",
|
|
35
|
+
category: :cache,
|
|
36
|
+
trace_id: MiniAPM::Context.current_trace_id,
|
|
37
|
+
parent_span_id: MiniAPM::Context.current_span&.span_id,
|
|
38
|
+
attributes: {
|
|
39
|
+
"db.system" => "redis",
|
|
40
|
+
"db.operation" => operation,
|
|
41
|
+
"db.redis.database_index" => conn_info[:db],
|
|
42
|
+
"net.peer.name" => conn_info[:host],
|
|
43
|
+
"net.peer.port" => conn_info[:port]
|
|
44
|
+
}.compact
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Add key info for common operations
|
|
48
|
+
if command.length > 1 && %w[GET SET DEL INCR DECR EXPIRE TTL EXISTS HGET HSET LPUSH RPUSH].include?(operation)
|
|
49
|
+
key = command[1].to_s
|
|
50
|
+
span.add_attribute("db.redis.key", truncate_key(key))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
MiniAPM::Context.with_span(span) do
|
|
54
|
+
begin
|
|
55
|
+
result = super
|
|
56
|
+
span.set_ok
|
|
57
|
+
result
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
span.record_exception(e)
|
|
60
|
+
raise
|
|
61
|
+
ensure
|
|
62
|
+
span.finish
|
|
63
|
+
MiniAPM.record_span(span)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def call_pipeline(pipeline)
|
|
69
|
+
return super unless MiniAPM.enabled?
|
|
70
|
+
return super unless MiniAPM::Context.current_trace
|
|
71
|
+
|
|
72
|
+
commands = pipeline.commands
|
|
73
|
+
operations = commands.map { |c| c.first.to_s.upcase }.uniq.join(", ")
|
|
74
|
+
conn_info = extract_connection_info
|
|
75
|
+
|
|
76
|
+
span = MiniAPM::Span.new(
|
|
77
|
+
name: "REDIS PIPELINE (#{commands.size} commands)",
|
|
78
|
+
category: :cache,
|
|
79
|
+
trace_id: MiniAPM::Context.current_trace_id,
|
|
80
|
+
parent_span_id: MiniAPM::Context.current_span&.span_id,
|
|
81
|
+
attributes: {
|
|
82
|
+
"db.system" => "redis",
|
|
83
|
+
"db.operation" => "PIPELINE",
|
|
84
|
+
"db.redis.database_index" => conn_info[:db],
|
|
85
|
+
"db.redis.pipeline_length" => commands.size,
|
|
86
|
+
"db.redis.operations" => operations,
|
|
87
|
+
"net.peer.name" => conn_info[:host],
|
|
88
|
+
"net.peer.port" => conn_info[:port]
|
|
89
|
+
}.compact
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
MiniAPM::Context.with_span(span) do
|
|
93
|
+
begin
|
|
94
|
+
result = super
|
|
95
|
+
span.set_ok
|
|
96
|
+
result
|
|
97
|
+
rescue StandardError => e
|
|
98
|
+
span.record_exception(e)
|
|
99
|
+
raise
|
|
100
|
+
ensure
|
|
101
|
+
span.finish
|
|
102
|
+
MiniAPM.record_span(span)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def extract_connection_info
|
|
110
|
+
# Redis::Client in older redis gem versions has different APIs
|
|
111
|
+
# Try various methods to get connection info safely
|
|
112
|
+
{
|
|
113
|
+
host: safe_connection_value(:host),
|
|
114
|
+
port: safe_connection_value(:port),
|
|
115
|
+
db: safe_connection_value(:db)
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def safe_connection_value(attr)
|
|
120
|
+
respond_to?(attr) ? send(attr) : options[attr]
|
|
121
|
+
rescue
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def truncate_key(key)
|
|
126
|
+
key.length > 100 ? key[0...100] + "..." : key
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Auto-install when loaded
|
|
135
|
+
MiniAPM::Instrumentations::Redis::Redis.install!
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MiniAPM
|
|
4
|
+
module Instrumentations
|
|
5
|
+
module Redis
|
|
6
|
+
class RedisClient
|
|
7
|
+
class << self
|
|
8
|
+
def install!
|
|
9
|
+
return if @installed
|
|
10
|
+
return unless defined?(::RedisClient)
|
|
11
|
+
|
|
12
|
+
@installed = true
|
|
13
|
+
|
|
14
|
+
# RedisClient supports middleware registration
|
|
15
|
+
::RedisClient.register(Middleware)
|
|
16
|
+
|
|
17
|
+
MiniAPM.logger.debug { "MiniAPM: redis-client instrumentation installed" }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def installed?
|
|
21
|
+
@installed || false
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module Middleware
|
|
26
|
+
def call(command, config)
|
|
27
|
+
return super unless MiniAPM.enabled?
|
|
28
|
+
return super unless MiniAPM::Context.current_trace
|
|
29
|
+
|
|
30
|
+
operation = command.first.to_s.upcase
|
|
31
|
+
|
|
32
|
+
span = MiniAPM::Span.new(
|
|
33
|
+
name: "REDIS #{operation}",
|
|
34
|
+
category: :cache,
|
|
35
|
+
trace_id: MiniAPM::Context.current_trace_id,
|
|
36
|
+
parent_span_id: MiniAPM::Context.current_span&.span_id,
|
|
37
|
+
attributes: {
|
|
38
|
+
"db.system" => "redis",
|
|
39
|
+
"db.operation" => operation,
|
|
40
|
+
"db.redis.database_index" => config.db,
|
|
41
|
+
"net.peer.name" => config.host,
|
|
42
|
+
"net.peer.port" => config.port
|
|
43
|
+
}
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Add key info for common operations (first arg after command)
|
|
47
|
+
if command.length > 1 && %w[GET SET DEL INCR DECR EXPIRE TTL EXISTS HGET HSET LPUSH RPUSH].include?(operation)
|
|
48
|
+
key = command[1].to_s
|
|
49
|
+
span.add_attribute("db.redis.key", truncate_key(key))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
MiniAPM::Context.with_span(span) do
|
|
53
|
+
begin
|
|
54
|
+
result = super
|
|
55
|
+
span.set_ok
|
|
56
|
+
result
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
span.record_exception(e)
|
|
59
|
+
raise
|
|
60
|
+
ensure
|
|
61
|
+
span.finish
|
|
62
|
+
MiniAPM.record_span(span)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call_pipelined(commands, config)
|
|
68
|
+
return super unless MiniAPM.enabled?
|
|
69
|
+
return super unless MiniAPM::Context.current_trace
|
|
70
|
+
|
|
71
|
+
operations = commands.map { |c| c.first.to_s.upcase }.uniq.join(", ")
|
|
72
|
+
|
|
73
|
+
span = MiniAPM::Span.new(
|
|
74
|
+
name: "REDIS PIPELINE (#{commands.size} commands)",
|
|
75
|
+
category: :cache,
|
|
76
|
+
trace_id: MiniAPM::Context.current_trace_id,
|
|
77
|
+
parent_span_id: MiniAPM::Context.current_span&.span_id,
|
|
78
|
+
attributes: {
|
|
79
|
+
"db.system" => "redis",
|
|
80
|
+
"db.operation" => "PIPELINE",
|
|
81
|
+
"db.redis.database_index" => config.db,
|
|
82
|
+
"db.redis.pipeline_length" => commands.size,
|
|
83
|
+
"db.redis.operations" => operations,
|
|
84
|
+
"net.peer.name" => config.host,
|
|
85
|
+
"net.peer.port" => config.port
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
MiniAPM::Context.with_span(span) do
|
|
90
|
+
begin
|
|
91
|
+
result = super
|
|
92
|
+
span.set_ok
|
|
93
|
+
result
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
span.record_exception(e)
|
|
96
|
+
raise
|
|
97
|
+
ensure
|
|
98
|
+
span.finish
|
|
99
|
+
MiniAPM.record_span(span)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def truncate_key(key)
|
|
107
|
+
key.length > 100 ? key[0...100] + "..." : key
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Auto-install when loaded
|
|
116
|
+
MiniAPM::Instrumentations::Redis::RedisClient.install!
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MiniAPM
|
|
4
|
+
module Instrumentations
|
|
5
|
+
class Registry
|
|
6
|
+
INSTRUMENTATIONS = {
|
|
7
|
+
# Rails core (via ActiveSupport::Notifications)
|
|
8
|
+
rails: "miniapm/instrumentations/rails/controller",
|
|
9
|
+
activerecord: "miniapm/instrumentations/activerecord",
|
|
10
|
+
activejob: "miniapm/instrumentations/activejob",
|
|
11
|
+
cache: "miniapm/instrumentations/cache",
|
|
12
|
+
|
|
13
|
+
# Background jobs
|
|
14
|
+
sidekiq: "miniapm/instrumentations/sidekiq",
|
|
15
|
+
|
|
16
|
+
# HTTP clients
|
|
17
|
+
net_http: "miniapm/instrumentations/http/net_http",
|
|
18
|
+
httparty: "miniapm/instrumentations/http/httparty",
|
|
19
|
+
faraday: "miniapm/instrumentations/http/faraday",
|
|
20
|
+
|
|
21
|
+
# Search
|
|
22
|
+
opensearch: "miniapm/instrumentations/search/opensearch",
|
|
23
|
+
elasticsearch: "miniapm/instrumentations/search/elasticsearch",
|
|
24
|
+
searchkick: "miniapm/instrumentations/search/searchkick",
|
|
25
|
+
|
|
26
|
+
# Redis
|
|
27
|
+
redis_client: "miniapm/instrumentations/redis/redis_client",
|
|
28
|
+
redis: "miniapm/instrumentations/redis/redis"
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def install_all!
|
|
33
|
+
INSTRUMENTATIONS.each do |name, path|
|
|
34
|
+
next unless should_install?(name)
|
|
35
|
+
|
|
36
|
+
begin
|
|
37
|
+
require_relative path.sub("miniapm/instrumentations/", "")
|
|
38
|
+
MiniAPM.logger.debug { "MiniAPM: Installed #{name} instrumentation" }
|
|
39
|
+
rescue LoadError => e
|
|
40
|
+
MiniAPM.logger.debug { "MiniAPM: Skipped #{name} (dependency not available: #{e.message})" }
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
MiniAPM.logger.warn { "MiniAPM: Failed to install #{name}: #{e.message}" }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def should_install?(name)
|
|
50
|
+
MiniAPM.configuration.instrumentations.enabled?(name) && gem_present?(name)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def gem_present?(name)
|
|
54
|
+
result = case name
|
|
55
|
+
when :rails
|
|
56
|
+
defined?(Rails) && defined?(ActionController)
|
|
57
|
+
when :activerecord
|
|
58
|
+
defined?(ActiveRecord::Base) && defined?(ActiveSupport::Notifications)
|
|
59
|
+
when :activejob
|
|
60
|
+
defined?(ActiveJob::Base) && defined?(ActiveSupport::Notifications)
|
|
61
|
+
when :sidekiq
|
|
62
|
+
defined?(Sidekiq)
|
|
63
|
+
when :cache
|
|
64
|
+
defined?(ActiveSupport::Cache::Store)
|
|
65
|
+
when :net_http
|
|
66
|
+
defined?(Net::HTTP)
|
|
67
|
+
when :httparty
|
|
68
|
+
defined?(HTTParty)
|
|
69
|
+
when :faraday
|
|
70
|
+
defined?(Faraday)
|
|
71
|
+
when :opensearch
|
|
72
|
+
defined?(OpenSearch::Client)
|
|
73
|
+
when :elasticsearch
|
|
74
|
+
defined?(Elasticsearch::Client)
|
|
75
|
+
when :searchkick
|
|
76
|
+
defined?(Searchkick)
|
|
77
|
+
when :redis_client
|
|
78
|
+
defined?(RedisClient)
|
|
79
|
+
when :redis
|
|
80
|
+
defined?(Redis) && !defined?(RedisClient)
|
|
81
|
+
else
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
# Convert to boolean (defined? returns string or nil)
|
|
85
|
+
!!result
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MiniAPM
|
|
4
|
+
module Instrumentations
|
|
5
|
+
module Search
|
|
6
|
+
class Elasticsearch
|
|
7
|
+
class << self
|
|
8
|
+
def install!
|
|
9
|
+
return if @installed
|
|
10
|
+
return unless defined?(::Elasticsearch::Transport::Client)
|
|
11
|
+
|
|
12
|
+
@installed = true
|
|
13
|
+
::Elasticsearch::Transport::Client.prepend(Patch)
|
|
14
|
+
|
|
15
|
+
MiniAPM.logger.debug { "MiniAPM: Elasticsearch instrumentation installed" }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def installed?
|
|
19
|
+
@installed || false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Patch
|
|
24
|
+
def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {})
|
|
25
|
+
return super unless MiniAPM.enabled?
|
|
26
|
+
return super unless MiniAPM::Context.current_trace
|
|
27
|
+
|
|
28
|
+
operation = extract_operation(method, path)
|
|
29
|
+
index = extract_index(path)
|
|
30
|
+
|
|
31
|
+
span = MiniAPM::Span.new(
|
|
32
|
+
name: "ES #{operation}#{index ? " #{index}" : ""}",
|
|
33
|
+
category: :search,
|
|
34
|
+
trace_id: MiniAPM::Context.current_trace_id,
|
|
35
|
+
parent_span_id: MiniAPM::Context.current_span&.span_id,
|
|
36
|
+
attributes: {
|
|
37
|
+
"db.system" => "elasticsearch",
|
|
38
|
+
"db.operation" => operation,
|
|
39
|
+
"http.method" => method.to_s.upcase,
|
|
40
|
+
"http.url" => path
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
span.add_attribute("elasticsearch.index", index) if index
|
|
45
|
+
|
|
46
|
+
# Add query body for search operations (truncated)
|
|
47
|
+
if body && %w[search msearch].include?(operation)
|
|
48
|
+
span.add_attribute("db.statement", truncate_body(body))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
MiniAPM::Context.with_span(span) do
|
|
52
|
+
begin
|
|
53
|
+
response = super
|
|
54
|
+
|
|
55
|
+
if response
|
|
56
|
+
span.add_attribute("http.status_code", response.status)
|
|
57
|
+
span.set_error("ES #{response.status}") if response.status >= 400
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
response
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
span.record_exception(e)
|
|
63
|
+
raise
|
|
64
|
+
ensure
|
|
65
|
+
span.finish
|
|
66
|
+
MiniAPM.record_span(span)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def extract_operation(method, path)
|
|
74
|
+
case
|
|
75
|
+
when path.include?("_search")
|
|
76
|
+
"search"
|
|
77
|
+
when path.include?("_msearch")
|
|
78
|
+
"msearch"
|
|
79
|
+
when path.include?("_bulk")
|
|
80
|
+
"bulk"
|
|
81
|
+
when path.include?("_count")
|
|
82
|
+
"count"
|
|
83
|
+
when path.include?("_update")
|
|
84
|
+
"update"
|
|
85
|
+
when path.include?("_delete_by_query")
|
|
86
|
+
"delete_by_query"
|
|
87
|
+
when path.include?("_refresh")
|
|
88
|
+
"refresh"
|
|
89
|
+
when method.to_s.upcase == "GET"
|
|
90
|
+
"get"
|
|
91
|
+
when method.to_s.upcase == "PUT"
|
|
92
|
+
"index"
|
|
93
|
+
when method.to_s.upcase == "POST"
|
|
94
|
+
"index"
|
|
95
|
+
when method.to_s.upcase == "DELETE"
|
|
96
|
+
"delete"
|
|
97
|
+
else
|
|
98
|
+
"query"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def extract_index(path)
|
|
103
|
+
# Extract index name from path like /my_index/_search
|
|
104
|
+
parts = path.to_s.split("/").reject { |p| p.empty? || p.start_with?("_") }
|
|
105
|
+
parts.first
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def truncate_body(body)
|
|
109
|
+
json = body.is_a?(String) ? body : body.to_json
|
|
110
|
+
json.length > 1000 ? json[0...1000] + "..." : json
|
|
111
|
+
rescue StandardError
|
|
112
|
+
body.to_s[0...1000]
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Auto-install when loaded
|
|
121
|
+
MiniAPM::Instrumentations::Search::Elasticsearch.install!
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MiniAPM
|
|
4
|
+
module Instrumentations
|
|
5
|
+
module Search
|
|
6
|
+
class OpenSearch
|
|
7
|
+
class << self
|
|
8
|
+
def install!
|
|
9
|
+
return if @installed
|
|
10
|
+
return unless defined?(::OpenSearch::Transport::Client)
|
|
11
|
+
|
|
12
|
+
@installed = true
|
|
13
|
+
::OpenSearch::Transport::Client.prepend(Patch)
|
|
14
|
+
|
|
15
|
+
MiniAPM.logger.debug { "MiniAPM: OpenSearch instrumentation installed" }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def installed?
|
|
19
|
+
@installed || false
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Patch
|
|
24
|
+
def perform_request(method, path, params = {}, body = nil, headers = nil, opts = {})
|
|
25
|
+
return super unless MiniAPM.enabled?
|
|
26
|
+
return super unless MiniAPM::Context.current_trace
|
|
27
|
+
|
|
28
|
+
operation = extract_operation(method, path)
|
|
29
|
+
index = extract_index(path)
|
|
30
|
+
|
|
31
|
+
span = MiniAPM::Span.new(
|
|
32
|
+
name: "OS #{operation}#{index ? " #{index}" : ""}",
|
|
33
|
+
category: :search,
|
|
34
|
+
trace_id: MiniAPM::Context.current_trace_id,
|
|
35
|
+
parent_span_id: MiniAPM::Context.current_span&.span_id,
|
|
36
|
+
attributes: {
|
|
37
|
+
"db.system" => "opensearch",
|
|
38
|
+
"db.operation" => operation,
|
|
39
|
+
"http.method" => method.to_s.upcase,
|
|
40
|
+
"http.url" => path
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
span.add_attribute("opensearch.index", index) if index
|
|
45
|
+
|
|
46
|
+
# Add query body for search operations (truncated)
|
|
47
|
+
if body && %w[search msearch].include?(operation)
|
|
48
|
+
span.add_attribute("db.statement", truncate_body(body))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
MiniAPM::Context.with_span(span) do
|
|
52
|
+
begin
|
|
53
|
+
response = super
|
|
54
|
+
|
|
55
|
+
if response
|
|
56
|
+
span.add_attribute("http.status_code", response.status)
|
|
57
|
+
span.set_error("OS #{response.status}") if response.status >= 400
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
response
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
span.record_exception(e)
|
|
63
|
+
raise
|
|
64
|
+
ensure
|
|
65
|
+
span.finish
|
|
66
|
+
MiniAPM.record_span(span)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def extract_operation(method, path)
|
|
74
|
+
case
|
|
75
|
+
when path.include?("_search")
|
|
76
|
+
"search"
|
|
77
|
+
when path.include?("_msearch")
|
|
78
|
+
"msearch"
|
|
79
|
+
when path.include?("_bulk")
|
|
80
|
+
"bulk"
|
|
81
|
+
when path.include?("_count")
|
|
82
|
+
"count"
|
|
83
|
+
when path.include?("_update")
|
|
84
|
+
"update"
|
|
85
|
+
when path.include?("_delete_by_query")
|
|
86
|
+
"delete_by_query"
|
|
87
|
+
when path.include?("_refresh")
|
|
88
|
+
"refresh"
|
|
89
|
+
when method.to_s.upcase == "GET"
|
|
90
|
+
"get"
|
|
91
|
+
when method.to_s.upcase == "PUT"
|
|
92
|
+
"index"
|
|
93
|
+
when method.to_s.upcase == "POST"
|
|
94
|
+
"index"
|
|
95
|
+
when method.to_s.upcase == "DELETE"
|
|
96
|
+
"delete"
|
|
97
|
+
else
|
|
98
|
+
"query"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def extract_index(path)
|
|
103
|
+
parts = path.to_s.split("/").reject { |p| p.empty? || p.start_with?("_") }
|
|
104
|
+
parts.first
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def truncate_body(body)
|
|
108
|
+
json = body.is_a?(String) ? body : body.to_json
|
|
109
|
+
json.length > 1000 ? json[0...1000] + "..." : json
|
|
110
|
+
rescue StandardError
|
|
111
|
+
body.to_s[0...1000]
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Auto-install when loaded
|
|
120
|
+
MiniAPM::Instrumentations::Search::OpenSearch.install!
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MiniAPM
|
|
4
|
+
module Instrumentations
|
|
5
|
+
module Search
|
|
6
|
+
class Searchkick < Base
|
|
7
|
+
class << self
|
|
8
|
+
def install!
|
|
9
|
+
return if installed?
|
|
10
|
+
return unless defined?(::Searchkick)
|
|
11
|
+
|
|
12
|
+
mark_installed!
|
|
13
|
+
|
|
14
|
+
# Searchkick uses ActiveSupport::Notifications
|
|
15
|
+
subscribe("search.searchkick") do |event|
|
|
16
|
+
handle_search(event)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
subscribe("request.searchkick") do |event|
|
|
20
|
+
handle_request(event)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
subscribe("reindex.searchkick") do |event|
|
|
24
|
+
handle_reindex(event)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
MiniAPM.logger.debug { "MiniAPM: Searchkick instrumentation installed" }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def handle_search(event)
|
|
33
|
+
return unless MiniAPM.enabled?
|
|
34
|
+
return unless Context.current_trace
|
|
35
|
+
|
|
36
|
+
payload = event.payload
|
|
37
|
+
model_name = payload[:name] || "unknown"
|
|
38
|
+
|
|
39
|
+
attributes = {
|
|
40
|
+
"db.system" => "elasticsearch",
|
|
41
|
+
"db.operation" => "search",
|
|
42
|
+
"searchkick.model" => model_name
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Add query info (truncated for privacy)
|
|
46
|
+
if payload[:query]
|
|
47
|
+
query_str = payload[:query].to_s
|
|
48
|
+
attributes["searchkick.query"] = query_str.length > 500 ? query_str[0...500] + "..." : query_str
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Add body for debugging
|
|
52
|
+
if payload[:body]
|
|
53
|
+
body_str = payload[:body].is_a?(String) ? payload[:body] : payload[:body].to_json
|
|
54
|
+
attributes["db.statement"] = body_str.length > 1000 ? body_str[0...1000] + "..." : body_str
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
span = create_span_from_event(
|
|
58
|
+
event,
|
|
59
|
+
name: "searchkick #{model_name}",
|
|
60
|
+
category: :search,
|
|
61
|
+
attributes: attributes
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
record_span(span)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def handle_request(event)
|
|
68
|
+
# Lower-level ES/OS requests from Searchkick
|
|
69
|
+
return unless MiniAPM.enabled?
|
|
70
|
+
return unless Context.current_trace
|
|
71
|
+
|
|
72
|
+
payload = event.payload
|
|
73
|
+
|
|
74
|
+
attributes = {
|
|
75
|
+
"db.system" => "elasticsearch",
|
|
76
|
+
"http.method" => payload[:method]&.to_s&.upcase,
|
|
77
|
+
"http.url" => payload[:path]
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
span = create_span_from_event(
|
|
81
|
+
event,
|
|
82
|
+
name: "searchkick #{payload[:method]} #{payload[:path]}",
|
|
83
|
+
category: :search,
|
|
84
|
+
attributes: attributes
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
record_span(span)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def handle_reindex(event)
|
|
91
|
+
return unless MiniAPM.enabled?
|
|
92
|
+
return unless Context.current_trace
|
|
93
|
+
|
|
94
|
+
payload = event.payload
|
|
95
|
+
model_name = payload[:name] || "unknown"
|
|
96
|
+
|
|
97
|
+
attributes = {
|
|
98
|
+
"db.system" => "elasticsearch",
|
|
99
|
+
"db.operation" => "reindex",
|
|
100
|
+
"searchkick.model" => model_name
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
span = create_span_from_event(
|
|
104
|
+
event,
|
|
105
|
+
name: "searchkick reindex #{model_name}",
|
|
106
|
+
category: :search,
|
|
107
|
+
attributes: attributes
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
record_span(span)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Auto-install when loaded
|
|
119
|
+
MiniAPM::Instrumentations::Search::Searchkick.install!
|