# frozen_string_literal: true
require_relative '../../spec_helper'
module Aws
module Plugins
describe 'Client Side Monitoring Plugins' do
ClientMetricsSvc = ApiHelper.sample_rest_xml # S3
let(:stub_publisher) do
StubPublisher.new
end
let(:client) {
client = ClientMetricsSvc::Client.new(
stub_responses: true,
client_side_monitoring_publisher: stub_publisher
)
client.handlers.add(
ClientMetricsPlugin::Handler,
step: :initialize
)
client.handlers.add(
ClientMetricsSendPlugin::LatencyHandler,
step: :sign,
priority: 0
)
client.handlers.add(
ClientMetricsSendPlugin::AttemptHandler,
step: :sign,
priority: 95
)
client
}
let(:env) {{}}
before do
stub_const('ENV', env)
end
before(:each) do
stub_publisher.metrics = []
end
describe 'configuration' do
it 'defaults config.client_side_monitoring to false' do
expect(client.config.client_side_monitoring).not_to be_truthy
end
it 'defaults to an empty string client id' do
expect(client.config.client_side_monitoring_client_id).to eq('')
end
it 'does not include the plugin when client_side_monitoring is false' do
client = ClientMetricsSvc::Client.new(
credentials: Aws::Credentials.new('stub_akid', 'stub_secret'),
region: 'us-stubbed-1',
client_side_monitoring: false
)
expect(client.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsPlugin::Handler
)
expect(client.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsSendPlugin::AttemptHandler
)
expect(client.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsSendPlugin::LatencyHandler
)
end
it 'does not include the plugin if an invalid port is provided' do
client = ClientMetricsSvc::Client.new(
credentials: Aws::Credentials.new('stub_akid', 'stub_secret'),
region: 'us-stubbed-1',
client_side_monitoring: true,
client_side_monitoring_port: nil
)
expect(client.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsPlugin::Handler
)
expect(client.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsSendPlugin::AttemptHandler
)
expect(client.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsSendPlugin::LatencyHandler
)
end
it 'does include the plugins when using the default port and host' do
client = ClientMetricsSvc::Client.new(
credentials: Aws::Credentials.new('stub_akid', 'stub_secret'),
region: 'us-stubbed-1',
client_side_monitoring: true
)
expect(client.config.client_side_monitoring_port).to eq(31000)
expect(client.config.client_side_monitoring_host).to eq('127.0.0.1')
expect(client.handlers.to_a).to include(
Aws::Plugins::ClientMetricsPlugin::Handler
)
expect(client.handlers.to_a).to include(
Aws::Plugins::ClientMetricsSendPlugin::AttemptHandler
)
expect(client.handlers.to_a).to include(
Aws::Plugins::ClientMetricsSendPlugin::LatencyHandler
)
end
it 'accepts client_side_monitoring as an env variable' do
env['AWS_CSM_ENABLED'] = 'fAlSe'
client = ClientMetricsSvc::Client.new(stub_responses: true)
expect(client.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsPlugin
)
expect(client.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsSendPlugin
)
env['AWS_CSM_ENABLED'] = 'F'
client2 = ClientMetricsSvc::Client.new(stub_responses: true)
expect(client2.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsPlugin
)
expect(client2.handlers.to_a).not_to include(
Aws::Plugins::ClientMetricsSendPlugin
)
end
it 'accepts a custom client id' do
env['AWS_CSM_CLIENT_ID'] = 'env-client'
client = ClientMetricsSvc::Client.new(
stub_responses: true,
client_side_monitoring_client_id: 'foo-client'
)
expect(client.config.client_side_monitoring_client_id).to eq(
'foo-client'
)
end
it 'fetches client id from env variables' do
env['AWS_CSM_CLIENT_ID'] = 'env-client'
client = ClientMetricsSvc::Client.new(stub_responses: true)
expect(client.config.client_side_monitoring_client_id).to eq(
'env-client'
)
end
it 'defaults to the agent publisher' do
client = ClientMetricsSvc::Client.new(stub_responses: true)
expect(client.config.client_side_monitoring_publisher).to be_a(
ClientSideMonitoring::Publisher
)
end
end
describe 'ApiCall Metrics' do
it 'collects basic call data' do
client.list_buckets
expect(stub_publisher.metrics.size).to eq(1)
api_call = stub_publisher.metrics[0].api_call
expect(api_call.service).to eq('S3')
expect(api_call.api).to eq('ListBuckets')
expect(api_call.timestamp).to be_a_kind_of(Integer)
expect(api_call.version).to be_a_kind_of(Integer)
expect(api_call.attempt_count).to eq(1)
expect(api_call.latency).to be_a_kind_of(Integer)
expect(api_call.client_id).to eq('')
expect(api_call.region).to eq('us-stubbed-1')
expect(api_call.user_agent).to match(/^aws-sdk-ruby3/)
expect(api_call.final_http_status_code).to eq(200)
end
end
describe 'ApiCallAttempt Metrics' do
it 'collects basic attempt data' do
client.list_buckets
expect(stub_publisher.metrics.size).to eq(1)
request_metrics = stub_publisher.metrics[0]
api_call_attempts = request_metrics.api_call_attempts
expect(api_call_attempts.size).to eq(1)
expect(request_metrics.api_call.max_retries_exceeded).to eq(0)
attempt = api_call_attempts[0]
expect(attempt.service).to eq('S3')
expect(attempt.api).to eq('ListBuckets')
expect(attempt.timestamp).to be_a_kind_of(Integer)
expect(attempt.version).to be_a_kind_of(Integer)
expect(attempt.fqdn).to eq('s3.us-stubbed-1.amazonaws.com')
expect(attempt.region).to eq('us-stubbed-1')
expect(attempt.user_agent).to match(/^aws-sdk-ruby3/)
expect(attempt.access_key).to eq('stubbed-akid')
expect(attempt.http_status_code).to eq(200)
expect(attempt.request_latency).to be_a_kind_of(Integer)
expect(attempt.client_id).to eq('')
end
it 'collects exception information when an error occurs' do
client.stub_responses(:get_object,
{
status_code: 404,
body: "\n"\
"\n"\
"NoSuchKey
\n"\
"The resource you requested does not exist\n"\
"/mybucket/myfoto.jpg\n"\
"4442587FB7D0A2F9\n"\
'',
headers: {}
},
{}
)
expect {
client.get_object(bucket: 'mybucket', key: 'myfoto.jpg')
}.to raise_error('The resource you requested does not exist')
expect(stub_publisher.metrics.size).to eq(1)
request_metrics = stub_publisher.metrics[0]
api_call_attempts = request_metrics.api_call_attempts
expect(request_metrics.api_call.max_retries_exceeded).to eq(0)
expect(request_metrics.api_call.final_aws_exception).to eq(
'NoSuchKey'
)
expect(request_metrics.api_call.final_aws_exception_message).to eq(
'The resource you requested does not exist'
)
expect(api_call_attempts.size).to eq(1)
attempt = api_call_attempts[0]
expect(attempt.aws_exception).to eq('NoSuchKey')
expect(attempt.aws_exception_msg).to eq(
'The resource you requested does not exist'
)
end
it 'collects request ID headers when available' do
client.stub_responses(:get_object,
{
status_code: 404,
body: "\n"\
"\n"\
"NoSuchKey
\n"\
"The resource you requested does not exist\n"\
"/mybucket/myfoto.jpg\n"\
"4442587FB7D0A2F9\n"\
'',
headers: {
'x-amz-id-2' => 'fWhd+V0u5IWKNLhbIZi2ZR/DoWpAt2Km8T9ZZ75UnvkZFl0MU3jlf2B2zRJYHmxqkEc6iAtctOc=',
'x-amz-request-id' => '226FC0DC6464C2AE'
}
},
{}
)
expect {
client.get_object(bucket: 'mybucket', key: 'myfoto.jpg')
}.to raise_error('The resource you requested does not exist')
expect(stub_publisher.metrics.size).to eq(1)
request_metrics = stub_publisher.metrics[0]
api_call_attempts = request_metrics.api_call_attempts
expect(api_call_attempts.size).to eq(1)
expect(request_metrics.api_call.max_retries_exceeded).to eq(0)
attempt = api_call_attempts[0]
expect(attempt.x_amz_id_2).to eq('fWhd+V0u5IWKNLhbIZi2ZR/DoWpAt2Km8T9ZZ75UnvkZFl0MU3jlf2B2zRJYHmxqkEc6iAtctOc=')
expect(attempt.x_amz_request_id).to eq('226FC0DC6464C2AE')
end
describe 'failures without network requests' do
let(:failure_client) {
client = ClientMetricsSvc::Client.new(
stub_responses: true,
client_side_monitoring_publisher: stub_publisher
)
client.handlers.add(
ClientMetricsPlugin::Handler,
step: :initialize
)
client.handlers.add(
ClientMetricsSendPlugin::LatencyHandler,
step: :sign,
priority: 0
)
client.handlers.add(
ClientMetricsSendPlugin::AttemptHandler,
step: :sign,
priority: 95
)
client.handlers.add(
FailureInjectionHandler,
step: :validate,
priority: 50
)
client
}
it 'correctly publishes metrics for a validation error' do
expect {
failure_client.list_buckets
}.to raise_error(ArgumentError, 'Injected exception.')
expect(stub_publisher.metrics.size).to eq(1)
request_metrics = stub_publisher.metrics[0]
api_call_attempts = request_metrics.api_call_attempts
expect(request_metrics.api_call.attempt_count).to eq(1)
expect(request_metrics.api_call.max_retries_exceeded).to eq(0)
expect(api_call_attempts.size).to eq(1)
attempt = api_call_attempts[0]
expect(request_metrics.api_call.final_sdk_exception).to eq(
'ArgumentError'
)
expect(request_metrics.api_call.final_sdk_exception_message).to eq(
'Injected exception.'
)
expect(attempt.sdk_exception).to eq('ArgumentError')
expect(attempt.sdk_exception_msg).to eq('Injected exception.')
end
end
describe 'failures without network requests' do
let(:failure_client) {
client = ClientMetricsSvc::Client.new(
stub_responses: true,
client_side_monitoring_publisher: stub_publisher
)
client.handlers.add(
ClientMetricsPlugin::Handler,
step: :initialize
)
client.handlers.add(
ClientMetricsSendPlugin::LatencyHandler,
step: :sign,
priority: 0
)
client.handlers.add(
ClientMetricsSendPlugin::AttemptHandler,
step: :sign,
priority: 95
)
client.handlers.add(
ResponseFailureHandler,
step: :validate,
priority: 50
)
client
}
it 'accounts for failures during response handling' do
expect {
failure_client.list_buckets
}.to raise_error(ArgumentError, 'Bad response.')
expect(stub_publisher.metrics.size).to eq(1)
request_metrics = stub_publisher.metrics[0]
api_call_attempts = request_metrics.api_call_attempts
expect(request_metrics.api_call.attempt_count).to eq(1)
expect(request_metrics.api_call.max_retries_exceeded).to eq(0)
expect(api_call_attempts.size).to eq(1)
attempt = api_call_attempts[0]
expect(request_metrics.api_call.final_sdk_exception).to eq(
'ArgumentError'
)
expect(request_metrics.api_call.final_sdk_exception_message).to eq(
'Bad response.'
)
expect(attempt.sdk_exception).to eq('ArgumentError')
expect(attempt.sdk_exception_msg).to eq('Bad response.')
end
it 'can handle sdk exceptions and aws exceptions together' do
failure_client.stub_responses(:get_object,
{
status_code: 404,
body: "\n"\
"\n"\
"NoSuchKey
\n"\
"The resource you requested does not exist\n"\
"/mybucket/myfoto.jpg\n"\
"4442587FB7D0A2F9\n"\
'',
headers: {}
},
{}
)
expect {
failure_client.get_object(bucket: 'mybucket', key: 'myfoto.jpg')
}.to raise_error(ArgumentError, 'Bad response.')
expect(stub_publisher.metrics.size).to eq(1)
request_metrics = stub_publisher.metrics[0]
api_call_attempts = request_metrics.api_call_attempts
expect(request_metrics.api_call.attempt_count).to eq(1)
expect(request_metrics.api_call.max_retries_exceeded).to eq(0)
expect(api_call_attempts.size).to eq(1)
attempt = api_call_attempts[0]
expect(request_metrics.api_call.final_aws_exception).to eq(
'NoSuchKey'
)
expect(request_metrics.api_call.final_aws_exception_message).to eq(
'The resource you requested does not exist'
)
expect(request_metrics.api_call.final_sdk_exception).to eq(
'ArgumentError'
)
expect(request_metrics.api_call.final_sdk_exception_message).to eq(
'Bad response.'
)
expect(attempt.aws_exception).to eq('NoSuchKey')
expect(attempt.aws_exception_msg).to eq(
'The resource you requested does not exist'
)
expect(attempt.sdk_exception).to eq('ArgumentError')
expect(attempt.sdk_exception_msg).to eq('Bad response.')
end
it 'recognizes retryable exceptions' do
client.stub_responses(:get_object,
{
status_code: 500,
body: "\n"\
"\n"\
"InternalServiceError
\n"\
"Fake internal service error.\n"\
"/mybucket/myfoto.jpg\n"\
"4442587FB7D0A2F9\n"\
'',
headers: {}
},
{}
)
expect {
client.get_object(bucket: 'mybucket', key: 'myfoto.jpg')
}.to raise_error('Fake internal service error.')
expect(stub_publisher.metrics.size).to eq(1)
request_metrics = stub_publisher.metrics[0]
api_call_attempts = request_metrics.api_call_attempts
expect(api_call_attempts.size).to eq(1)
expect(request_metrics.api_call.max_retries_exceeded).to eq(1)
end
end
end
end
class StubPublisher
attr_accessor :metrics
attr_accessor :agent_port
attr_accessor :agent_host
def initialize
@metrics = []
end
def publish(request_metrics)
@metrics << request_metrics
end
end
class FailureInjectionHandler < Seahorse::Client::Handler
def call(context)
raise ArgumentError, 'Injected exception.'
end
end
class ResponseFailureHandler < Seahorse::Client::Handler
def call(context)
@handler.call(context)
raise ArgumentError, 'Bad response.'
end
end
end
end