Commit 540eb0a9 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

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
......@@ -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
......
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......@@ -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)
......
......@@ -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
......@@ -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
......@@ -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
require 'spec_helper'
describe Gitlab::Metrics::Metric do
let(:metric) do
described_class.new('foo', { number: 10 }, { host: 'localtoast' })
end
describe '#series' do
subject { metric.series }
it { is_expected.to eq('foo') }
end
describe '#values' do
subject { metric.values }
it { is_expected.to eq({ number: 10 }) }
end
describe '#tags' do
subject { metric.tags }
it { is_expected.to eq({ host: 'localtoast' }) }
end
describe '#to_hash' do
it 'returns a Hash' do
expect(metric.to_hash).to be_an_instance_of(Hash)
end
describe 'the returned Hash' do
let(:hash) { metric.to_hash }
it 'includes the series' do
expect(hash[:series]).to eq('foo')
end
it 'includes the tags' do
expect(hash[:tags]).to be_an_instance_of(Hash)
expect(hash[:tags][:hostname]).to be_an_instance_of(String)
expect(hash[:tags][:ruby_engine]).to be_an_instance_of(String)
expect(hash[:tags][:ruby_version]).to be_an_instance_of(String)
expect(hash[:tags][:gitlab_version]).to be_an_instance_of(String)
expect(hash[:tags][:process_type]).to be_an_instance_of(String)
end
it 'includes the values' do
expect(hash[:values]).to eq({ number: 10 })
end
it 'includes the timestamp' do
expect(hash[:timestamp]).to be_an_instance_of(Fixnum)
end
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::ObfuscatedSQL do
describe '#to_s' do
describe 'using single values' do
it 'replaces a single integer' do
sql = described_class.new('SELECT x FROM y WHERE a = 10')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
it 'replaces a single float' do
sql = described_class.new('SELECT x FROM y WHERE a = 10.5')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
it 'replaces a single quoted string' do
sql = described_class.new("SELECT x FROM y WHERE a = 'foo'")
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
if Gitlab::Database.mysql?
it 'replaces a double quoted string' do
sql = described_class.new('SELECT x FROM y WHERE a = "foo"')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
end
it 'replaces a single regular expression' do
sql = described_class.new('SELECT x FROM y WHERE a = /foo/')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
it 'replaces regular expressions using escaped slashes' do
sql = described_class.new('SELECT x FROM y WHERE a = /foo\/bar/')
expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?')
end
end
describe 'using consecutive values' do
it 'replaces multiple integers' do
sql = described_class.new('SELECT x FROM y WHERE z IN (10, 20, 30)')
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)')
end
it 'replaces multiple floats' do
sql = described_class.new('SELECT x FROM y WHERE z IN (1.5, 2.5, 3.5)')
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)')
end
it 'replaces multiple single quoted strings' do
sql = described_class.new("SELECT x FROM y WHERE z IN ('foo', 'bar')")
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
end
if Gitlab::Database.mysql?
it 'replaces multiple double quoted strings' do
sql = described_class.new('SELECT x FROM y WHERE z IN ("foo", "bar")')
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
end
end
it 'replaces multiple regular expressions' do
sql = described_class.new('SELECT x FROM y WHERE z IN (/foo/, /bar/)')
expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)')
end
end
if Gitlab::Database.postgresql?
it 'replaces double quotes' do
sql = described_class.new('SELECT "x" FROM "y"')
expect(sql.to_s).to eq('SELECT x FROM y')
end
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::RackMiddleware do
let(:app) { double(:app) }
let(:middleware) { described_class.new(app) }
let(:env) { { 'REQUEST_METHOD' => 'GET', 'REQUEST_URI' => '/foo' } }
describe '#call' do
before do
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
end
it 'tracks a transaction' do
expect(app).to receive(:call).with(env).and_return('yay')
expect(middleware.call(env)).to eq('yay')
end
it 'tags a transaction with the name and action of a controller' do
klass = double(:klass, name: 'TestController')
controller = double(:controller, class: klass, action_name: 'show')
env['action_controller.instance'] = controller
allow(app).to receive(:call).with(env)
expect(middleware).to receive(:tag_controller).
with(an_instance_of(Gitlab::Metrics::Transaction), env)
middleware.call(env)
end
end
describe '#transaction_from_env' do
let(:transaction) { middleware.transaction_from_env(env) }
it 'returns a Transaction' do
expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction)
end
it 'tags the transaction with the request method and URI' do
expect(transaction.tags[:request_method]).to eq('GET')
expect(transaction.tags[:request_uri]).to eq('/foo')
end
end
describe '#tag_controller' do
let(:transaction) { middleware.transaction_from_env(env) }
it 'tags a transaction with the name and action of a controller' do
klass = double(:klass, name: 'TestController')
controller = double(:controller, class: klass, action_name: 'show')
env['action_controller.instance'] = controller
middleware.tag_controller(transaction, env)
expect(transaction.tags[:action]).to eq('TestController#show')
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Sampler do
let(:sampler) { described_class.new(5) }
after do
Allocations.stop if Gitlab::Metrics.mri?
end
describe '#start' do
it 'gathers a sample at a given interval' do
expect(sampler).to receive(:sleep).with(5)
expect(sampler).to receive(:sample)
expect(sampler).to receive(:loop).and_yield
sampler.start.join
end
end
describe '#sample' do
it 'samples various statistics' do
expect(sampler).to receive(:sample_memory_usage)
expect(sampler).to receive(:sample_file_descriptors)
expect(sampler).to receive(:sample_objects)
expect(sampler).to receive(:sample_gc)
expect(sampler).to receive(:flush)
sampler.sample
end
it 'clears any GC profiles' do
expect(sampler).to receive(:flush)
expect(GC::Profiler).to receive(:clear)
sampler.sample
end
end
describe '#flush' do
it 'schedules the metrics using Sidekiq' do
expect(MetricsWorker).to receive(:perform_async).
with([an_instance_of(Hash)])
sampler.sample_memory_usage
sampler.flush
end
end
describe '#sample_memory_usage' do
it 'adds a metric containing the memory usage' do
expect(Gitlab::Metrics::System).to receive(:memory_usage).
and_return(9000)
expect(Gitlab::Metrics::Metric).to receive(:new).
with('memory_usage', value: 9000).
and_call_original
sampler.sample_memory_usage
end
end
describe '#sample_file_descriptors' do
it 'adds a metric containing the amount of open file descriptors' do
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count).
and_return(4)
expect(Gitlab::Metrics::Metric).to receive(:new).
with('file_descriptors', value: 4).
and_call_original
sampler.sample_file_descriptors
end
end
describe '#sample_objects' do
it 'adds a metric containing the amount of allocated objects' do
expect(Gitlab::Metrics::Metric).to receive(:new).
with('object_counts', an_instance_of(Hash), an_instance_of(Hash)).
at_least(:once).
and_call_original
sampler.sample_objects
end
end
describe '#sample_gc' do
it 'adds a metric containing garbage collection statistics' do
expect(GC::Profiler).to receive(:total_time).and_return(0.24)
expect(Gitlab::Metrics::Metric).to receive(:new).
with('gc_statistics', an_instance_of(Hash)).
and_call_original
sampler.sample_gc
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::SidekiqMiddleware do
let(:middleware) { described_class.new }
describe '#call' do
it 'tracks the transaction' do
worker = Class.new.new
expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish)
middleware.call(worker, 'test', :test) { nil }
end
it 'does not track jobs of the MetricsWorker' do
worker = MetricsWorker.new
expect(Gitlab::Metrics::Transaction).to_not receive(:new)
middleware.call(worker, 'test', :test) { nil }
end
end
describe '#tag_worker' do
it 'adds the worker class and action to the transaction' do
trans = Gitlab::Metrics::Transaction.new
worker = double(:worker, class: double(:class, name: 'TestWorker'))
expect(trans).to receive(:add_tag).with(:action, 'TestWorker#perform')
middleware.tag_worker(trans, worker)
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActionView do
let(:transaction) { Gitlab::Metrics::Transaction.new }
let(:subscriber) { described_class.new }
let(:event) do
root = Rails.root.to_s
double(:event, duration: 2.1,
payload: { identifier: "#{root}/app/views/x.html.haml" })
end
before do
allow(subscriber).to receive(:current_transaction).and_return(transaction)
allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
and_return(['app/views/x.html.haml', 4])
end
describe '#render_template' do
it 'tracks rendering of a template' do
values = { duration: 2.1 }
tags = {
view: 'app/views/x.html.haml',
file: 'app/views/x.html.haml',
line: 4
}
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, values, tags)
subscriber.render_template(event)
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Subscribers::ActiveRecord do
let(:transaction) { Gitlab::Metrics::Transaction.new }
let(:subscriber) { described_class.new }
let(:event) do
double(:event, duration: 0.2,
payload: { sql: 'SELECT * FROM users WHERE id = 10' })
end
before do
allow(subscriber).to receive(:current_transaction).and_return(transaction)
allow(Gitlab::Metrics).to receive(:last_relative_application_frame).
and_return(['app/models/foo.rb', 4])
end
describe '#sql' do
it 'tracks the execution of a SQL query' do
sql = 'SELECT * FROM users WHERE id = ?'
values = { duration: 0.2 }
tags = { sql: sql, file: 'app/models/foo.rb', line: 4 }
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, values, tags)
subscriber.sql(event)
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::System do
if File.exist?('/proc')
describe '.memory_usage' do
it "returns the process' memory usage in bytes" do
expect(described_class.memory_usage).to be > 0
end
end
describe '.file_descriptor_count' do
it 'returns the amount of open file descriptors' do
expect(described_class.file_descriptor_count).to be > 0
end
end
else
describe '.memory_usage' do
it 'returns 0.0' do
expect(described_class.memory_usage).to eq(0.0)
end
end
describe '.file_descriptor_count' do
it 'returns 0' do
expect(described_class.file_descriptor_count).to eq(0)
end
end
end
end
require 'spec_helper'
describe Gitlab::Metrics::Transaction do
let(:transaction) { described_class.new }
describe '#duration' do
it 'returns the duration of a transaction in seconds' do
transaction.run { sleep(0.5) }
expect(transaction.duration).to be >= 0.5
end
end
describe '#run' do
it 'yields the supplied block' do
expect { |b| transaction.run(&b) }.to yield_control
end
it 'stores the transaction in the current thread' do
transaction.run do
expect(Thread.current[described_class::THREAD_KEY]).to eq(transaction)
end
end
it 'removes the transaction from the current thread upon completion' do
transaction.run { }
expect(Thread.current[described_class::THREAD_KEY]).to be_nil
end
end
describe '#add_metric' do
it 'adds a metric tagged with the transaction UUID' do
expect(Gitlab::Metrics::Metric).to receive(:new).
with('foo', { number: 10 }, { transaction_id: transaction.uuid })
transaction.add_metric('foo', number: 10)
end
end
describe '#add_tag' do
it 'adds a tag' do
transaction.add_tag(:foo, 'bar')
expect(transaction.tags).to eq({ foo: 'bar' })
end
end
describe '#finish' do
it 'tracks the transaction details and submits them to Sidekiq' do
expect(transaction).to receive(:track_self)
expect(transaction).to receive(:submit)
transaction.finish
end
end
describe '#track_self' do
it 'adds a metric for the transaction itself' do
expect(transaction).to receive(:add_metric).
with(described_class::SERIES, { duration: transaction.duration }, {})
transaction.track_self
end
end
describe '#submit' do
it 'submits the metrics to Sidekiq' do
transaction.track_self
expect(MetricsWorker).to receive(:perform_async).
with([an_instance_of(Hash)])
transaction.submit
end
end
end
require 'spec_helper'
describe Gitlab::Metrics do
describe '.pool_size' do
it 'returns a Fixnum' do
expect(described_class.pool_size).to be_an_instance_of(Fixnum)
end
end
describe '.timeout' do
it 'returns a Fixnum' do
expect(described_class.timeout).to be_an_instance_of(Fixnum)
end
end
describe '.enabled?' do
it 'returns a boolean' do
expect([true, false].include?(described_class.enabled?)).to eq(true)
end
end
describe '.hostname' do
it 'returns a String containing the hostname' do
expect(described_class.hostname).to eq(Socket.gethostname)
end
end
describe '.last_relative_application_frame' do
it 'returns an Array containing a file path and line number' do
file, line = described_class.last_relative_application_frame
expect(line).to eq(30)
expect(file).to eq('spec/lib/gitlab/metrics_spec.rb')
end
end
end
require 'spec_helper'
describe MetricsWorker do
let(:worker) { described_class.new }
describe '#perform' do
it 'prepares and writes the metrics to InfluxDB' do
connection = double(:connection)
pool = double(:pool)
expect(pool).to receive(:with).and_yield(connection)
expect(connection).to receive(:write_points).with(an_instance_of(Array))
expect(Gitlab::Metrics).to receive(:pool).and_return(pool)
worker.perform([{ 'series' => 'kittens', 'tags' => {} }])
end
end
describe '#prepare_metrics' do
it 'returns a Hash with the keys as Symbols' do
metrics = worker.prepare_metrics([{ 'values' => {}, 'tags' => {} }])
expect(metrics).to eq([{ values: {}, tags: {} }])
end
it 'escapes tag values' do
metrics = worker.prepare_metrics([
{ 'values' => {}, 'tags' => { 'foo' => 'bar=' } }
])
expect(metrics).to eq([{ values: {}, tags: { 'foo' => 'bar\\=' } }])
end
it 'drops empty tags' do
metrics = worker.prepare_metrics([
{ 'values' => {}, 'tags' => { 'cats' => '', 'dogs' => nil } }
])
expect(metrics).to eq([{ values: {}, tags: {} }])
end
end
describe '#escape_value' do
it 'escapes an equals sign' do
expect(worker.escape_value('foo=')).to eq('foo\\=')
end
it 'casts values to Strings' do
expect(worker.escape_value(10)).to eq('10')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment