spec/elastic_apm/instrumenter_spec.rb (363 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 'spec_helper'
module ElasticAPM
RSpec.describe Instrumenter, :intercept do
let(:config) { Config.new }
let(:agent) { ElasticAPM.agent }
before do
intercept!
ElasticAPM.start config
allow(agent).to receive(:enqueue) { nil }
end
after do
ElasticAPM.stop
end
subject do
agent.instrumenter
end
context 'life cycle' do
describe '#stop' do
let(:subscriber) { double(register!: true, unregister!: true) }
before do
subject.subscriber = subscriber
subject.start_transaction(config: config)
subject.stop
end
its(:current_transaction) { should be_nil }
it 'deletes thread local' do
expect(Thread.current[ElasticAPM::Instrumenter::TRANSACTION_KEY])
.to be_nil
end
it 'unregisters subscriber' do
expect(subscriber).to have_received(:unregister!)
end
end
describe 'stop and start again' do
let(:subscriber) { double(register!: true, unregister!: true) }
before do
subject.subscriber = subscriber
subject.start_transaction(config: config)
subject.stop
end
after { subject.stop }
it 're-registers the subscriber' do
expect(subscriber).to receive(:register!)
subject.start
end
end
end
describe '#start_transaction' do
it 'returns a new transaction and sets it as current' do
context = Context.new
transaction =
subject.start_transaction 'Test', 't',
config: config, context: context
expect(transaction.name).to eq 'Test'
expect(transaction.type).to eq 't'
expect(transaction.id).to be subject.current_transaction.id
expect(transaction.context).to be context
expect(subject.current_transaction).to be transaction
end
it 'explodes if called inside other transaction' do
subject.start_transaction 'Test', config: config
expect { subject.start_transaction 'Test', config: config }
.to raise_error(ExistingTransactionError)
end
context 'when instrumentation is disabled' do
let(:config) { Config.new(instrument: false) }
it 'is nil' do
expect(subject.start_transaction(config: config)).to be nil
expect(subject.current_transaction).to be nil
end
end
context 'with default labels' do
let(:config) { Config.new(default_labels: { more: 'yes!' }) }
it 'adds them to transaction context' do
transaction = subject.start_transaction 'Test', 't', config: config
expect(transaction.context.labels).to match(more: 'yes!')
end
end
context 'with default labels' do
let(:config) { Config.new(default_labels: { more: 'yes!' }) }
it 'adds them to transaction context' do
transaction = subject.start_transaction 'Test', 't', config: config
expect(transaction.context.labels).to match(more: 'yes!')
end
end
context 'when the transaction is unsampled' do
let(:config) { Config.new }
it 'sets the sample rate to 0' do
expect(subject).to receive(:random_sample?) { false }
t = subject.start_transaction 'Test', 't', config: config
expect(t.sampled?).to be false
expect(t.sample_rate).to eq 0
end
end
context 'when the transaction is sampled' do
let(:config) { Config.new(transaction_sample_rate: '0.2') }
it 'sets the sample rate to the configured sample rate' do
expect(subject).to receive(:random_sample?) { true }
t = subject.start_transaction 'Test', 't', config: config
expect(t.sampled?).to be true
expect(t.sample_rate).to eq 0.2
end
end
end
describe '#end_transaction' do
it 'is nil when no transaction' do
expect(subject.end_transaction).to be nil
end
it 'ends and enqueues current transaction' do
transaction = subject.start_transaction(config: config)
return_value = subject.end_transaction('result')
expect(return_value).to be transaction
expect(transaction).to be_stopped
expect(transaction.result).to eq 'result'
expect(subject.current_transaction).to be nil
expect(agent).to have_received(:enqueue).with(transaction)
end
it 'reports metrics' do
agent.metrics.stop
subject.start_transaction('a_transaction', config: config)
sleep(0.1)
subject.start_span('a_span', 'a', subtype: 'b')
sleep(0.1)
subject.end_span
sleep(0.1)
subject.end_transaction('result')
brk_sets = agent.metrics.get(:breakdown).collect
txn_self_time = brk_sets.find do |d|
d.span&.fetch(:type) == 'app'
end
spn_self_time = brk_sets.find { |d| d.span&.fetch(:type) == 'a' }
# txn_self_time
expect(txn_self_time.samples[:'span.self_time.sum.us']).to be > 200000
expect(txn_self_time.samples[:'span.self_time.count']).to eq 1
expect(txn_self_time.transaction).to match(
name: 'a_transaction',
type: 'custom'
)
expect(txn_self_time.span).to match(type: 'app', subtype: nil)
# spn_self_time
expect(spn_self_time.samples[:'span.self_time.sum.us']).to be > 100000
expect(spn_self_time.samples[:'span.self_time.count']).to eq 1
expect(spn_self_time.transaction).to match(
name: 'a_transaction',
type: 'custom'
)
expect(spn_self_time.span).to match(type: 'a', subtype: 'b')
# resets on collect
new_txn_set, = agent.metrics.get(:transaction).collect
expect(new_txn_set).to be nil
end
context 'with breakdown metrics disabled' do
let(:config) { Config.new breakdown_metrics: false }
it 'skips breakdown but keeps transaction metrics', :mock_time do
subject.start_transaction('a_transaction', config: config)
travel 100
subject.start_span('a_span', 'a', subtype: 'b')
travel 100
subject.end_span
travel 100
subject.end_transaction('result')
brk_sets = agent.metrics.get(:breakdown).collect
expect(brk_sets).to be nil
end
end
end
describe '#start_span' do
context 'when no transaction' do
it { expect(subject.start_span('Span')).to be nil }
end
context 'when transaction unsampled' do
let(:config) { Config.new(transaction_sample_rate: 0.0) }
it 'skips spans' do
transaction = subject.start_transaction(config: config)
expect(transaction).to_not be_sampled
span = subject.start_span 'Span'
expect(span).to be_nil
end
end
context 'inside a sampled transaction' do
let(:transaction) { subject.start_transaction(config: config) }
before do
transaction
end
it "increments transaction's span count" do
expect { subject.start_span 'Span' }
.to change(transaction, :started_spans).by 1
end
it 'starts and returns a span' do
span = subject.start_span 'Span'
expect(span).to be_a Span
expect(span).to be_started
expect(span.transaction).to eq transaction
expect(span.parent_id).to eq transaction.id
expect(subject.current_span).to eq span
end
context 'with a backtrace' do
it 'saves original backtrace for later' do
backtrace = caller
span = subject.start_span 'Span', backtrace: backtrace
expect(span.original_backtrace).to eq backtrace
end
end
context 'inside another span' do
it 'sets current span as parent' do
parent = subject.start_span 'Level 1'
child = subject.start_span 'Level 2'
expect(child.parent_id).to be parent.id
end
end
context 'when max spans reached' do
let(:config) { Config.new(transaction_max_spans: 1) }
before do
2.times do |i|
subject.start_span i.to_s
subject.end_span
end
end
it "increments transaction's span count, returns nil" do
expect do
expect(subject.start_span('Span')).to be nil
end.to change(transaction, :started_spans).by 1
end
end
context "with an exit_span parent" do
it "is nil" do
parent = subject.start_span('Parent', exit_span: true)
span = subject.start_span('Inside')
expect(span).to be nil
subject.end_span(parent)
end
it 'makes a subspan if type/subtype matches' do
parent = subject.start_span('Parent', 'my_type', exit_span: true)
span = subject.start_span('Inside', 'my_type')
expect(span).to_not be nil
subject.end_span(parent)
end
end
end
end
describe '#end_span' do
context 'when missing span' do
before { subject.start_transaction(config: config) }
it { expect(subject.end_span).to be nil }
end
context 'inside transaction and span' do
let(:transaction) { subject.start_transaction(config: config) }
let(:span) { subject.start_span 'Span' }
before do
transaction
span
end
it 'closes span, sets new current, enqueues' do
return_value = subject.end_span
expect(return_value).to be span
expect(span).to be_stopped
expect(subject.current_span).to be nil
expect(agent).to have_received(:enqueue).with(span)
end
context 'inside another span' do
it 'sets current span to parent' do
nested = subject.start_span 'Nested'
return_value = subject.end_span
expect(return_value).to be nested
expect(subject.current_span).to be span
end
end
context 'when passing a span' do
let(:another_span) { subject.start_span 'Another Span' }
before do
another_span
end
it 'closes span, sets new current, enqueues' do
return_value = subject.end_span(span)
expect(return_value).to be span
expect(span).to be_stopped
expect(subject.current_span).to be another_span
expect(agent).to have_received(:enqueue).with(span)
end
end
end
end
describe '#set_label' do
it 'sets tag on current transaction' do
transaction = subject.start_transaction 'Test', config: config
subject.set_label :things, 'are all good!'
expect(transaction.context.labels).to match(things: 'are all good!')
end
it 'de-dots keys' do
transaction = subject.start_transaction 'Test', config: config
subject.set_label 'th.ings', 'are all good!'
subject.set_label 'thi"ngs', 'are all good!'
subject.set_label 'thin*gs', 'are all good!'
expect(transaction.context.labels).to match(
th_ings: 'are all good!',
thi_ngs: 'are all good!',
thin_gs: 'are all good!'
)
end
it 'allows boolean values' do
transaction = subject.start_transaction 'Test', config: config
subject.set_label :things, true
expect(transaction.context.labels).to match(things: true)
end
it 'allows numerical values' do
transaction = subject.start_transaction 'Test', config: config
subject.set_label :things, 123
expect(transaction.context.labels).to match(things: 123)
end
end
describe '#set_custom_context' do
it 'sets custom context on transaction' do
transaction = subject.start_transaction 'Test', config: config
subject.set_custom_context(one: 'is in', two: 2, three: false)
expect(transaction.context.custom).to match(
one: 'is in',
two: 2,
three: false
)
end
end
describe '#set_user' do
User = Struct.new(:id, :email, :username)
it 'sets user in context' do
transaction = subject.start_transaction 'Test', config: config
subject.set_user(User.new(1, 'a@a', 'abe'))
subject.end_transaction
user = transaction.context.user
expect(user.id).to eq '1'
expect(user.email).to eq 'a@a'
expect(user.username).to eq 'abe'
end
end
describe '#handle_forking!' do
let(:subscriber) { double(register!: true, unregister!: true) }
it 'restarts with the subscriber still registered' do
subject.start
subject.subscriber = subscriber
expect(subscriber).to receive(:register!)
subject.handle_forking!
subject.stop
end
end
describe 'unsampled transactions' do
let(:config) { Config.new(transaction_sample_rate: '0') }
context 'when the server version is less than 8.0' do
it 'enqueues the transaction' do
stub_request(:get, "http://localhost:8200/")
.to_return(status: 200, body: '{"version": "7.17.4"}')
expect(subject.enqueue).to receive(:call).and_call_original
subject.start_transaction 'Test', config: config
subject.end_transaction
end
end
context 'when the server version is at least 8.0' do
it 'does not enqueue the transaction' do
stub_request(:get, "http://localhost:8200/")
.to_return(status: 200, body: '{"version": "8.5.3"}')
expect(subject.enqueue).not_to receive(:call)
subject.start_transaction 'Test', config: config
subject.end_transaction
end
end
end
end
end