All Files (97.74% covered at 2.88 hits/line)
18 files in total.
442 relevant lines.
432 lines covered and
10 lines missed
# frozen_string_literal: true
- 1
require 'logger'
- 1
require 'ruby-static-tracing/version'
- 1
require 'ruby-static-tracing/platform'
- 1
require 'ruby-static-tracing/provider'
- 1
require 'ruby-static-tracing/tracepoint'
- 1
require 'ruby-static-tracing/configuration'
- 1
require 'ruby-static-tracing/tracer'
- 1
require 'ruby-static-tracing/tracers'
# FIXME: Including StaticTracing should cause every method in a module or class to be registered
# Implement this by introspecting all methods on the includor, and wrapping them.
- 1
module StaticTracing
- 1
extend self
- 1
BaseError = Class.new(StandardError)
- 1
USDTError = Class.new(BaseError)
- 1
InternalError = Class.new(BaseError)
- 1
attr_accessor :logger
- 1
self.logger = Logger.new(STDERR)
# This will print a message indicating that tracepoints are disabled on this platform
- 1
def issue_disabled_tracepoints_warning
return if defined?(@warning_issued)
@warning_issued = true
logger.info("USDT tracepoints are not presently supported supported on #{RUBY_PLATFORM} - all operations will no-op")
end
# Efficiently return the current monotonic clocktime.
# Wraps libc clock_gettime
# The overhead of this is tested to be on the order of 10 microseconds under normal conditions
# You should inline this method in your tracer to avoid an extra method call.
- 1
def nsec
- 7
Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
end
# Should indicate if static tracing is enabled - a global constant
- 1
def enabled?
- 9
!!@enabled
end
# Overwrite the definition of all functions that are enabled
# with a wrapped version that has tracing enabled
- 1
def enable!
- 4
StaticTracing::Tracers.enable!
- 4
StaticTracing::Provider.enable! # FIXME individually call enable
- 4
@enabled = true
end
# Overwrite the definition of all functions to their original definition,
# no longer wrapping them
- 1
def disable!
- 3
StaticTracing::Tracers.disable!
- 3
StaticTracing::Provider.disable! # FIXME dangerous
- 3
@enabled = false
end
# Toggles between enabling and disabling tracepoints declared through tracers
- 1
def toggle_tracing!
- 2
enabled? ? disable! : enable!
end
# Block to configure static tracing, eg:
#
# StaticTracing.configure do |config|
# config.add_tracer(StaticTracing::Tracer::Latency)
# end
- 1
def configure
yield Configuration.instance
end
end
- 1
require 'ruby-static-tracing/ruby_static_tracing' if StaticTracing::Platform.supported_platform?
# frozen_string_literal: true
- 1
module StaticTracing
- 1
class Configuration
# Modes of operation for tracers
- 1
module Modes
- 1
ON = 'ON'
- 1
OFF = 'OFF'
- 1
SIGNAL = 'SIGNAL'
- 1
module SIGNALS
- 1
SIGPROF = 'PROF'
end
end
- 1
class << self
- 1
def instance
- 5
@instance ||= new
end
end
- 1
attr_reader :mode, :signal
# A new configuration instance
- 1
def initialize
- 1
@mode = Modes::SIGNAL
- 1
@signal = Modes::SIGNALS::SIGPROF
- 1
enable_trap
end
# Sets the mode [ON, OFF, SIGNAL]
# Default is SIGNAL
- 1
def mode=(new_mode)
- 3
handle_old_mode
- 3
@mode = new_mode
- 3
handle_new_mode
end
# Sets the SIGNAL to listen to,
# Default is SIGPROF
- 1
def signal=(new_signal)
- 2
disable_trap
- 2
@signal = new_signal
- 2
enable_trap
end
# Adds a new tracer globally
- 1
def add_tracer(tracer)
Tracers.add(tracer)
end
- 1
private
# Clean up trap handlers if mode changed to not need it
- 1
def handle_old_mode
- 3
disable_trap if @mode == Modes::SIGNAL
end
# Enable trap handlers if needed
- 1
def handle_new_mode
- 3
if @mode == Modes::SIGNAL
enable_trap
- 3
elsif @mode == Modes::ON
- 1
StaticTracing.enable!
- 2
elsif @mode == Modes::OFF
- 1
StaticTracing.disable!
end
end
# Disables trap handler
- 1
def disable_trap
- 3
Signal.trap(@signal, 'DEFAULT')
end
# Enables a new trap handler
- 1
def enable_trap
- 3
Signal.trap(@signal) { StaticTracing.toggle_tracing! }
end
end
end
# frozen_string_literal: true
- 1
module StaticTracing
# A wrapper for a USDT tracepoint provider
# This corresponds to a namespace of tracepoints
# By convention, we will often create one per
# class or module.
- 1
class Provider
- 1
attr_accessor :name
# Provider couldn't be found in collection
- 1
class ProviderMissingError < StandardError; end
- 1
class << self
# Gets a provider by name
# or creates one if not exists
- 1
def register(namespace)
- 32
providers[namespace] ||= new(namespace)
end
# Gets a provider instance by name
- 1
def fetch(namespace)
- 33
providers.fetch(namespace) do
- 2
raise ProviderMissingError
end
end
# Enables each provider, ensuring
# it is loaded into memeory
- 1
def enable!
- 4
providers.values.each(&:enable)
end
# Forcefully disables all providers,
# unloading them from memory
- 1
def disable!
- 3
providers.values.each(&:disable)
end
- 1
private
# A global collection of providers
- 1
def providers
- 72
@providers ||= {}
end
end
- 1
attr_reader :namespace, :tracepoints
# Adds a new tracepoint to this provider
# FIXME - should this be a dictionary, or are duplicate names allowed?
- 1
def add_tracepoint(tracepoint, *args)
- 17
if tracepoint.is_a?(String)
- 1
tracepoint = Tracepoint.new(namespace, tracepoint, *args)
- 16
elsif tracepoint.is_a?(Tracepoint)
- 16
@tracepoints[tracepoint.name] = tracepoint
end
end
# Enable the provider, loading it into memory
- 1
def enable
- 32
@enabled = _enable_provider
end
# Disables the provider, unloading it from memory
- 1
def disable
- 25
@enabled = !_disable_provider
end
# Returns true if the provider is enabled,
# meaning it is loaded into memory
- 1
def enabled?
- 14
@enabled
end
# Only supported on systems (linux) where backed by file
- 1
def path; end
# Completely removes the provider
- 1
def destroy; end
- 1
private
# ALWAYS use register, never call .new dilectly
- 1
def initialize(namespace)
- 7
if StaticTracing::Platform.supported_platform?
- 7
provider_initialize(namespace)
- 7
@enabled = false
else
StaticTracing.issue_disabled_tracepoints_warning
end
- 7
@namespace = namespace
- 7
@tracepoints = {}
end
end
end
# frozen_string_literal: true
- 1
module StaticTracing
- 1
class Tracepoint
- 1
class TracepointMissingError < StandardError; end
- 1
class << self
# Gets a trace instance by provider name and name
- 1
def fetch(provider, name)
- 8
Provider.fetch(provider).tracepoints.fetch(name.to_s) do
- 1
raise TracepointMissingError
end
end
end
- 1
class InvalidArgumentError < StandardError
- 1
def initialize(argument, expected_type)
- 1
error_message = <<~ERROR_MESSAGE
We expected the fire arguments to match with the ones specified on the creation of the Tracepoint
You passed #{argument} => #{argument.class} and we expected the argument to be type #{expected_type}
ERROR_MESSAGE
- 1
super(error_message)
end
end
- 1
class InvalidArgType < StandardError; end
- 1
VALID_ARGS_TYPES = [Integer, String].freeze
- 1
attr_reader :provider_name, :name, :args
# Creates a new tracepoint.
# If a provider by the name specified doesn't exist already,
# one will be added implicitly.
- 1
def initialize(provider_name, name, *args)
- 17
@provider_name = provider_name
- 17
@name = name
- 17
validate_args(args)
- 16
@args = args
- 16
if StaticTracing::Platform.supported_platform?
- 16
tracepoint_initialize(provider_name, name, args)
- 16
provider.add_tracepoint(self)
else
StaticTracing.issue_disabled_tracepoints_warning
end
end
# Fire a tracepoint, sending the data off to be received by
# a tracing program like dtrace
- 1
def fire(*values)
- 1
values.each_with_index do |arg, i|
- 1
raise InvalidArgumentError.new(arg, args[i]) unless arg.is_a?(args[i])
end
_fire_tracepoint(values)
end
# The provider this tracepoint is defined on
- 1
def provider
- 17
Provider.fetch(@provider_name)
end
# Returns true if a tracepoint is currently
# attached to, indicating we should fire it
- 1
def enabled?; end
- 1
private
- 1
def validate_args(values)
- 51
raise InvalidArgType unless values.all? { |value| VALID_ARGS_TYPES.include?(value) }
end
end
end
# frozen_string_literal: true
- 1
require_relative 'tracer/base'
- 1
require_relative 'tracer/latency'
- 1
require_relative 'tracer/stack'
# frozen_string_literal: true
- 1
require 'unmixer'
- 1
using Unmixer
- 1
require 'ruby-static-tracing/tracer/helpers'
- 1
module StaticTracing
- 1
module Tracer
- 1
class Base
- 1
class << self
- 1
include Tracer::Helpers
- 1
def register(klass, *method_names, provider: nil)
- 5
provider_name ||= underscore(klass.name)
- 5
provider = Provider.register(provider_name)
- 5
method_overrides = function_wrapper.new(provider, @wrapping_function, @data_types)
- 5
modified_classes[klass] ||= method_overrides
- 5
modified_classes[klass].add_override(method_names.flatten)
end
- 1
def enable!
- 10
modified_classes.each do |klass, wrapped_methods|
- 22
klass.prepend(wrapped_methods)
end
end
- 1
def disable!
- 12
modified_classes.each do |klass, wrapped_methods|
- 56
klass.instance_eval { unprepend(wrapped_methods) }
end
end
- 1
private
- 1
def function_wrapper
- 5
Class.new(Module) do
- 5
def initialize(provider, wrapping_function, data_types)
- 5
@provider = provider
- 5
@wrapping_function = wrapping_function
- 5
@data_types = data_types
end
- 5
def add_override(methods)
- 5
methods.each do |method|
- 8
Tracepoint.new(@provider.namespace, method.to_s, *@data_types)
- 8
define_method(method.to_s, @wrapping_function)
end
end
- 5
attr_reader :provider
end
end
- 1
def modified_classes
- 32
@modified_classes ||= {}
end
- 1
def set_tracepoint_data_types(*args)
- 2
@data_types = *args
end
- 1
def tracepoint_data_types
@data_types
end
- 1
def set_wrapping_function(callable)
- 2
@wrapping_function = callable
end
end
end
end
end
# frozen_string_literal: true
- 1
module StaticTracing
- 1
module Tracer
- 1
module Concerns
# Including this module will cause the target
# to have latency tracers added around every method
- 1
module Latency
- 1
def self.included(base)
- 1
methods = base.public_instance_methods(false)
- 1
StaticTracing::Tracer::Latency.register(base, methods)
end
end
end
end
end
# frozen_string_literal: true
- 1
module StaticTracing
- 1
module Tracer
- 1
module Helpers
- 1
module_function
- 1
def underscore(class_name)
class_name.gsub(/::/, '_')
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
.tr('-', '_')
- 12
.downcase
end
end
end
end
# frozen_string_literal: true
- 1
module StaticTracing
- 1
module Tracer
- 1
class Latency < Base
- 1
set_wrapping_function lambda { |*args, &block|
- 3
start_time = StaticTracing.nsec
- 3
result = super(*args, &block)
- 3
duration = StaticTracing.nsec - start_time
- 3
method_name = __method__.to_s
- 3
provider = Tracer::Helpers.underscore(self.class.name)
# FIXME: benchmark this, we may need to cache the provider instance on the object
# This lookup is a bit of a hack
- 3
Tracepoint.fetch(provider, method_name).fire(method_name, duration)
- 3
result
}
- 1
set_tracepoint_data_types(String, Integer)
end
end
end
# frozen_string_literal: true
- 1
module StaticTracing
- 1
module Tracer
# A stack tracer gets the stack trace at point when
# the tracer is executed
- 1
class Stack < Base
- 1
set_wrapping_function lambda { |*args, &block|
- 4
current_stack = send(:caller).join("\n")
- 4
method_name = __method__.to_s
- 4
provider = Tracer::Helpers.underscore(self.class.name)
- 4
Tracepoint.fetch(provider, method_name).fire(method_name, current_stack)
- 4
super(*args, &block)
}
- 1
set_tracepoint_data_types(String, String)
end
end
end
# frozen_string_literal: true
- 1
module StaticTracing
# Tracers are a layer of abstraction above tracepoints. They are opinionated
# and contextual ways of applying tracepoints to an application.
- 1
class Tracers
# Error for an invalid tracer
- 1
class InvalidTracerError < StandardError
- 1
def initialize
- 1
msg = <<~MSG
You need to add a valid tracer.
To create a valid tracer please inherit from StaticTracing::Tracer::Base
and follow the guide on how to create tracers
MSG
- 1
super(msg)
end
end
- 1
class << self
- 1
def add(tracer)
- 2
raise InvalidTracerError unless tracer < StaticTracing::Tracer::Base
- 1
tracers << tracer
end
# Enables each tracer, overriding original
# method definition with traced one
- 1
def enable!
- 5
tracers.each(&:enable!)
end
# Disables each tracer, replacing the method definition
- 1
def disable!
- 4
tracers.each(&:disable!)
end
# Clears all tracers
- 1
def clean
# FIXME: - actuallly ensure destroyed to avoid memory leaks
- 12
@tracers = []
end
- 1
private
- 1
def tracers
- 10
@tracers ||= []
end
end
end
end
# frozen_string_literal: true
- 1
require 'test_helper'
- 1
module StaticTracing
- 1
class ProviderTest < MiniTest::Test
- 1
def setup
- 10
@namespace = 'tracing'
- 10
@provider = Provider.register(@namespace)
end
- 1
def test_instance_not_found
- 1
assert_raises Provider::ProviderMissingError do
- 1
Provider.fetch('not_registered')
end
end
- 1
def test_provider
- 1
assert_equal @provider, Provider.fetch(@namespace)
end
- 1
def test_add_tracepoint
- 1
tracepoint = @provider.add_tracepoint('my_method', Integer, String)
- 1
assert_instance_of Tracepoint, tracepoint
- 1
assert_equal @provider.tracepoints.length, 1
end
- 1
def test_provider_starts_disabled
- 1
p = Provider.register('starts_disabled')
- 1
refute p.enabled?
- 1
refute @provider.enabled?
end
- 1
def test_new_provider_empty_path
- 1
assert_empty(@provider.path)
end
- 1
def test_enable_provider
- 1
refute(@provider.enabled?)
- 1
assert(@provider.enable)
- 1
assert(@provider.enabled?)
- 1
@provider.disable
end
# FIXME: this is expected to fail on darwin
- 1
def test_enabled_provider_has_nonempty_path
- 1
refute(@provider.enabled?)
- 1
assert(@provider.enable)
- 1
assert(@provider.enabled?)
- 1
refute_empty(@provider.path)
- 1
@provider.disable
end
- 1
def test_disabled_provider_has_empty_path
- 1
refute(@provider.enabled?)
- 1
assert(@provider.enable)
- 1
assert(@provider.enabled?)
- 1
@provider.disable
- 1
assert_empty(@provider.path)
end
- 1
def test_disable_provider
- 1
refute(@provider.enabled?)
- 1
assert(@provider.enable)
- 1
assert(@provider.enabled?)
- 1
@provider.disable
- 1
refute(@provider.enabled?)
end
- 1
def test_raises_error_if_provider_does_not_exists
- 1
Tracepoint.new('test', 'my_method', Integer, String)
- 1
assert_raises(StaticTracing::Provider::ProviderMissingError) do
- 1
Provider.fetch('noop')
end
end
end
end
# frozen_string_literal: true
- 1
require 'test_helper'
- 1
module StaticTracing
- 1
class TracepointTest < MiniTest::Test
- 1
def setup
- 6
@tracepoint = Tracepoint.new('test', 'my_method', Integer, String)
end
- 1
def test_initialize_raises_when_args_is_not_supported
- 1
assert_raises(Tracepoint::InvalidArgType) do
- 1
Tracepoint.new('test', 'my_method', Integer, Float)
end
end
- 1
def test_fire_match_the_right_args
- 1
assert_raises(Tracepoint::InvalidArgumentError) do
- 1
@tracepoint.fire('hello', 1)
end
- 1
@tracepoint.expects(:fire).once
- 1
@tracepoint.fire(1, 'hello')
end
- 1
def test_tracepoint_implicitly_declare_provider
- 1
p = StaticTracing::Provider.fetch(@tracepoint.provider_name)
- 1
assert_equal(p.namespace, 'test')
end
- 1
def test_access_provider_through_tracepoint
- 1
assert_equal(@tracepoint.provider.namespace, 'test')
end
- 1
def test_get_returns_tracepoint
- 1
tracepoint = Provider.fetch('test').tracepoints['my_method']
- 1
assert_instance_of Tracepoint, tracepoint
end
- 1
def test_raises_error_if_tracepoint_does_not_exists
- 1
assert_raises(StaticTracing::Tracepoint::TracepointMissingError) do
- 1
Tracepoint.fetch('test', 'noop')
end
end
end
end
# frozen_string_literal: true
- 1
require 'test_helper'
- 1
require 'ruby-static-tracing/tracer/concerns/latency_tracer'
- 1
module StaticTracing
- 1
module Tracer
- 1
module Concerns
- 1
class LatencyTest < MiniTest::Test
- 1
class Example
- 1
def noop; end
- 1
include StaticTracing::Tracer::Concerns::Latency
- 1
def untraced_noop; end
end
- 1
def setup
- 2
@example = Example.new
- 2
Tracer::Latency.enable!
end
- 1
def teardown
- 2
Tracer::Latency.disable!
- 2
Tracers.clean
end
- 1
def test_noop_will_fire_an_event_when
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).once
- 1
@example.noop
end
- 1
def test_untraced_noop_will_not_fire_an_event
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).never
- 1
@example.untraced_noop
end
end
end
end
end
# frozen_string_literal: true
- 1
require 'test_helper'
- 1
module StaticTracing
- 1
module Tracer
- 1
class LatencyTest < MiniTest::Test
- 1
class Example
- 1
def noop; end
- 1
Tracer::Latency.register(self, :noop)
- 1
def noop_with_args(*args, arg1:)
- 6
Array(args).map { |arg| arg1 + arg }
end
- 1
Tracer::Latency.register(self, :noop_with_args)
end
- 1
def setup
- 4
@example = Example.new
- 4
Tracer::Latency.enable!
end
- 1
def teardown
- 4
Tracer::Latency.disable!
- 4
Tracers.clean
end
- 1
def test_noop_will_fire_an_event_when
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).once
- 1
@example.noop
end
- 1
def test_disable_will_prevent_firing_an_event
- 1
Tracer::Latency.disable!
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).never
- 1
@example.noop
end
- 1
def test_noop_with_args_will_fire_events
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).once
- 1
result = @example.noop_with_args(2, 3, arg1: 1)
- 1
assert_equal([3, 4], result)
end
- 1
def test_noop_with_args_works_correctly_when_disabled
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).never
- 1
Tracer::Latency.disable!
- 1
result = @example.noop_with_args(2, 3, arg1: 1)
- 1
assert_equal([3, 4], result)
end
end
end
end
# frozen_string_literal: true
- 1
require 'test_helper'
- 1
module StaticTracing
- 1
module Tracer
- 1
class StackTest < MiniTest::Test
- 1
class Example
- 1
def noop
- 1
true
end
- 1
def noop_with_arg(foo)
- 1
foo
end
- 1
def noop_with_block
- 1
yield
end
- 1
def noop_with_arg_and_block(foo)
- 1
yield foo
end
StaticTracing::Tracer::Stack
- 1
.register(self, :noop, :noop_with_arg, :noop_with_block,
:noop_with_arg_and_block)
end
- 1
def teardown
- 4
Tracer::Stack.disable!
- 4
Tracers.clean
end
- 1
def test_basic_methods_fire_tracepoints
- 1
Tracer::Stack.enable!
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).once
- 1
@example = Example.new
- 1
assert_equal(true, @example.noop)
end
- 1
def test_methods_with_args_still_work
- 1
Tracer::Stack.enable!
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).once
- 1
@example = Example.new
- 1
assert_equal(1, @example.noop_with_arg(1))
end
- 1
def test_methods_with_blocks_still_work
- 1
Tracer::Stack.enable!
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).once
- 1
@example = Example.new
- 2
assert_equal(1, @example.noop_with_block { 1 })
end
- 1
def test_methods_with_blocks_and_args_still_work
- 1
Tracer::Stack.enable!
- 1
StaticTracing::Tracepoint.any_instance.expects(:fire).once
- 1
@example = Example.new
- 2
assert_equal(1, @example.noop_with_arg_and_block(1) { |a| a })
end
end
end
end
# frozen_string_literal: true
- 1
require 'test_helper'
- 1
module StaticTracing
- 1
class TracersTest < MiniTest::Test
- 1
def teardown
- 2
Tracers.clean
end
- 1
def test_add_raises_error_if_not_a_valid_tracer
- 1
assert_raises(StaticTracing::Tracers::InvalidTracerError) do
- 1
Tracers.add(String)
end
end
- 1
def test_add_a_valid_tracer
- 1
Tracers.add(StaticTracing::Tracer::Latency)
- 1
StaticTracing::Tracer::Latency.expects(:enable!)
- 1
Tracers.enable!
- 1
StaticTracing::Tracer::Latency.expects(:disable!)
- 1
Tracers.disable!
end
end
end
# frozen_string_literal: true
- 1
require 'test_helper'
- 1
class DummyClass
- 1
include StaticTracing
end
- 1
class RubyStaticTracingTest < MiniTest::Test
- 1
class Example
- 1
def noop; end
- 1
StaticTracing::Tracer::Latency.register(self, :noop)
end
- 1
def test_nsec_returns_monotonic_time_in_nanoseconds
Process
.expects(:clock_gettime)
- 1
.with(Process::CLOCK_MONOTONIC, :nanosecond)
- 1
StaticTracing.nsec
end
- 1
def test_toggle_tracing
- 1
StaticTracing.enable!
- 1
assert StaticTracing.enabled?
- 1
assert StaticTracing::Provider.fetch('ruby_static_tracing_test_example').enabled?
- 1
StaticTracing.toggle_tracing!
- 1
refute StaticTracing.enabled?
- 1
refute StaticTracing::Provider.fetch('ruby_static_tracing_test_example').enabled?
- 1
StaticTracing.toggle_tracing!
- 1
assert StaticTracing.enabled?
- 1
assert StaticTracing::Provider.fetch('ruby_static_tracing_test_example').enabled?
end
end