Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add synthetics info header #2203

Merged
merged 17 commits into from Sep 26, 2023
11 changes: 11 additions & 0 deletions lib/new_relic/agent/monitors/synthetics_monitor.rb
Expand Up @@ -6,6 +6,7 @@ module NewRelic
module Agent
class SyntheticsMonitor < InboundRequestMonitor
SYNTHETICS_HEADER_KEY = 'HTTP_X_NEWRELIC_SYNTHETICS'.freeze
SYNTHETICS_INFO_HEADER_KEY = 'HTTP_X_NEWRELIC_SYNTHETICS_INFO'
tannalynn marked this conversation as resolved.
Show resolved Hide resolved

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
fallwith marked this conversation as resolved.
Show resolved Hide resolved

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}")
fallwith marked this conversation as resolved.
Show resolved Hide resolved
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
Comment on lines +86 to +88
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! We have a language support test file. Could you add tests for this method to it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added a test for this here 436f5d2

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the name of this method 🐍


def bundled_gem?(gem_name)
defined?(Bundler) && Bundler.rubygems.all_specs.map(&:name).include?(gem_name)
rescue => e
Expand Down
44 changes: 42 additions & 2 deletions test/new_relic/agent/monitors/synthetics_monitor_test.rb
Expand Up @@ -78,6 +78,46 @@ 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 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 +127,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
15 changes: 15 additions & 0 deletions test/new_relic/agent/transaction_test.rb
Expand Up @@ -644,6 +644,15 @@ def test_is_not_synthetic_request_without_header
end
end

def test_not_synthetics_with_only_info_header
in_transaction do |txn|
txn.raw_synthetics_info_header = '{"version" => 1, "type" => "automatedTest", "initiator" => "cli"}'
txn.synthetics_info_payload = {'version' => 1, 'type' => 'automatedTest', 'initiator' => 'cli'}

refute_predicate txn, :is_synthetics_request?
end
end

def test_is_synthetic_request
in_transaction do |txn|
txn.raw_synthetics_header = ''
Expand Down Expand Up @@ -676,11 +685,17 @@ def test_synthetics_fields_in_finish_event_payload
in_transaction do |txn|
txn.raw_synthetics_header = 'something'
txn.synthetics_payload = [1, 1, 100, 200, 300]
txn.raw_synthetics_info_header = '{"version" => 1, "type" => "automatedTest", "initiator" => "cli", "attributes" => {"batchId" => 42}}'
txn.synthetics_info_payload = {'version' => 1, 'type' => 'automatedTest', 'initiator' => 'cli', 'attributes' => {'batchId' => 42, 'otherAttribute' => 'somethingelse'}}
end

assert_includes keys, :synthetics_resource_id
assert_includes keys, :synthetics_job_id
assert_includes keys, :synthetics_monitor_id
assert_includes keys, :synthetics_type
assert_includes keys, :synthetics_initiator
assert_includes keys, :synthetics_batch_id
assert_includes keys, :synthetics_other_attribute
end

def test_synthetics_fields_not_in_finish_event_payload_if_no_cross_app_calls
Expand Down