Skip to content

Commit

Permalink
Merge pull request #2203 from newrelic/add_synthetics_info_header
Browse files Browse the repository at this point in the history
Add synthetics info header
  • Loading branch information
tannalynn committed Sep 26, 2023
2 parents 1a30b8c + 15c7685 commit 10846a6
Show file tree
Hide file tree
Showing 13 changed files with 195 additions and 9 deletions.
7 changes: 6 additions & 1 deletion CHANGELOG.md
Expand Up @@ -2,12 +2,17 @@

## dev

Version <dev> brings support for gleaning a Docker container id from cgroups v2 based containers.
Version <dev> brings support for gleaning a Docker container id from cgroups v2 based containers and records additional synthetics attributes.

- **Feature: Enhance Docker container id reporting**

Previously, the agent was only capable of determining a host Docker container's ID if the container was based on cgroups v1. Now, containers based on cgroups v2 will also have their container IDs reported to New Relic. [PR#2229](https://github.com/newrelic/newrelic-ruby-agent/issues/2229).

- **Feature: Update events with additional synthetics attributes when available**

The agent will now record additional synthetics attributes on synthetics events if these attributes are available. [PR#2203](https://github.com/newrelic/newrelic-ruby-agent/pull/2203)


## v9.5.0

Version 9.5.0 introduces Stripe instrumentation, allows the agent to record additional response information on a transaction when middleware instrumentation is disabled, introduces new `:'sidekiq.args.include'` and `:'sidekiq.args.exclude:` configuration options to permit capturing only certain Sidekiq job arguments, updates Elasticsearch datastore instance metrics, and fixes a bug in `NewRelic::Rack::AgentHooks.needed?`.
Expand Down
13 changes: 12 additions & 1 deletion lib/new_relic/agent/monitors/synthetics_monitor.rb
Expand Up @@ -5,7 +5,8 @@
module NewRelic
module Agent
class SyntheticsMonitor < InboundRequestMonitor
SYNTHETICS_HEADER_KEY = 'HTTP_X_NEWRELIC_SYNTHETICS'.freeze
SYNTHETICS_HEADER_KEY = 'HTTP_X_NEWRELIC_SYNTHETICS'
SYNTHETICS_INFO_HEADER_KEY = 'HTTP_X_NEWRELIC_SYNTHETICS_INFO'

SUPPORTED_VERSION = 1
EXPECTED_PAYLOAD_LENGTH = 5
Expand All @@ -16,6 +17,7 @@ def on_finished_configuring(events)

def on_before_call(request) # THREAD_LOCAL_ACCESS
encoded_header = request[SYNTHETICS_HEADER_KEY]
info_header = request[SYNTHETICS_INFO_HEADER_KEY]
return unless encoded_header

incoming_payload = deserialize_header(encoded_header, SYNTHETICS_HEADER_KEY)
Expand All @@ -27,7 +29,16 @@ def on_before_call(request) # THREAD_LOCAL_ACCESS

txn = Tracer.current_transaction
txn.raw_synthetics_header = encoded_header
txn.raw_synthetics_info_header = info_header
txn.synthetics_payload = incoming_payload
txn.synthetics_info_payload = load_json(info_header, SYNTHETICS_INFO_HEADER_KEY)
end

def load_json(header, key)
::JSON.load(header)
rescue => err
NewRelic::Agent.logger.debug("Failure loading json header '#{key}' in #{self.class}, #{err.class}, #{err.message}")
nil
end

class << self
Expand Down
25 changes: 24 additions & 1 deletion lib/new_relic/agent/transaction.rb
Expand Up @@ -90,7 +90,7 @@ class Transaction
attr_reader :transaction_trace

# Fields for tracking synthetics requests
attr_accessor :raw_synthetics_header, :synthetics_payload
attr_accessor :raw_synthetics_header, :synthetics_payload, :synthetics_info_payload, :raw_synthetics_info_header

# Return the currently active transaction, or nil.
def self.tl_current
Expand Down Expand Up @@ -623,11 +623,24 @@ def assign_intrinsics
attributes.add_intrinsic_attribute(:synthetics_resource_id, synthetics_resource_id)
attributes.add_intrinsic_attribute(:synthetics_job_id, synthetics_job_id)
attributes.add_intrinsic_attribute(:synthetics_monitor_id, synthetics_monitor_id)
attributes.add_intrinsic_attribute(:synthetics_type, synthetics_info('type'))
attributes.add_intrinsic_attribute(:synthetics_initiator, synthetics_info('initiator'))

synthetics_additional_attributes do |key, value|
attributes.add_intrinsic_attribute(key, value)
end
end

distributed_tracer.assign_intrinsics
end

def synthetics_additional_attributes(&block)
synthetics_info('attributes')&.each do |k, v|
new_key = "synthetics_#{NewRelic::LanguageSupport.snakeize(k.to_s)}".to_sym
yield(new_key, v.to_s)
end
end

def calculate_gc_time
gc_stop_snapshot = NewRelic::Agent::StatsEngine::GCProfiler.take_snapshot
NewRelic::Agent::StatsEngine::GCProfiler.record_delta(gc_start_snapshot, gc_stop_snapshot)
Expand Down Expand Up @@ -707,6 +720,10 @@ def synthetics_monitor_id
info[4]
end

def synthetics_info(key)
synthetics_info_payload[key] if synthetics_info_payload
end

def append_apdex_perf_zone(payload)
if recording_web_transaction?
bucket = apdex_bucket(duration, apdex_t)
Expand All @@ -730,6 +747,12 @@ def append_synthetics_to(payload)
payload[:synthetics_resource_id] = synthetics_resource_id
payload[:synthetics_job_id] = synthetics_job_id
payload[:synthetics_monitor_id] = synthetics_monitor_id
payload[:synthetics_type] = synthetics_info('type')
payload[:synthetics_initiator] = synthetics_info('initiator')

synthetics_additional_attributes do |key, value|
payload[key] = value
end
end

def merge_metrics
Expand Down
7 changes: 5 additions & 2 deletions lib/new_relic/agent/transaction/external_request_segment.rb
Expand Up @@ -14,6 +14,7 @@ class Transaction
# @api public
class ExternalRequestSegment < Segment
NR_SYNTHETICS_HEADER = 'X-NewRelic-Synthetics'
NR_SYNTHETICS_INFO_HEADER = 'X-NewRelic-Synthetics-Info'
APP_DATA_KEY = 'NewRelicAppData'

EXTERNAL_ALL = 'External/all'
Expand Down Expand Up @@ -63,7 +64,8 @@ def record_agent_attributes?
def add_request_headers(request)
process_host_header(request)
synthetics_header = transaction&.raw_synthetics_header
insert_synthetics_header(request, synthetics_header) if synthetics_header
synthetics_info_header = transaction&.raw_synthetics_info_header
insert_synthetics_header(request, synthetics_header, synthetics_info_header) if synthetics_header

return unless record_metrics?

Expand Down Expand Up @@ -207,8 +209,9 @@ def set_http_status_code(response)
end
end

def insert_synthetics_header(request, header)
def insert_synthetics_header(request, header, info)
request[NR_SYNTHETICS_HEADER] = header
request[NR_SYNTHETICS_INFO_HEADER] = info if info
end

def segment_complete
Expand Down
16 changes: 16 additions & 0 deletions lib/new_relic/agent/transaction_error_primitive.rb
Expand Up @@ -31,9 +31,14 @@ module TransactionErrorPrimitive
SYNTHETICS_RESOURCE_ID_KEY = 'nr.syntheticsResourceId'.freeze
SYNTHETICS_JOB_ID_KEY = 'nr.syntheticsJobId'.freeze
SYNTHETICS_MONITOR_ID_KEY = 'nr.syntheticsMonitorId'.freeze
SYNTHETICS_TYPE_KEY = 'nr.syntheticsType'
SYNTHETICS_INITIATOR_KEY = 'nr.syntheticsInitiator'
SYNTHETICS_KEY_PREFIX = 'nr.synthetics'
PRIORITY_KEY = 'priority'.freeze
SPAN_ID_KEY = 'spanId'.freeze

SYNTHETICS_PAYLOAD_EXPECTED = [:synthetics_resource_id, :synthetics_job_id, :synthetics_monitor_id, :synthetics_type, :synthetics_initiator]

def create(noticed_error, payload, span_id)
[
intrinsic_attributes_for(noticed_error, payload, span_id),
Expand Down Expand Up @@ -71,9 +76,20 @@ def intrinsic_attributes_for(noticed_error, payload, span_id)
end

def append_synthetics(payload, sample)
return unless payload[:synthetics_job_id]

sample[SYNTHETICS_RESOURCE_ID_KEY] = payload[:synthetics_resource_id] if payload[:synthetics_resource_id]
sample[SYNTHETICS_JOB_ID_KEY] = payload[:synthetics_job_id] if payload[:synthetics_job_id]
sample[SYNTHETICS_MONITOR_ID_KEY] = payload[:synthetics_monitor_id] if payload[:synthetics_monitor_id]
sample[SYNTHETICS_TYPE_KEY] = payload[:synthetics_type] if payload[:synthetics_type]
sample[SYNTHETICS_INITIATOR_KEY] = payload[:synthetics_initiator] if payload[:synthetics_initiator]

payload.each do |k, v|
next unless k.to_s.start_with?('synthetics_') && !SYNTHETICS_PAYLOAD_EXPECTED.include?(k)

new_key = SYNTHETICS_KEY_PREFIX + NewRelic::LanguageSupport.camelize(k.to_s.gsub('synthetics_', ''))
sample[new_key] = v
end
end

def append_cat(payload, sample)
Expand Down
19 changes: 19 additions & 0 deletions lib/new_relic/agent/transaction_event_primitive.rb
Expand Up @@ -38,6 +38,11 @@ module TransactionEventPrimitive
SYNTHETICS_RESOURCE_ID_KEY = 'nr.syntheticsResourceId'
SYNTHETICS_JOB_ID_KEY = 'nr.syntheticsJobId'
SYNTHETICS_MONITOR_ID_KEY = 'nr.syntheticsMonitorId'
SYNTHETICS_TYPE_KEY = 'nr.syntheticsType'
SYNTHETICS_INITIATOR_KEY = 'nr.syntheticsInitiator'
SYNTHETICS_KEY_PREFIX = 'nr.synthetics'

SYNTHETICS_PAYLOAD_EXPECTED = [:synthetics_resource_id, :synthetics_job_id, :synthetics_monitor_id, :synthetics_type, :synthetics_initiator]

def create(payload)
intrinsics = {
Expand Down Expand Up @@ -71,9 +76,23 @@ def append_optional_attributes(sample, payload)
optionally_append(SYNTHETICS_RESOURCE_ID_KEY, :synthetics_resource_id, sample, payload)
optionally_append(SYNTHETICS_JOB_ID_KEY, :synthetics_job_id, sample, payload)
optionally_append(SYNTHETICS_MONITOR_ID_KEY, :synthetics_monitor_id, sample, payload)
optionally_append(SYNTHETICS_TYPE_KEY, :synthetics_type, sample, payload)
optionally_append(SYNTHETICS_INITIATOR_KEY, :synthetics_initiator, sample, payload)
append_synthetics_info_attributes(sample, payload)
append_cat_alternate_path_hashes(sample, payload)
end

def append_synthetics_info_attributes(sample, payload)
return unless payload.include?(:synthetics_job_id)

payload.each do |k, v|
next unless k.to_s.start_with?('synthetics_') && !SYNTHETICS_PAYLOAD_EXPECTED.include?(k)

new_key = SYNTHETICS_KEY_PREFIX + NewRelic::LanguageSupport.camelize(k.to_s.gsub('synthetics_', ''))
sample[new_key] = v.to_s
end
end

def append_cat_alternate_path_hashes(sample, payload)
if payload.include?(:cat_alternate_path_hashes)
sample[CAT_ALTERNATE_PATH_HASHES_KEY] = payload[:cat_alternate_path_hashes].sort.join(COMMA)
Expand Down
4 changes: 4 additions & 0 deletions lib/new_relic/language_support.rb
Expand Up @@ -83,6 +83,10 @@ def camelize_with_first_letter_downcased(string)
camelized[0].downcase.concat(camelized[1..-1])
end

def snakeize(string)
string.gsub(/(.)([A-Z])/, '\1_\2').downcase
end

def bundled_gem?(gem_name)
defined?(Bundler) && Bundler.rubygems.all_specs.map(&:name).include?(gem_name)
rescue => e
Expand Down
70 changes: 68 additions & 2 deletions test/new_relic/agent/monitors/synthetics_monitor_test.rb
Expand Up @@ -78,6 +78,72 @@ def test_records_synthetics_state_from_header
end
end

def test_records_synthetics_info_header_if_available
key = SyntheticsMonitor::SYNTHETICS_HEADER_KEY
synthetics_payload = [VERSION_ID] + STANDARD_DATA
info_payload = <<~PAYLOAD
{
"version": "1",
"type": "automatedTest",
"initiator": "cli",
"attributes": {
"attribute1": "one"
}
}
PAYLOAD

expected_info = {
'version' => '1',
'type' => 'automatedTest',
'initiator' => 'cli',
'attributes' => {
'attribute1' => 'one'
}
}

with_synthetics_headers(synthetics_payload, headers: both_synthetics_headers(synthetics_payload, info_payload)) do
txn = NewRelic::Agent::Tracer.current_transaction

assert_equal @last_encoded_header, txn.raw_synthetics_header
assert_equal synthetics_payload, txn.synthetics_payload
assert_equal info_payload, txn.raw_synthetics_info_header
assert_equal expected_info, txn.synthetics_info_payload
end
end

def test_load_json
info_payload = <<~PAYLOAD
{
"version": "1",
"type": "automatedTest",
"initiator": "cli",
"attributes": {
"attribute1": "one"
}
}
PAYLOAD

expected_info = {
'version' => '1',
'type' => 'automatedTest',
'initiator' => 'cli',
'attributes' => {
'attribute1' => 'one'
}
}

loaded = NewRelic::Agent::SyntheticsMonitor.new(@events).load_json(info_payload, 'info-header')

assert_equal expected_info, loaded
end

def both_synthetics_headers(payload, info_payload)
header_info_key = SyntheticsMonitor::SYNTHETICS_INFO_HEADER_KEY
synthetics_header(payload).merge({
header_info_key => info_payload
})
end

def synthetics_header(payload, header_key = SyntheticsMonitor::SYNTHETICS_HEADER_KEY)
@last_encoded_header = json_dump_and_encode(payload)
{header_key => @last_encoded_header}
Expand All @@ -87,9 +153,9 @@ def assert_no_synthetics_payload
assert_nil NewRelic::Agent::Tracer.current_transaction.synthetics_payload
end

def with_synthetics_headers(payload, header_key = SyntheticsMonitor::SYNTHETICS_HEADER_KEY)
def with_synthetics_headers(payload, header_key = SyntheticsMonitor::SYNTHETICS_HEADER_KEY, headers: nil)
in_transaction do
@events.notify(:before_call, synthetics_header(payload, header_key))
@events.notify(:before_call, headers || synthetics_header(payload, header_key))
yield
end
end
Expand Down
Expand Up @@ -366,10 +366,12 @@ def test_segment_writes_outbound_request_headers_for_trace_context
end

def test_segment_writes_synthetics_header_for_synthetics_txn
synthetics_info_header = {'version' => '1', 'type' => 'automatedTest', 'initiator' => 'cli', 'attributes' => {'attribute1' => 'one'}}
request = RequestWrapper.new
with_config(cat_config) do
in_transaction(:category => :controller) do |txn|
txn.raw_synthetics_header = json_dump_and_encode([1, 42, 100, 200, 300])
txn.raw_synthetics_info_header = synthetics_info_header
segment = Tracer.start_external_request_segment(
library: 'Net::HTTP',
uri: 'http://remotehost.com/blogs/index',
Expand All @@ -381,6 +383,8 @@ def test_segment_writes_synthetics_header_for_synthetics_txn
end

assert request.headers.key?('X-NewRelic-Synthetics'), 'Expected to find X-NewRelic-Synthetics header'
assert request.headers.key?('X-NewRelic-Synthetics-Info'), 'Expected to find X-NewRelic-Synthetics-Info header'
assert_equal request.headers['X-NewRelic-Synthetics-Info'], synthetics_info_header
end

def test_add_request_headers_renames_segment_based_on_host_header
Expand Down
9 changes: 8 additions & 1 deletion test/new_relic/agent/transaction_error_primitive_test.rb
Expand Up @@ -42,12 +42,19 @@ def test_event_includes_synthetics
intrinsics, *_ = create_event(:payload_options => {
:synthetics_resource_id => 3,
:synthetics_job_id => 4,
:synthetics_monitor_id => 5
:synthetics_monitor_id => 5,
:synthetics_type => 'automatedTest',
:synthetics_initiator => 'cli',
:synthetics_batch_id => 42
})

assert_equal 3, intrinsics['nr.syntheticsResourceId']
assert_equal 4, intrinsics['nr.syntheticsJobId']
assert_equal 5, intrinsics['nr.syntheticsMonitorId']

assert_equal 'automatedTest', intrinsics['nr.syntheticsType']
assert_equal 'cli', intrinsics['nr.syntheticsInitiator']
assert_equal 42, intrinsics['nr.syntheticsBatchId']
end

def test_includes_mapped_metrics
Expand Down
9 changes: 8 additions & 1 deletion test/new_relic/agent/transaction_event_primitive_test.rb
Expand Up @@ -29,14 +29,21 @@ def test_event_includes_synthetics
payload = generate_payload('whatever', {
:synthetics_resource_id => 3,
:synthetics_job_id => 4,
:synthetics_monitor_id => 5
:synthetics_monitor_id => 5,
:synthetics_type => 'automatedTest',
:synthetics_initiator => 'cli',
:synthetics_batch_id => 42
})

intrinsics, *_ = TransactionEventPrimitive.create(payload)

assert_equal '3', intrinsics['nr.syntheticsResourceId']
assert_equal '4', intrinsics['nr.syntheticsJobId']
assert_equal '5', intrinsics['nr.syntheticsMonitorId']

assert_equal 'automatedTest', intrinsics['nr.syntheticsType']
assert_equal 'cli', intrinsics['nr.syntheticsInitiator']
assert_equal '42', intrinsics['nr.syntheticsBatchId']
end

def test_custom_attributes_in_event_are_normalized_to_string_keys
Expand Down

0 comments on commit 10846a6

Please sign in to comment.