Skip to content

Commit

Permalink
Merge pull request #2796 from newrelic/opensearch-instrumentation
Browse files Browse the repository at this point in the history
OpenSearch instrumentation
  • Loading branch information
kaylareopelle committed Aug 13, 2024
2 parents 4f71983 + 35b1090 commit 9009a39
Show file tree
Hide file tree
Showing 10 changed files with 413 additions and 2 deletions.
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

## dev

Version <dev> updates framework detection, fixes Falcon dispatcher detection, and addresses a JRuby specific concurrency issue.
Version <dev> adds experimental OpenSearch instrumentation, updates framework detection, fixes Falcon dispatcher detection, and addresses a JRuby specific concurrency issue.

- **Feature: Add experimental OpenSearch instrumentation**

The agent will now automatically instrument the `opensearch-ruby` gem. We're marking this instrumentation as experimental because more work is needed to fully test it. OpenSearch instrumentation provides telemetry similar to Elasticsearch. Thank you, [@Earlopain](https://github.com/Earlopain) for reporting the issue and [@praveen-ks](https://github.com/praveen-ks) for an initial draft of the instrumentation. [Issue#2228](https://github.com/newrelic/newrelic-ruby-agent/issues/2228) [PR#2796](https://github.com/newrelic/newrelic-ruby-agent/pull/2796)

- **Feature: Improve framework detection accuracy for Grape and Padrino**

Expand Down
13 changes: 12 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: "3.9"
services:
elasticsearch7:
image: elasticsearch:7.16.2
Expand All @@ -20,6 +19,17 @@ services:
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
mem_limit: 1g
# this docker-compose service doesn't always work with "docker compose run opensearch"
# the tests consistently work with:
# docker run -it -p 9200:9200 -p 9600:9600 -e "discovery.type=single-node" --name opensearch-node opensearchproject/opensearch:1.3.18
# opensearch:
# image: opensearchproject/opensearch:1.3.18
# environment:
# - discovery.type=single-node
# ports:
# - 9200:9200
# - 9600:9600
# mem_limit: 1g
mysql:
image: mysql:5.7
platform: linux/x86_64
Expand Down Expand Up @@ -91,6 +101,7 @@ services:
depends_on:
- elasticsearch7
- elasticsearch8
- opensearch
- mysql
- memcached
- mongodb
Expand Down
24 changes: 24 additions & 0 deletions lib/new_relic/agent/configuration/default_source.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1463,6 +1463,15 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil)
:allowed_from_server => false,
:description => 'Controls auto-instrumentation of bunny at start-up. May be one of: `auto`, `prepend`, `chain`, `disabled`.'
},
:'instrumentation.opensearch' => {
:default => 'auto',
:documentation_default => 'auto',
:public => true,
:type => String,
:dynamic_name => true,
:allowed_from_server => false,
:description => 'Controls auto-instrumentation of the opensearch-ruby library at start-up. May be one of `auto`, `prepend`, `chain`, `disabled`.'
},
:'instrumentation.aws_sqs' => {
:default => 'auto',
:public => true,
Expand Down Expand Up @@ -1870,6 +1879,21 @@ def self.enforce_fallback(allowed_values: nil, fallback: nil)
:allowed_from_server => true,
:description => 'If `true`, the agent obfuscates Mongo queries in transaction traces.'
},
# OpenSearch
:'opensearch.capture_queries' => {
:default => true,
:public => true,
:type => Boolean,
:allowed_from_server => true,
:description => 'If `true`, the agent captures OpenSearch queries in transaction traces.'
},
:'opensearch.obfuscate_queries' => {
:default => true,
:public => true,
:type => Boolean,
:allowed_from_server => true,
:description => 'If `true`, the agent obfuscates OpenSearch queries in transaction traces.'
},
# Process host
:'process_host.display_name' => {
:default => proc { NewRelic::Agent::Hostname.get },
Expand Down
25 changes: 25 additions & 0 deletions lib/new_relic/agent/instrumentation/opensearch.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

require_relative 'opensearch/instrumentation'
require_relative 'opensearch/chain'
require_relative 'opensearch/prepend'

DependencyDetection.defer do
named :opensearch

depends_on do
defined?(OpenSearch)
end

executes do
NewRelic::Agent.logger.info('Installing opensearch-ruby instrumentation')

if use_prepend?
prepend_instrument OpenSearch::Transport::Client, NewRelic::Agent::Instrumentation::OpenSearch::Prepend
else
chain_instrument NewRelic::Agent::Instrumentation::OpenSearch::Chain
end
end
end
21 changes: 21 additions & 0 deletions lib/new_relic/agent/instrumentation/opensearch/chain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

module NewRelic::Agent::Instrumentation
module OpenSearch::Chain
def self.instrument!
::OpenSearch::Transport::Client.class_eval do
include NewRelic::Agent::Instrumentation::OpenSearch

alias_method(:perform_request_without_tracing, :perform_request)

def perform_request(*args)
perform_request_with_tracing(*args) do
perform_request_without_tracing(*args)
end
end
end
end
end
end
66 changes: 66 additions & 0 deletions lib/new_relic/agent/instrumentation/opensearch/instrumentation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

module NewRelic::Agent::Instrumentation
module OpenSearch
PRODUCT_NAME = 'OpenSearch'
OPERATION = 'perform_request'
OPERATION_PATTERN = %r{/lib/opensearch/api/(?!.+#{OPERATION})}
INSTANCE_METHOD_PATTERN = /:in (?:`|')(?:.+#)?([^']+)'\z/
INSTRUMENTATION_NAME = NewRelic::Agent.base_name(name)

def perform_request_with_tracing(_method, _path, params = NewRelic::EMPTY_HASH, body = nil, _headers = nil, _opts = nil, &_block)
return yield unless NewRelic::Agent::Tracer.tracing_enabled?

segment = NewRelic::Agent::Tracer.start_datastore_segment(
product: PRODUCT_NAME,
operation: nr_operation || OPERATION,
host: nr_hosts[:host],
port_path_or_id: nr_hosts[:port],
database_name: nr_cluster_name
)
begin
NewRelic::Agent::Tracer.capture_segment_error(segment) { yield }
ensure
if segment
segment.notice_nosql_statement(nr_reported_query(body || params))
segment.finish
end
end
end

private

# See Elasticsearch instrumentation for explanation on Ruby 3.4 changes to match instance method
def nr_operation
location = caller_locations.detect { |loc| loc.to_s.match?(OPERATION_PATTERN) }
return unless location && location.to_s =~ INSTANCE_METHOD_PATTERN

Regexp.last_match(1)
end

def nr_reported_query(query)
return unless NewRelic::Agent.config[:'opensearch.capture_queries']
return query unless NewRelic::Agent.config[:'opensearch.obfuscate_queries']

NewRelic::Agent::Datastores::NosqlObfuscator.obfuscate_statement(query)
end

def nr_cluster_name
return @nr_cluster_name if defined?(@nr_cluster_name)
return if nr_hosts.empty?

NewRelic::Agent.disable_all_tracing do
@nr_cluster_name ||= perform_request('GET', '/').body['cluster_name']
end
rescue StandardError => e
NewRelic::Agent.logger.error('Failed to get cluster name for OpenSearch', e)
nil
end

def nr_hosts
@nr_hosts ||= (transport.hosts.first || NewRelic::EMPTY_HASH)
end
end
end
13 changes: 13 additions & 0 deletions lib/new_relic/agent/instrumentation/opensearch/prepend.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

module NewRelic::Agent::Instrumentation
module OpenSearch::Prepend
include NewRelic::Agent::Instrumentation::OpenSearch

def perform_request(*args)
perform_request_with_tracing(*args) { super }
end
end
end
24 changes: 24 additions & 0 deletions test/multiverse/suites/opensearch/Envfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true

suite_condition('Skip OpenSearch on GitHub Actions, service not currently available') do
ENV['CI'] != 'true'
end

instrumentation_methods :chain, :prepend

OPENSEARCH_VERSIONS = [
[nil, 2.5],
['3.4.0', 2.5],
['2.1.0', 2.4]
]

def gem_list(opensearch_version = nil)
<<~RB
gem 'opensearch-ruby'#{opensearch_version}
RB
end

create_gemfiles(OPENSEARCH_VERSIONS)

19 changes: 19 additions & 0 deletions test/multiverse/suites/opensearch/config/newrelic.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
development:
error_collector:
enabled: true
apdex_t: 0.5
monitor_mode: true
license_key: bootstrap_newrelic_admin_license_key_000
instrumentation:
opensearch: <%= $instrumentation_method %>
app_name: test
log_level: debug
host: 127.0.0.1
api_host: 127.0.0.1
transaction_trace:
record_sql: obfuscated
enabled: true
stack_trace_threshold: 0.5
transaction_threshold: 1.0
capture_params: false
Loading

0 comments on commit 9009a39

Please sign in to comment.