Skip to content
Snippets Groups Projects
Commit 540eb0a9 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets
Browse files

Merge branch 'influxdb' into 'master'

Storing of application metrics in InfluxDB

This adds support for tracking metrics in InfluxDB, which in turn can be visualized using Grafana. For more information see #2936.

See merge request !2042
parents c08cb923 1be5668a
No related branches found
No related tags found
No related merge requests found
Showing
with 1092 additions and 12 deletions
Loading
Loading
@@ -215,6 +215,14 @@ gem 'select2-rails', '~> 3.5.9'
gem 'virtus', '~> 1.0.1'
gem 'net-ssh', '~> 3.0.1'
 
# Metrics
group :metrics do
gem 'allocations', '~> 1.0', require: false, platform: :mri
gem 'method_source', '~> 0.8', require: false
gem 'influxdb', '~> 0.2', require: false
gem 'connection_pool', '~> 2.0', require: false
end
group :development do
gem "foreman"
gem 'brakeman', '~> 3.1.0', require: false
Loading
Loading
Loading
Loading
@@ -49,6 +49,7 @@ GEM
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
allocations (1.0.1)
annotate (2.6.10)
activerecord (>= 3.2, <= 4.3)
rake (~> 10.4)
Loading
Loading
@@ -65,7 +66,7 @@ GEM
attr_encrypted (1.3.4)
encryptor (>= 1.3.0)
attr_required (1.0.0)
autoprefixer-rails (6.1.1)
autoprefixer-rails (6.1.2)
execjs
json
awesome_print (1.2.0)
Loading
Loading
@@ -104,7 +105,7 @@ GEM
bundler-audit (0.4.0)
bundler (~> 1.2)
thor (~> 0.18)
byebug (8.2.0)
byebug (8.2.1)
cal-heatmap-rails (0.0.1)
capybara (2.4.4)
mime-types (>= 1.16)
Loading
Loading
@@ -119,6 +120,7 @@ GEM
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
json (>= 1.7)
cause (0.1)
charlock_holmes (0.7.3)
chunky_png (1.3.5)
cliver (0.3.2)
Loading
Loading
@@ -142,10 +144,10 @@ GEM
term-ansicolor (~> 1.3)
thor (~> 0.19.1)
tins (~> 1.6.0)
crack (0.4.2)
crack (0.4.3)
safe_yaml (~> 1.0.0)
creole (0.5.0)
d3_rails (3.5.6)
d3_rails (3.5.11)
railties (>= 3.1.0)
daemons (1.2.3)
database_cleaner (1.4.1)
Loading
Loading
@@ -232,7 +234,7 @@ GEM
ipaddress (~> 0.5)
nokogiri (~> 1.5, >= 1.5.11)
opennebula
fog-brightbox (0.9.0)
fog-brightbox (0.10.1)
fog-core (~> 1.22)
fog-json
inflecto (~> 0.0.2)
Loading
Loading
@@ -251,7 +253,7 @@ GEM
fog-core (>= 1.21.0)
fog-json
fog-xml (>= 0.0.1)
fog-sakuracloud (1.4.0)
fog-sakuracloud (1.5.0)
fog-core
fog-json
fog-softlayer (1.0.2)
Loading
Loading
@@ -279,11 +281,11 @@ GEM
ruby-progressbar (~> 1.4)
gemnasium-gitlab-service (0.2.6)
rugged (~> 0.21)
gemojione (2.1.0)
gemojione (2.1.1)
json
get_process_mem (0.2.0)
gherkin-ruby (0.3.2)
github-linguist (4.7.2)
github-linguist (4.7.3)
charlock_holmes (~> 0.7.3)
escape_utils (~> 1.1.0)
mime-types (>= 1.19)
Loading
Loading
@@ -300,7 +302,7 @@ GEM
posix-spawn (~> 0.3)
gitlab_emoji (0.2.0)
gemojione (~> 2.1)
gitlab_git (7.2.21)
gitlab_git (7.2.22)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
Loading
Loading
@@ -372,6 +374,9 @@ GEM
i18n (0.7.0)
ice_nine (0.11.1)
inflecto (0.0.2)
influxdb (0.2.3)
cause
json
ipaddress (0.8.0)
jquery-atwho-rails (1.3.2)
jquery-rails (4.0.5)
Loading
Loading
@@ -419,7 +424,7 @@ GEM
net-ldap (0.12.1)
net-ssh (3.0.1)
netrc (0.11.0)
newrelic-grape (2.0.0)
newrelic-grape (2.1.0)
grape
newrelic_rpm
newrelic_rpm (3.9.4.245)
Loading
Loading
@@ -797,7 +802,7 @@ GEM
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.3)
warden (1.2.4)
rack (>= 1.0)
web-console (2.2.1)
activemodel (>= 4.0)
Loading
Loading
@@ -828,6 +833,7 @@ DEPENDENCIES
acts-as-taggable-on (~> 3.4)
addressable (~> 2.3.8)
after_commit_queue
allocations (~> 1.0)
annotate (~> 2.6.0)
asana (~> 0.4.0)
asciidoctor (~> 1.5.2)
Loading
Loading
@@ -850,6 +856,7 @@ DEPENDENCIES
charlock_holmes (~> 0.7.3)
coffee-rails (~> 4.1.0)
colorize (~> 0.7.0)
connection_pool (~> 2.0)
coveralls (~> 0.8.2)
creole (~> 0.5.0)
d3_rails (~> 3.5.5)
Loading
Loading
@@ -887,6 +894,7 @@ DEPENDENCIES
hipchat (~> 1.5.0)
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
influxdb (~> 0.2)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.0.0)
jquery-scrollto-rails (~> 1.4.3)
Loading
Loading
@@ -895,6 +903,7 @@ DEPENDENCIES
kaminari (~> 0.16.3)
letter_opener (~> 1.1.2)
mail_room (~> 0.6.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
mysql2 (~> 0.3.16)
Loading
Loading
Loading
Loading
@@ -3,5 +3,5 @@
# lib/support/init.d, which call scripts in bin/ .
#
web: bundle exec unicorn_rails -p ${PORT:="3000"} -E ${RAILS_ENV:="development"} -c ${UNICORN_CONFIG:="config/unicorn.rb"}
worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default
worker: bundle exec sidekiq -q post_receive -q mailers -q archive_repo -q system_hook -q project_web_hook -q gitlab_shell -q incoming_email -q runner -q common -q default -q metrics
# mail_room: bundle exec mail_room -q -c config/mail_room.yml
class MetricsWorker
include Sidekiq::Worker
sidekiq_options queue: :metrics
def perform(metrics)
prepared = prepare_metrics(metrics)
Gitlab::Metrics.pool.with do |connection|
connection.write_points(prepared)
end
end
def prepare_metrics(metrics)
metrics.map do |hash|
new_hash = hash.symbolize_keys
new_hash[:tags].each do |key, value|
if value.blank?
new_hash[:tags].delete(key)
else
new_hash[:tags][key] = escape_value(value)
end
end
new_hash
end
end
def escape_value(value)
value.to_s.gsub('=', '\\=')
end
end
Loading
Loading
@@ -449,9 +449,26 @@ production: &base
#
# Ban an IP for one hour (3600s) after too many auth attempts
# bantime: 3600
metrics:
host: localhost
enabled: false
# The name of the InfluxDB database to store metrics in.
database: gitlab
# Credentials to use for logging in to InfluxDB.
# username:
# password:
# The amount of InfluxDB connections to open.
# pool_size: 16
# The timeout of a connection in seconds.
# timeout: 10
# The minimum amount of milliseconds a method call has to take before it's
# tracked. Defaults to 10.
# method_call_threshold: 10
 
development:
<<: *base
metrics:
enabled: false
 
test:
<<: *base
Loading
Loading
@@ -494,6 +511,10 @@ test:
user_filter: ''
group_base: 'ou=groups,dc=example,dc=com'
admin_group: ''
metrics:
enabled: false
 
staging:
<<: *base
metrics:
enabled: false
if Gitlab::Metrics.enabled?
require 'influxdb'
require 'socket'
require 'connection_pool'
require 'method_source'
# These are manually require'd so the classes are registered properly with
# ActiveSupport.
require 'gitlab/metrics/subscribers/action_view'
require 'gitlab/metrics/subscribers/active_record'
Gitlab::Application.configure do |config|
config.middleware.use(Gitlab::Metrics::RackMiddleware)
end
Sidekiq.configure_server do |config|
config.server_middleware do |chain|
chain.add Gitlab::Metrics::SidekiqMiddleware
end
end
# This instruments all methods residing in app/models that (appear to) use any
# of the ActiveRecord methods. This has to take place _after_ initializing as
# for some unknown reason calling eager_load! earlier breaks Devise.
Gitlab::Application.config.after_initialize do
Rails.application.eager_load!
models = Rails.root.join('app', 'models').to_s
regex = Regexp.union(
ActiveRecord::Querying.public_instance_methods(false).map(&:to_s)
)
Gitlab::Metrics::Instrumentation.
instrument_class_hierarchy(ActiveRecord::Base) do |_, method|
loc = method.source_location
loc && loc[0].start_with?(models) && method.source =~ regex
end
end
Gitlab::Metrics::Instrumentation.configure do |config|
config.instrument_instance_methods(Gitlab::Shell)
config.instrument_methods(Gitlab::Git)
Gitlab::Git.constants.each do |name|
const = Gitlab::Git.const_get(name)
config.instrument_methods(const) if const.is_a?(Module)
end
end
GC::Profiler.enable
Gitlab::Metrics::Sampler.new.start
end
module Gitlab
module Metrics
RAILS_ROOT = Rails.root.to_s
METRICS_ROOT = Rails.root.join('lib', 'gitlab', 'metrics').to_s
PATH_REGEX = /^#{RAILS_ROOT}\/?/
def self.pool_size
Settings.metrics['pool_size'] || 16
end
def self.timeout
Settings.metrics['timeout'] || 10
end
def self.enabled?
!!Settings.metrics['enabled']
end
def self.mri?
RUBY_ENGINE == 'ruby'
end
def self.method_call_threshold
Settings.metrics['method_call_threshold'] || 10
end
def self.pool
@pool
end
def self.hostname
@hostname
end
# Returns a relative path and line number based on the last application call
# frame.
def self.last_relative_application_frame
frame = caller_locations.find do |l|
l.path.start_with?(RAILS_ROOT) && !l.path.start_with?(METRICS_ROOT)
end
if frame
return frame.path.sub(PATH_REGEX, ''), frame.lineno
else
return nil, nil
end
end
@hostname = Socket.gethostname
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
@pool = ConnectionPool.new(size: pool_size, timeout: timeout) do
host = Settings.metrics['host']
db = Settings.metrics['database']
user = Settings.metrics['username']
pw = Settings.metrics['password']
InfluxDB::Client.new(db, host: host, username: user, password: pw)
end
end
end
end
module Gitlab
module Metrics
# Class for calculating the difference between two numeric values.
#
# Every call to `compared_with` updates the internal value. This makes it
# possible to use a single Delta instance to calculate the delta over time
# of an ever increasing number.
#
# Example usage:
#
# delta = Delta.new(0)
#
# delta.compared_with(10) # => 10
# delta.compared_with(15) # => 5
# delta.compared_with(20) # => 5
class Delta
def initialize(value = 0)
@value = value
end
# new_value - The value to compare with as a Numeric.
#
# Returns a new Numeric (depending on the type of `new_value`).
def compared_with(new_value)
delta = new_value - @value
@value = new_value
delta
end
end
end
end
module Gitlab
module Metrics
# Module for instrumenting methods.
#
# This module allows instrumenting of methods without having to actually
# alter the target code (e.g. by including modules).
#
# Example usage:
#
# Gitlab::Metrics::Instrumentation.instrument_method(User, :by_login)
module Instrumentation
SERIES = 'method_calls'
def self.configure
yield self
end
# Instruments a class method.
#
# mod - The module to instrument as a Module/Class.
# name - The name of the method to instrument.
def self.instrument_method(mod, name)
instrument(:class, mod, name)
end
# Instruments an instance method.
#
# mod - The module to instrument as a Module/Class.
# name - The name of the method to instrument.
def self.instrument_instance_method(mod, name)
instrument(:instance, mod, name)
end
# Recursively instruments all subclasses of the given root module.
#
# This can be used to for example instrument all ActiveRecord models (as
# these all inherit from ActiveRecord::Base).
#
# This method can optionally take a block to pass to `instrument_methods`
# and `instrument_instance_methods`.
#
# root - The root module for which to instrument subclasses. The root
# module itself is not instrumented.
def self.instrument_class_hierarchy(root, &block)
visit = root.subclasses
until visit.empty?
klass = visit.pop
instrument_methods(klass, &block)
instrument_instance_methods(klass, &block)
klass.subclasses.each { |c| visit << c }
end
end
# Instruments all public methods of a module.
#
# This method optionally takes a block that can be used to determine if a
# method should be instrumented or not. The block is passed the receiving
# module and an UnboundMethod. If the block returns a non truthy value the
# method is not instrumented.
#
# mod - The module to instrument.
def self.instrument_methods(mod)
mod.public_methods(false).each do |name|
method = mod.method(name)
if method.owner == mod.singleton_class
if !block_given? || block_given? && yield(mod, method)
instrument_method(mod, name)
end
end
end
end
# Instruments all public instance methods of a module.
#
# See `instrument_methods` for more information.
#
# mod - The module to instrument.
def self.instrument_instance_methods(mod)
mod.public_instance_methods(false).each do |name|
method = mod.instance_method(name)
if method.owner == mod
if !block_given? || block_given? && yield(mod, method)
instrument_instance_method(mod, name)
end
end
end
end
# Instruments a method.
#
# type - The type (:class or :instance) of method to instrument.
# mod - The module containing the method.
# name - The name of the method to instrument.
def self.instrument(type, mod, name)
return unless Metrics.enabled?
name = name.to_sym
alias_name = :"_original_#{name}"
target = type == :instance ? mod : mod.singleton_class
if type == :instance
target = mod
label = "#{mod.name}##{name}"
else
target = mod.singleton_class
label = "#{mod.name}.#{name}"
end
target.class_eval <<-EOF, __FILE__, __LINE__ + 1
alias_method #{alias_name.inspect}, #{name.inspect}
def #{name}(*args, &block)
trans = Gitlab::Metrics::Instrumentation.transaction
if trans
start = Time.now
retval = __send__(#{alias_name.inspect}, *args, &block)
duration = (Time.now - start) * 1000.0
if duration >= Gitlab::Metrics.method_call_threshold
trans.add_metric(Gitlab::Metrics::Instrumentation::SERIES,
{ duration: duration },
method: #{label.inspect})
end
retval
else
__send__(#{alias_name.inspect}, *args, &block)
end
end
EOF
end
# Small layer of indirection to make it easier to stub out the current
# transaction.
def self.transaction
Transaction.current
end
end
end
end
module Gitlab
module Metrics
# Class for storing details of a single metric (label, value, etc).
class Metric
attr_reader :series, :values, :tags, :created_at
# series - The name of the series (as a String) to store the metric in.
# values - A Hash containing the values to store.
# tags - A Hash containing extra tags to add to the metrics.
def initialize(series, values, tags = {})
@values = values
@series = series
@tags = tags
@created_at = Time.now.utc
end
# Returns a Hash in a format that can be directly written to InfluxDB.
def to_hash
{
series: @series,
tags: @tags.merge(
hostname: Metrics.hostname,
ruby_engine: RUBY_ENGINE,
ruby_version: RUBY_VERSION,
gitlab_version: Gitlab::VERSION,
process_type: Sidekiq.server? ? 'sidekiq' : 'rails'
),
values: @values,
timestamp: @created_at.to_i
}
end
end
end
end
module Gitlab
module Metrics
# Class for producing SQL queries with sensitive data stripped out.
class ObfuscatedSQL
REPLACEMENT = /
\d+(\.\d+)? # integers, floats
| '.+?' # single quoted strings
| \/.+?(?<!\\)\/ # regexps (including escaped slashes)
/x
MYSQL_REPLACEMENTS = /
".+?" # double quoted strings
/x
# Regex to replace consecutive placeholders with a single one indicating
# the length. This can be useful when a "IN" statement uses thousands of
# IDs (storing this would just be a waste of space).
CONSECUTIVE = /(\?(\s*,\s*)?){2,}/
# sql - The raw SQL query as a String.
def initialize(sql)
@sql = sql
end
# Returns a new, obfuscated SQL query.
def to_s
regex = REPLACEMENT
if Gitlab::Database.mysql?
regex = Regexp.union(regex, MYSQL_REPLACEMENTS)
end
sql = @sql.gsub(regex, '?').gsub(CONSECUTIVE) do |match|
"#{match.count(',') + 1} values"
end
# InfluxDB escapes double quotes upon output, so lets get rid of them
# whenever we can.
if Gitlab::Database.postgresql?
sql = sql.delete('"')
end
sql
end
end
end
end
module Gitlab
module Metrics
# Rack middleware for tracking Rails requests.
class RackMiddleware
CONTROLLER_KEY = 'action_controller.instance'
def initialize(app)
@app = app
end
# env - A Hash containing Rack environment details.
def call(env)
trans = transaction_from_env(env)
retval = nil
begin
retval = trans.run { @app.call(env) }
# Even in the event of an error we want to submit any metrics we
# might've gathered up to this point.
ensure
if env[CONTROLLER_KEY]
tag_controller(trans, env)
end
trans.finish
end
retval
end
def transaction_from_env(env)
trans = Transaction.new
trans.add_tag(:request_method, env['REQUEST_METHOD'])
trans.add_tag(:request_uri, env['REQUEST_URI'])
trans
end
def tag_controller(trans, env)
controller = env[CONTROLLER_KEY]
label = "#{controller.class.name}##{controller.action_name}"
trans.add_tag(:action, label)
end
end
end
end
module Gitlab
module Metrics
# Class that sends certain metrics to InfluxDB at a specific interval.
#
# This class is used to gather statistics that can't be directly associated
# with a transaction such as system memory usage, garbage collection
# statistics, etc.
class Sampler
# interval - The sampling interval in seconds.
def initialize(interval = 15)
@interval = interval
@metrics = []
@last_minor_gc = Delta.new(GC.stat[:minor_gc_count])
@last_major_gc = Delta.new(GC.stat[:major_gc_count])
if Gitlab::Metrics.mri?
require 'allocations'
Allocations.start
end
end
def start
Thread.new do
Thread.current.abort_on_exception = true
loop do
sleep(@interval)
sample
end
end
end
def sample
sample_memory_usage
sample_file_descriptors
sample_objects
sample_gc
flush
ensure
GC::Profiler.clear
@metrics.clear
end
def flush
MetricsWorker.perform_async(@metrics.map(&:to_hash))
end
def sample_memory_usage
@metrics << Metric.new('memory_usage', value: System.memory_usage)
end
def sample_file_descriptors
@metrics << Metric.
new('file_descriptors', value: System.file_descriptor_count)
end
if Metrics.mri?
def sample_objects
sample = Allocations.to_hash
counts = sample.each_with_object({}) do |(klass, count), hash|
hash[klass.name] = count
end
# Symbols aren't allocated so we'll need to add those manually.
counts['Symbol'] = Symbol.all_symbols.length
counts.each do |name, count|
@metrics << Metric.new('object_counts', { count: count }, type: name)
end
end
else
def sample_objects
end
end
def sample_gc
time = GC::Profiler.total_time * 1000.0
stats = GC.stat.merge(total_time: time)
# We want the difference of GC runs compared to the last sample, not the
# total amount since the process started.
stats[:minor_gc_count] =
@last_minor_gc.compared_with(stats[:minor_gc_count])
stats[:major_gc_count] =
@last_major_gc.compared_with(stats[:major_gc_count])
stats[:count] = stats[:minor_gc_count] + stats[:major_gc_count]
@metrics << Metric.new('gc_statistics', stats)
end
end
end
end
module Gitlab
module Metrics
# Sidekiq middleware for tracking jobs.
#
# This middleware is intended to be used as a server-side middleware.
class SidekiqMiddleware
def call(worker, message, queue)
# We don't want to track the MetricsWorker itself as otherwise we'll end
# up in an infinite loop.
if worker.class == MetricsWorker
yield
return
end
trans = Transaction.new
begin
trans.run { yield }
ensure
tag_worker(trans, worker)
trans.finish
end
end
def tag_worker(trans, worker)
trans.add_tag(:action, "#{worker.class.name}#perform")
end
end
end
end
module Gitlab
module Metrics
module Subscribers
# Class for tracking the rendering timings of views.
class ActionView < ActiveSupport::Subscriber
attach_to :action_view
SERIES = 'views'
def render_template(event)
track(event) if current_transaction
end
alias_method :render_view, :render_template
private
def track(event)
values = values_for(event)
tags = tags_for(event)
current_transaction.add_metric(SERIES, values, tags)
end
def relative_path(path)
path.gsub(/^#{Rails.root.to_s}\/?/, '')
end
def values_for(event)
{ duration: event.duration }
end
def tags_for(event)
path = relative_path(event.payload[:identifier])
tags = { view: path }
file, line = Metrics.last_relative_application_frame
if file and line
tags[:file] = file
tags[:line] = line
end
tags
end
def current_transaction
Transaction.current
end
end
end
end
end
module Gitlab
module Metrics
module Subscribers
# Class for tracking raw SQL queries.
#
# Queries are obfuscated before being logged to ensure no private data is
# exposed via InfluxDB/Grafana.
class ActiveRecord < ActiveSupport::Subscriber
attach_to :active_record
SERIES = 'sql_queries'
def sql(event)
return unless current_transaction
values = values_for(event)
tags = tags_for(event)
current_transaction.add_metric(SERIES, values, tags)
end
private
def values_for(event)
{ duration: event.duration }
end
def tags_for(event)
sql = ObfuscatedSQL.new(event.payload[:sql]).to_s
tags = { sql: sql }
file, line = Metrics.last_relative_application_frame
if file and line
tags[:file] = file
tags[:line] = line
end
tags
end
def current_transaction
Transaction.current
end
end
end
end
end
module Gitlab
module Metrics
# Module for gathering system/process statistics such as the memory usage.
#
# This module relies on the /proc filesystem being available. If /proc is
# not available the methods of this module will be stubbed.
module System
if File.exist?('/proc')
# Returns the current process' memory usage in bytes.
def self.memory_usage
mem = 0
match = File.read('/proc/self/status').match(/VmRSS:\s+(\d+)/)
if match and match[1]
mem = match[1].to_f * 1024
end
mem
end
def self.file_descriptor_count
Dir.glob('/proc/self/fd/*').length
end
else
def self.memory_usage
0.0
end
def self.file_descriptor_count
0
end
end
end
end
end
module Gitlab
module Metrics
# Class for storing metrics information of a single transaction.
class Transaction
THREAD_KEY = :_gitlab_metrics_transaction
SERIES = 'transactions'
attr_reader :uuid, :tags
def self.current
Thread.current[THREAD_KEY]
end
# name - The name of this transaction as a String.
def initialize
@metrics = []
@uuid = SecureRandom.uuid
@started_at = nil
@finished_at = nil
@tags = {}
end
def duration
@finished_at ? (@finished_at - @started_at) * 1000.0 : 0.0
end
def run
Thread.current[THREAD_KEY] = self
@started_at = Time.now
yield
ensure
@finished_at = Time.now
Thread.current[THREAD_KEY] = nil
end
def add_metric(series, values, tags = {})
tags = tags.merge(transaction_id: @uuid)
@metrics << Metric.new(series, values, tags)
end
def add_tag(key, value)
@tags[key] = value
end
def finish
track_self
submit
end
def track_self
add_metric(SERIES, { duration: duration }, @tags)
end
def submit
MetricsWorker.perform_async(@metrics.map(&:to_hash))
end
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Delta do
let(:delta) { described_class.new }
describe '#compared_with' do
it 'returns the delta as a Numeric' do
expect(delta.compared_with(5)).to eq(5)
end
it 'bases the delta on a previously used value' do
expect(delta.compared_with(5)).to eq(5)
expect(delta.compared_with(15)).to eq(10)
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Instrumentation do
let(:transaction) { Gitlab::Metrics::Transaction.new }
before do
@dummy = Class.new do
def self.foo(text = 'foo')
text
end
def bar(text = 'bar')
text
end
end
allow(@dummy).to receive(:name).and_return('Dummy')
end
describe '.configure' do
it 'yields self' do
described_class.configure do |c|
expect(c).to eq(described_class)
end
end
end
describe '.instrument_method' do
describe 'with metrics enabled' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
described_class.instrument_method(@dummy, :foo)
end
it 'renames the original method' do
expect(@dummy).to respond_to(:_original_foo)
end
it 'calls the instrumented method with the correct arguments' do
expect(@dummy.foo).to eq('foo')
end
it 'tracks the call duration upon calling the method' do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(0)
allow(described_class).to receive(:transaction).
and_return(transaction)
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash),
method: 'Dummy.foo')
@dummy.foo
end
it 'does not track method calls below a given duration threshold' do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(100)
expect(transaction).to_not receive(:add_metric)
@dummy.foo
end
end
describe 'with metrics disabled' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(false)
end
it 'does not instrument the method' do
described_class.instrument_method(@dummy, :foo)
expect(@dummy).to_not respond_to(:_original_foo)
end
end
end
describe '.instrument_instance_method' do
describe 'with metrics enabled' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
described_class.
instrument_instance_method(@dummy, :bar)
end
it 'renames the original method' do
expect(@dummy.method_defined?(:_original_bar)).to eq(true)
end
it 'calls the instrumented method with the correct arguments' do
expect(@dummy.new.bar).to eq('bar')
end
it 'tracks the call duration upon calling the method' do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(0)
allow(described_class).to receive(:transaction).
and_return(transaction)
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, an_instance_of(Hash),
method: 'Dummy#bar')
@dummy.new.bar
end
it 'does not track method calls below a given duration threshold' do
allow(Gitlab::Metrics).to receive(:method_call_threshold).
and_return(100)
expect(transaction).to_not receive(:add_metric)
@dummy.new.bar
end
end
describe 'with metrics disabled' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(false)
end
it 'does not instrument the method' do
described_class.
instrument_instance_method(@dummy, :bar)
expect(@dummy.method_defined?(:_original_bar)).to eq(false)
end
end
end
describe '.instrument_class_hierarchy' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
@child1 = Class.new(@dummy) do
def self.child1_foo; end
def child1_bar; end
end
@child2 = Class.new(@child1) do
def self.child2_foo; end
def child2_bar; end
end
end
it 'recursively instruments a class hierarchy' do
described_class.instrument_class_hierarchy(@dummy)
expect(@child1).to respond_to(:_original_child1_foo)
expect(@child2).to respond_to(:_original_child2_foo)
expect(@child1.method_defined?(:_original_child1_bar)).to eq(true)
expect(@child2.method_defined?(:_original_child2_bar)).to eq(true)
end
it 'does not instrument the root module' do
described_class.instrument_class_hierarchy(@dummy)
expect(@dummy).to_not respond_to(:_original_foo)
expect(@dummy.method_defined?(:_original_bar)).to eq(false)
end
end
describe '.instrument_methods' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
end
it 'instruments all public class methods' do
described_class.instrument_methods(@dummy)
expect(@dummy).to respond_to(:_original_foo)
end
it 'only instruments methods directly defined in the module' do
mod = Module.new do
def kittens
end
end
@dummy.extend(mod)
described_class.instrument_methods(@dummy)
expect(@dummy).to_not respond_to(:_original_kittens)
end
it 'can take a block to determine if a method should be instrumented' do
described_class.instrument_methods(@dummy) do
false
end
expect(@dummy).to_not respond_to(:_original_foo)
end
end
describe '.instrument_instance_methods' do
before do
allow(Gitlab::Metrics).to receive(:enabled?).and_return(true)
end
it 'instruments all public instance methods' do
described_class.instrument_instance_methods(@dummy)
expect(@dummy.method_defined?(:_original_bar)).to eq(true)
end
it 'only instruments methods directly defined in the module' do
mod = Module.new do
def kittens
end
end
@dummy.include(mod)
described_class.instrument_instance_methods(@dummy)
expect(@dummy.method_defined?(:_original_kittens)).to eq(false)
end
it 'can take a block to determine if a method should be instrumented' do
described_class.instrument_instance_methods(@dummy) do
false
end
expect(@dummy.method_defined?(:_original_bar)).to eq(false)
end
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment