spec/integration/rails_spec.rb (304 lines of code) (raw):

# Licensed to Elasticsearch B.V. under one or more contributor # license agreements. See the NOTICE file distributed with # this work for additional information regarding copyright # ownership. Elasticsearch B.V. licenses this file to you under # the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. # frozen_string_literal: true require 'integration_helper' if defined?(Rails) enabled = true else puts '[INFO] Skipping Rails spec' end if enabled module MetricsHelpers def transaction_metrics @mock_intake.metricsets.select do |set| set && set['transaction'] && !set['span'] end end def span_metrics @mock_intake.metricsets.select do |set| set && set['transaction'] && set['span'] end end end require 'action_controller/railtie' require 'action_mailer/railtie' RSpec.describe 'Rails integration', :allow_running_agent, :spec_logger, :mock_intake do include Rack::Test::Methods include MetricsHelpers let(:app) do Rails.application end # Add some padding to make sure requests have settled down between examples before { sleep 0.25 } after { sleep 0.25 } after :all do ElasticAPM.stop ElasticAPM::Transport::Worker.adapter = nil end before :all do module RailsTestApp class Application < Rails::Application RailsTestHelpers.setup_rails_test_config(config) config.log_level = :debug config.elastic_apm.api_request_time = '200ms' config.elastic_apm.capture_body = 'all' config.elastic_apm.disable_start_message = true config.elastic_apm.log_path = 'spec/elastic_apm.log' config.elastic_apm.metrics_interval = '2s' config.elastic_apm.cloud_provider = 'none' config.elastic_apm.pool_size = Concurrent.processor_count config.elastic_apm.transaction_ignore_urls = '/ping' end end class ApplicationController < ActionController::Base class FancyError < ::StandardError; end before_action do ElasticAPM.set_user(current_user) end http_basic_authenticate_with( name: 'dhh', password: 'secret123', only: [:create] ) def index render_ok end def context ElasticAPM.set_label :things, 1 ElasticAPM.set_custom_context nested: { banana: 'explosion' } render_ok end def create render_ok end def test_body render_ok end def raise_error raise FancyError, "Help! I'm trapped in a specfile!" end def report_message ElasticAPM.report_message 'Very very message' render_ok end def send_notification NotificationsMailer.ping('someone@example.com', 'Hello').deliver_now render_ok end private def render_ok if Rails.version.start_with?('4') render text: 'Yes!' else render plain: 'Yes!' end end User = Struct.new(:id, :email) def current_user @current_user ||= User.new(1, 'person@example.com') end end class NotificationsMailer < ActionMailer::Base def ping(recipient, subject) mail to: [recipient], subject: subject do |format| format.text { 'Hello you!' } end end end MockIntake.stub! RailsTestApp::Application.initialize! RailsTestApp::Application.routes.draw do root to: 'application#index' get '/tags_and_context', to: 'application#context' post '/', to: 'application#create' post '/test_body', to: 'application#test_body' get '/error', to: 'application#raise_error' get '/report_message', to: 'application#report_message' get '/send_notification', to: 'application#send_notification' get '/ping', to: 'application#ping' end end context 'Service metadata' do it 'includes Rails info' do responses = Array.new(10).map { get '/' } wait_for transactions: 10 expect(responses.last.body).to eq 'Yes!' expect(@mock_intake.metadatas.length >= 1).to be true expect(@mock_intake.transactions.length).to be 10 service = @mock_intake.metadatas[0]['service'] expect(service['name']).to eq 'RailsTestApp' expect(service['framework']['name']).to eq 'Ruby on Rails' expect(service['framework']['version']) .to match(/\d+\.\d+\.\d+(\.\d+)?/) end end context 'log path' do it 'prepends Rails.root to log_path' do final_log_path = ElasticAPM.agent.config.log_path.to_s expect(final_log_path).to eq "#{Rails.root}/spec/elastic_apm.log" end end context 'log level' do it 'uses the default log level' do log_level = ElasticAPM.agent.config.logger.level expect(log_level).to eq Logger::INFO end context 'when the log level is updated via central config' do before do ElasticAPM.agent.config.replace_options('log_level' => 'off') end it 'does not change the Rails log level' do log_level = Rails.logger.level expect(log_level).to eq Logger::DEBUG end it 'changes the ElasticAPM config log level' do log_level = ElasticAPM.agent.config.log_level expect(log_level).to eq Logger::FATAL end end end describe 'transactions' do context 'when a simple get request is made' do it 'spans action and posts it' do get '/' wait_for transactions: 1, spans: 2 name = @mock_intake.transactions.fetch(0)['name'] expect(name).to eq 'ApplicationController#index' end end context 'when a simple post request is made with a body' do it 'spans action and posts it' do post '/test_body', '{"data":{"a":"1","b":"five"}}', 'CONTENT_TYPE' => 'application/json' wait_for transactions: 1, spans: 2 name = @mock_intake.transactions.fetch(0)['name'] expect(name).to eq 'ApplicationController#test_body' body = @mock_intake.transactions.fetch(0).dig('context', 'request', 'body') expect(body).to eq '{"data":{"a":"1","b":"five"}}' end end context 'when tags and context are set' do it 'sets the values' do get '/tags_and_context' wait_for transactions: 1, spans: 2 context = @mock_intake.transactions.fetch(0)['context'] expect(context['tags']).to eq('things' => 1) expect(context['custom']).to eq( 'nested' => { 'banana' => 'explosion' } ) end end context 'when there is user information' do it 'includes the info in transactions' do get '/' wait_for transactions: 1, spans: 2 context = @mock_intake.transactions.fetch(0)['context'] user = context['user'] expect(user['id']).to eq '1' expect(user['email']).to eq 'person@example.com' end end context 'when there are ignored url patterns defined' do it 'does not create events for the patterns' do get '/ping' get '/' wait_for transactions: 1, spans: 2 name = @mock_intake.transactions.fetch(0)['name'] expect(name).to eq 'ApplicationController#index' end end context 'when there is sensitive data' do it 'filters the data and does not alter the original' do resp = post '/', access_token: 'abc123' wait_for transactions: 1, spans: 1 expect(resp.body).to eq("HTTP Basic: Access denied.\n") transaction, = @mock_intake.transactions body = transaction.dig('context', 'request', 'body') expect(body['access_token']).to eq '[FILTERED]' end end context 'when json' do it 'validates the schema', type: :json_schema do get '/' wait_for transactions: 1 metadata = @mock_intake.metadatas.fetch(0) expect(metadata).to match_json_schema(:metadatas), metadata.inspect transaction = @mock_intake.transactions.fetch(0) expect(transaction).to match_json_schema(:transactions), transaction.inspect span = @mock_intake.spans.fetch(0) expect(span).to match_json_schema(:spans), span.inspect end end end describe 'errors' do context 'when there is an exception' do it 'creates an error and transaction event' do response = get '/error' wait_for transactions: 1, errors: 1, spans: 1 expect(response.status).to be 500 error = @mock_intake.errors.fetch(0) expect(error['transaction_id']).to_not be_nil expect(error['transaction']['sampled']).to be true expect(error['context']).to_not be nil exception = error['exception'] expect(exception['type']).to eq 'ApplicationController::FancyError' expect(exception['handled']).to eq false end end context 'when json' do it 'validates the schema' do get '/error' wait_for transactions: 1, errors: 1 payload = @mock_intake.errors.fetch(0) expect(payload).to match_json_schema(:errors), payload.inspect end end context 'when a message is reported' do it 'sends the message' do get '/report_message' wait_for transactions: 1, errors: 1, spans: 2 error, = @mock_intake.errors expect(error['log']).to be_a Hash end end describe 'mailers' do context 'when a mail is sent' do it 'spans the mail' do get '/send_notification' wait_for transactions: 1, spans: 3 transaction, = @mock_intake.transactions expect(transaction['name']) .to eq 'ApplicationController#send_notification' span = @mock_intake.spans.find do |payload| payload['name'] == 'NotificationsMailer#ping' end expect(span).to_not be_nil end end end end describe 'metrics' do context 'when metrics are collected' do it 'sends them' do get '/' wait_for( transactions: 1, spans: 2, timeout: 10 ) wait_for { span_metrics.count >= 3 } span_keys_counts = span_metrics.each_with_object(Hash.new { 0 }) do |set, keys| keys[set['samples'].keys] += 1 end expect( span_keys_counts[ %w[span.self_time.sum.us span.self_time.count] ] ).to be >= 1 end end end end end