├── .ruby-version ├── .rubocop.yml ├── test ├── integration │ ├── test_hello │ │ ├── hello.out │ │ ├── hello.bt │ │ ├── hello.dt │ │ ├── hello.rb │ │ └── hello_test.rb │ ├── test_latency │ │ ├── latency.out │ │ ├── latency.bt │ │ ├── latency.dt │ │ ├── latency.rb │ │ └── latency_test.rb │ ├── test_stacktrace │ │ ├── .rubocop.yml │ │ ├── stacktrace.bt │ │ ├── stacktrace.dt │ │ ├── stacktrace.out │ │ ├── stacktrace.rb │ │ ├── .rubocop_todo.yml │ │ └── stacktrace_test.rb │ ├── README.md │ └── integration_helper.rb ├── test_helper.rb ├── ruby-static-tracing │ ├── tracers_test.rb │ ├── tracer │ │ ├── concerns │ │ │ └── latency_test.rb │ │ ├── latency_test.rb │ │ └── stack_test.rb │ ├── configuration_test.rb │ ├── tracepoint_test.rb │ └── provider_test.rb └── ruby_static_tracing_test.rb ├── Gemfile ├── docker ├── build.sh ├── sources.list ├── Dockerfile.ci ├── Dockerfile.old └── scripts │ └── fetch-linux-headers.sh ├── docs ├── latency_tracer.gif ├── tracing.md └── ruby-interface.md ├── vagrant ├── debugfs.sh ├── bundle.sh ├── docker.sh └── provision.sh ├── .gitignore ├── lib ├── ruby-static-tracing │ ├── version.rb │ ├── tracer.rb │ ├── tracer │ │ ├── helpers.rb │ │ ├── concerns │ │ │ └── latency_tracer.rb │ │ ├── stack.rb │ │ ├── latency.rb │ │ └── base.rb │ ├── platform.rb │ ├── tracers.rb │ ├── configuration.rb │ ├── tracepoint.rb │ └── provider.rb ├── tasks │ ├── vagrant.rb │ └── docker.rb └── ruby-static-tracing.rb ├── .gitmodules ├── ext └── ruby-static-tracing │ ├── linux │ ├── types.h │ ├── tracepoint.h │ ├── provider.h │ ├── ruby_static_tracing.c │ ├── provider.c │ └── tracepoint.c │ ├── include │ └── ruby_static_tracing.h │ ├── lib │ ├── post-extconf.rb │ └── deps-extconf.rb │ ├── darwin │ ├── tracepoint.h │ ├── provider.h │ ├── ruby_static_tracing.c │ ├── provider.c │ └── tracepoint.c │ └── extconf.rb ├── Vagrantfile ├── Gemfile.lock ├── ruby-static-tracing.gemspec ├── .travis.yml ├── DEVELOPMENT.md ├── Rakefile ├── README.md └── .rubocop_todo.yml /.ruby-version: -------------------------------------------------------------------------------- 1 | 2.5.5 2 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | -------------------------------------------------------------------------------- /test/integration/test_hello/hello.out: -------------------------------------------------------------------------------- 1 | Attaching 1 probe... 2 | Hello world 3 | -------------------------------------------------------------------------------- /test/integration/test_latency/latency.out: -------------------------------------------------------------------------------- 1 | Attaching 1 probe... 2 | execute 3 | -------------------------------------------------------------------------------- /test/integration/test_stacktrace/.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | source 'https://rubygems.org' 4 | 5 | gemspec 6 | -------------------------------------------------------------------------------- /docker/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build -f Dockerfile.ci . -t static-tracing-test:latest 4 | -------------------------------------------------------------------------------- /docs/latency_tracer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dalehamel/ruby-static-tracing/HEAD/docs/latency_tracer.gif -------------------------------------------------------------------------------- /vagrant/debugfs.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | grep -q debugfs /proc/mounts || mount -t debugfs debugfs /sys/kernel/debug/ 3 | -------------------------------------------------------------------------------- /test/integration/test_hello/hello.bt: -------------------------------------------------------------------------------- 1 | usdt::global:hello_test 2 | { 3 | printf("%s\n", str(arg0)); 4 | exit(); 5 | } 6 | -------------------------------------------------------------------------------- /test/integration/test_hello/hello.dt: -------------------------------------------------------------------------------- 1 | global*:::hello_test 2 | { 3 | printf("Attaching 1 probe...\n%s\n", copyinstr(arg0)) 4 | } 5 | -------------------------------------------------------------------------------- /test/integration/test_latency/latency.bt: -------------------------------------------------------------------------------- 1 | usdt::expensive_operation:execute 2 | { 3 | printf("%s\n", str(arg0)); 4 | exit(); 5 | } 6 | -------------------------------------------------------------------------------- /docs/tracing.md: -------------------------------------------------------------------------------- 1 | This has been moved to http://blog.srvthe.net/usdt-report-doc/ 2 | 3 | It is part of the USDT tracing report hosted there. 4 | -------------------------------------------------------------------------------- /test/integration/test_stacktrace/stacktrace.bt: -------------------------------------------------------------------------------- 1 | usdt::stacktrace_operation:call 2 | { 3 | printf("%s\n", str(arg1)); 4 | exit(); 5 | } 6 | -------------------------------------------------------------------------------- /test/integration/test_latency/latency.dt: -------------------------------------------------------------------------------- 1 | expensive_operation*:::execute 2 | { 3 | printf("Attaching 1 probe...\n%s\n", copyinstr(arg0)) 4 | } 5 | -------------------------------------------------------------------------------- /test/integration/test_stacktrace/stacktrace.dt: -------------------------------------------------------------------------------- 1 | stacktrace_operation*:::call 2 | { 3 | printf("Attaching 1 probe...\n%s", copyinstr(arg1)) 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.so 2 | *.so.0 3 | *.swp 4 | html 5 | coverage 6 | *.iso 7 | *.log 8 | *.dylib 9 | *.bundle 10 | .vagrant 11 | tmp 12 | pkg 13 | .bin 14 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | # The current version of this gem 5 | VERSION = '0.0.15' 6 | end 7 | -------------------------------------------------------------------------------- /vagrant/bundle.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -x 3 | 4 | cd /app 5 | 6 | bundle install 7 | bundle exec rake clean 8 | bundle exec rake build 9 | bundle exec rake gem 10 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require_relative 'tracer/base' 4 | require_relative 'tracer/latency' 5 | require_relative 'tracer/stack' 6 | -------------------------------------------------------------------------------- /test/integration/test_stacktrace/stacktrace.out: -------------------------------------------------------------------------------- 1 | Attaching 1 probe... 2 | stacktrace.rb:25:in `block in
' 3 | stacktrace.rb:29:in `block in
' 4 | stacktrace.rb:28:in `lo 5 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'simplecov' 4 | SimpleCov.start 5 | require 'minitest/autorun' 6 | require 'mocha/minitest' 7 | require 'pry-byebug' if ENV['PRY'] 8 | require_relative '../lib/ruby-static-tracing.rb' 9 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "ext/ruby-static-tracing/libusdt"] 2 | path = ext/ruby-static-tracing/lib/libusdt 3 | url = https://github.com/dalehamel/libusdt.git 4 | [submodule "ext/ruby-static-tracing/lib/libstapsdt"] 5 | path = ext/ruby-static-tracing/lib/libstapsdt 6 | url = https://github.com/sthima/libstapsdt.git 7 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/linux/types.h: -------------------------------------------------------------------------------- 1 | #ifndef STATIC_TRACING_TYPES_H 2 | #define STATIC_TRACING_TYPES_H 3 | 4 | #include 5 | 6 | typedef enum TRACEPOINT_ARG_TYPES_ENUM { 7 | Integer = int64, // STAP enum type -8 8 | String = uint64, // STAP enum type 8 9 | } Tracepoint_arg_types; 10 | 11 | #endif // STATIC_TRACING_TYPEs_H 12 | -------------------------------------------------------------------------------- /vagrant/docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | cd /vagrant 6 | bundle install 7 | bundle exec rake docker:build 8 | bundle exec rake docker:run 9 | 10 | id=$(docker container ls --latest --quiet --filter status=running --filter name=ruby-static-tracing* | tr -d '\n') 11 | docker exec $id /app/vagrant/debugfs.sh 12 | docker exec $id /app/vagrant/bundle.sh 13 | -------------------------------------------------------------------------------- /test/integration/test_hello/hello.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ruby-static-tracing' 4 | STDOUT.sync = true 5 | 6 | t = StaticTracing::Tracepoint.new('global', 'hello_test', String) 7 | puts t.provider.enable 8 | 9 | Signal.trap('USR2') do 10 | puts "TRAP #{t.enabled?}" 11 | t.fire('Hello world') if t.enabled? 12 | sleep 2 13 | end 14 | 15 | loop { puts t.enabled?; sleep 1 } 16 | -------------------------------------------------------------------------------- /docker/sources.list: -------------------------------------------------------------------------------- 1 | deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial main 2 | deb-src http://apt.llvm.org/xenial/ llvm-toolchain-xenial main 3 | # 5.0 4 | deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-5.0 main 5 | deb-src http://apt.llvm.org/xenial/ llvm-toolchain-xenial-5.0 main 6 | # 6.0 7 | deb http://apt.llvm.org/xenial/ llvm-toolchain-xenial-6.0 main 8 | deb-src http://apt.llvm.org/xenial/ llvm-toolchain-xenial-6.0 main 9 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/include/ruby_static_tracing.h: -------------------------------------------------------------------------------- 1 | /* 2 | System, 3rd party, and project includes 3 | Implements Init_ruby_static_tracing, which is used as C/Ruby entrypoint. 4 | */ 5 | 6 | #ifndef RUBY_STATIC_TRACING_H 7 | #define RUBY_STATIC_TRACING_H 8 | 9 | #include "ruby.h" 10 | 11 | #include "provider.h" 12 | #include "tracepoint.h" 13 | 14 | void Init_ruby_static_tracing(); 15 | extern VALUE eUSDT, eInternal; 16 | 17 | #endif // RUBY_STATIC_TRACING_H 18 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/tracer/helpers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | module Tracer 5 | module Helpers 6 | module_function 7 | 8 | def underscore(class_name) 9 | class_name.gsub(/::/, '_') 10 | .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2') 11 | .gsub(/([a-z\d])([A-Z])/, '\1_\2') 12 | .tr('-', '_') 13 | .downcase 14 | end 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/tracer/concerns/latency_tracer.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | module Tracer 5 | module Concerns 6 | # Including this module will cause the target 7 | # to have latency tracers added around every method 8 | module Latency 9 | def self.included(base) 10 | methods = base.public_instance_methods(false) 11 | StaticTracing::Tracer::Latency.register(base, methods) 12 | end 13 | end 14 | end 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/integration/test_latency/latency.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ruby-static-tracing' 4 | STDOUT.sync = true 5 | 6 | class ExpensiveOperation 7 | def execute 8 | ([] << 1) * 100_000 9 | end 10 | 11 | StaticTracing::Tracer::Latency.register(self, :execute) 12 | end 13 | 14 | StaticTracing.configure do |config| 15 | config.add_tracer(StaticTracing::Tracer::Latency) 16 | end 17 | 18 | expensive_operation = ExpensiveOperation.new 19 | 20 | Signal.trap('USR2') do 21 | expensive_operation.execute 22 | end 23 | 24 | loop do 25 | end 26 | -------------------------------------------------------------------------------- /test/integration/test_hello/hello_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'integration_helper' 4 | 5 | # FIXME: can any of this be generalized / should the convention be encoded? 6 | class RubyStaticTracingTest < IntegrationTestCase 7 | def test_hello 8 | target = command('bundle exec ruby hello.rb', wait: 1) 9 | tracer = TraceRunner.trace('-p', target.pid, script: 'hello', wait: 1) 10 | 11 | # Signal the target to trigger probe firing 12 | target.usr2(1) 13 | 14 | assert_tracer_output(tracer.output, read_probe_file('hello.out')) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/platform.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | # Platform detection for ruby-static-tracing 5 | module Platform 6 | module_function 7 | 8 | # Returns true if platform is linux 9 | def linux? 10 | /linux/.match(RUBY_PLATFORM) 11 | end 12 | 13 | # Returns true if platform is darwin 14 | def darwin? 15 | /darwin/.match(RUBY_PLATFORM) 16 | end 17 | 18 | # Returns true if platform is known to be supported 19 | def supported_platform? 20 | linux? || darwin? 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/integration/test_stacktrace/stacktrace.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'ruby-static-tracing' 4 | STDOUT.sync = true 5 | 6 | class StacktraceOperation 7 | def execute 8 | 1 + 1 9 | end 10 | 11 | def call 12 | execute 13 | end 14 | 15 | StaticTracing::Tracer::Stack.register(self, :call) 16 | end 17 | 18 | StaticTracing.configure do |config| 19 | config.add_tracer(StaticTracing::Tracer::Stack) 20 | end 21 | 22 | stack_operation = StacktraceOperation.new 23 | 24 | Signal.trap('USR2') do 25 | stack_operation.call 26 | end 27 | 28 | loop do 29 | end 30 | -------------------------------------------------------------------------------- /test/integration/test_stacktrace/.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2019-04-20 19:23:07 -0400 using RuboCop version 0.67.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 2 10 | Style/Documentation: 11 | Exclude: 12 | - 'spec/**/*' 13 | - 'test/**/*' 14 | - 'stacktrace.rb' 15 | - 'stacktrace_test.rb' 16 | -------------------------------------------------------------------------------- /test/integration/test_latency/latency_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'integration_helper' 4 | 5 | # FIXME: can any of this be generalized / should the convention be encoded? 6 | class LatencyTest < IntegrationTestCase 7 | def test_latency 8 | target = command('bundle exec ruby latency.rb', wait: 1) 9 | 10 | # Enable probing 11 | target.prof(1) 12 | 13 | tracer = TraceRunner.trace('-p', target.pid, script: 'latency', wait: 1) 14 | 15 | # Signal the target to trigger probe firing 16 | target.usr2(1) 17 | 18 | assert_tracer_output(tracer.output, read_probe_file('latency.out')) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/integration/test_stacktrace/stacktrace_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'integration_helper' 4 | 5 | # FIXME: can any of this be generalized / should the convention be encoded? 6 | class StacktraceTest < IntegrationTestCase 7 | def test_stacktrace 8 | target = command('bundle exec ruby stacktrace.rb', wait: 1) 9 | # Enable probing 10 | target.prof(1) 11 | 12 | tracer = TraceRunner.trace('-p', target.pid, script: 'stacktrace', wait: 1) 13 | 14 | # Signal the target to trigger probe firing 15 | target.usr2(1) 16 | 17 | assert_tracer_output(tracer.output, read_probe_file('stacktrace.out')) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /vagrant/provision.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - 6 | sudo echo "deb https://download.docker.com/linux/ubuntu cosmic stable" | sudo tee > /etc/apt/sources.list.d/docker.list 7 | sudo apt-get update 8 | sudo apt-get install -y docker-ce docker-ce-cli containerd.io 9 | sudo apt-get install -y ruby ruby-dev build-essential 10 | sudo gem install bundler:1.17.3 11 | sudo usermod -a -G docker vagrant 12 | 13 | echo "cd /vagrant" >> /home/vagrant/.bashrc 14 | echo -e "You are running in the Vagrant VM.\nTo enter the dev env, you must run:\n\tbundle exec rake docker:shell" | sudo tee > /etc/motd 15 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/tracer/stack.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | module Tracer 5 | # A stack tracer gets the stack trace at point when 6 | # the tracer is executed 7 | class Stack < Base 8 | set_wrapping_function lambda { |*args, &block| 9 | current_stack = send(:caller).join("\n") 10 | method_name = __method__.to_s 11 | provider = Tracer::Helpers.underscore(self.class.name) 12 | Tracepoint.fetch(provider, method_name).fire(method_name, current_stack) 13 | 14 | super(*args, &block) 15 | } 16 | 17 | set_tracepoint_data_types(String, String) 18 | end 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/ruby-static-tracing/tracers_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module StaticTracing 6 | class TracersTest < MiniTest::Test 7 | def teardown 8 | Tracers.clean 9 | end 10 | 11 | def test_add_raises_error_if_not_a_valid_tracer 12 | assert_raises(StaticTracing::Tracers::InvalidTracerError) do 13 | Tracers.add(String) 14 | end 15 | end 16 | 17 | def test_add_a_valid_tracer 18 | Tracers.add(StaticTracing::Tracer::Latency) 19 | 20 | StaticTracing::Tracer::Latency.expects(:enable!) 21 | Tracers.enable! 22 | 23 | StaticTracing::Tracer::Latency.expects(:disable!) 24 | Tracers.disable! 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Vagrant.configure('2') do |config| 4 | config.vm.box = 'ubuntu/cosmic64' 5 | config.vm.provider 'virtualbox' do |v| 6 | v.memory = 4096 7 | v.cpus = 4 8 | end 9 | config.vm.provision 'shell', 10 | inline: '/bin/bash /vagrant/vagrant/provision.sh' 11 | 12 | config.vm.provision 'shell', 13 | inline: '/bin/bash /vagrant/vagrant/docker.sh' 14 | 15 | config.vm.provision 'shell', inline: 'echo "Vagrant is now provisioned - run \"vagrant ssh\" to access the vm, and \"bundle exec rake docker:shell\" to start a development shell"' 16 | config.vm.provision 'shell', inline: 'echo -e "Or, to do both of these, just run:\n\tbundle exec rake vagrant:shell"' 17 | end 18 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/tracer/latency.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | module Tracer 5 | class Latency < Base 6 | set_wrapping_function lambda { |*args, &block| 7 | start_time = StaticTracing.nsec 8 | result = super(*args, &block) 9 | duration = StaticTracing.nsec - start_time 10 | method_name = __method__.to_s 11 | provider = Tracer::Helpers.underscore(self.class.name) 12 | # FIXME: benchmark this, we may need to cache the provider instance on the object 13 | # This lookup is a bit of a hack 14 | Tracepoint.fetch(provider, method_name).fire(method_name, duration) 15 | result 16 | } 17 | 18 | set_tracepoint_data_types(String, Integer) 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tasks/vagrant.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | namespace :vagrant do 4 | desc 'Sets up a vagrant VM, needed for our development environment.' 5 | task :up do 6 | system('vagrant up') 7 | end 8 | 9 | desc 'Provides a shell within vagrant.' 10 | task :ssh do 11 | system('vagrant ssh') 12 | end 13 | 14 | desc 'Enters a shell within our development docker image, within vagrant.' 15 | task :shell do 16 | system("vagrant ssh -c 'cd /vagrant && bundle exec rake docker:shell'") 17 | end 18 | 19 | desc 'Runs tests within the development docker image, within vagrant' 20 | task :tests do 21 | system("vagrant ssh -c 'cd /vagrant && bundle exec rake docker:tests'") 22 | end 23 | 24 | desc 'Runs integration tests within the development docker image, within vagrant' 25 | task :integration do 26 | system("vagrant ssh -c 'cd /vagrant && bundle exec rake docker:integration'") 27 | end 28 | 29 | desc 'Cleans up the vagrant VM' 30 | task :clean do 31 | system('vagrant destroy') 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /test/ruby_static_tracing_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | class DummyClass 6 | include StaticTracing 7 | end 8 | 9 | class RubyStaticTracingTest < MiniTest::Test 10 | class Example 11 | def noop; end 12 | StaticTracing::Tracer::Latency.register(self, :noop) 13 | end 14 | 15 | def test_nsec_returns_monotonic_time_in_nanoseconds 16 | Process 17 | .expects(:clock_gettime) 18 | .with(Process::CLOCK_MONOTONIC, :nanosecond) 19 | 20 | StaticTracing.nsec 21 | end 22 | 23 | def test_toggle_tracing 24 | StaticTracing.enable! 25 | assert StaticTracing.enabled? 26 | assert StaticTracing::Provider.fetch('ruby_static_tracing_test_example').enabled? 27 | StaticTracing.toggle_tracing! 28 | refute StaticTracing.enabled? 29 | refute StaticTracing::Provider.fetch('ruby_static_tracing_test_example').enabled? 30 | StaticTracing.toggle_tracing! 31 | assert StaticTracing.enabled? 32 | assert StaticTracing::Provider.fetch('ruby_static_tracing_test_example').enabled? 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/ruby-static-tracing/tracer/concerns/latency_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | require 'ruby-static-tracing/tracer/concerns/latency_tracer' 5 | 6 | module StaticTracing 7 | module Tracer 8 | module Concerns 9 | class LatencyTest < MiniTest::Test 10 | class Example 11 | def noop; end 12 | 13 | include StaticTracing::Tracer::Concerns::Latency 14 | 15 | def untraced_noop; end 16 | end 17 | 18 | def setup 19 | @example = Example.new 20 | Tracer::Latency.enable! 21 | end 22 | 23 | def teardown 24 | Tracer::Latency.disable! 25 | Tracers.clean 26 | end 27 | 28 | def test_noop_will_fire_an_event_when 29 | StaticTracing::Tracepoint.any_instance.expects(:fire).once 30 | @example.noop 31 | end 32 | 33 | def test_untraced_noop_will_not_fire_an_event 34 | StaticTracing::Tracepoint.any_instance.expects(:fire).never 35 | @example.untraced_noop 36 | end 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/lib/post-extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 4 | 5 | require 'mkmf' 6 | require 'ruby-static-tracing/platform' 7 | 8 | BASE_DIR = __dir__ 9 | LIB_DIR = File.expand_path('../../../lib/ruby-static-tracing', __dir__) 10 | 11 | # Linux is a noop 12 | if StaticTracing::Platform.linux? 13 | File.write 'Makefile', <<~MAKEFILE 14 | all: 15 | touch post.so 16 | clean: 17 | install: 18 | MAKEFILE 19 | exit 20 | # We'll build libusdt and install and update linker info 21 | elsif StaticTracing::Platform.darwin? 22 | # This is done to ensure that the bundle will look in its local directory for the library 23 | File.write 'Makefile', <<~MAKEFILE 24 | all: 25 | touch post.bundle 26 | install_name_tool -change libusdt.dylib @loader_path/../ruby-static-tracing/libusdt.dylib #{File.join(LIB_DIR, 'ruby_static_tracing.bundle')} 27 | clean: 28 | install: 29 | MAKEFILE 30 | exit 31 | else 32 | # - Stub, for other platforms that we don't support, we write an empty makefile 33 | File.write 'Makefile', <<~MAKEFILE 34 | all: 35 | clean: 36 | install: 37 | MAKEFILE 38 | exit 39 | end 40 | -------------------------------------------------------------------------------- /test/ruby-static-tracing/configuration_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module StaticTracing 6 | class ConfigurationTest < MiniTest::Test 7 | def setup 8 | @config = Configuration.instance 9 | end 10 | 11 | def test_mode 12 | @config.mode = 'tracing' 13 | assert_equal 'tracing', @config.mode 14 | end 15 | 16 | def test_signal 17 | @config.signal = 'INT' 18 | assert_equal 'INT', @config.signal 19 | end 20 | 21 | def test_changing_the_mode_to_off_will_force_static_tracing_to_be_disabled 22 | StaticTracing.enable! 23 | assert StaticTracing.enabled? 24 | @config.mode = Configuration::Modes::OFF 25 | refute StaticTracing.enabled? 26 | end 27 | 28 | def test_changing_the_mode_to_on_will_force_static_tracing_to_be_enabled 29 | StaticTracing.disable! 30 | refute StaticTracing.enabled? 31 | @config.mode = Configuration::Modes::ON 32 | assert StaticTracing.enabled? 33 | end 34 | 35 | def test_changing_the_signal_will_disable_trapping_the_original_signal 36 | Signal.expects(:trap).with(@config.signal, 'DEFAULT') 37 | Signal.expects(:trap).with('INT') 38 | @config.signal = 'INT' 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/linux/tracepoint.h: -------------------------------------------------------------------------------- 1 | #ifndef STATIC_TRACING_TRACEPOINT_H 2 | #define STATIC_TRACING_TRACEPOINT_H 3 | 4 | #include 5 | // Include libstapsdt.h to wrap 6 | #include 7 | 8 | #include "ruby_static_tracing.h" 9 | #include "types.h" 10 | 11 | typedef union { 12 | unsigned long long intval; 13 | char *strval; 14 | } Tracepoint_fire_arg; 15 | 16 | typedef struct { 17 | char *name; 18 | SDTProbe_t *sdt_tracepoint; 19 | Tracepoint_arg_types *args; 20 | } static_tracing_tracepoint_t; 21 | 22 | /* 23 | * call-seq: 24 | * StaticTracing::Tracepoint.new(provider, id, *vargs) -> tracepoint 25 | * 26 | * Creates a new tracepoint on a provider 27 | */ 28 | VALUE 29 | tracepoint_initialize(VALUE self, VALUE provider, VALUE id, VALUE vargs); 30 | 31 | /* 32 | * call-seq: 33 | * tracepoint.fire(*vargs) -> true 34 | * 35 | * Fires data for the tracepoint to be probed 36 | */ 37 | VALUE 38 | tracepoint_fire(VALUE self, VALUE vargs); 39 | 40 | /* 41 | * call-seq: 42 | * tracepoint.enabled? -> true 43 | * 44 | * Checks if the tracepoint is enabled, indicating it is being traced 45 | */ 46 | VALUE 47 | tracepoint_enabled(VALUE self); 48 | 49 | // Allocate a static_tracing_tracepoint_type struct for ruby memory management 50 | VALUE 51 | static_tracing_tracepoint_alloc(VALUE klass); 52 | 53 | #endif // STATIC_TRACING_TRACEPOINT_H 54 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/darwin/tracepoint.h: -------------------------------------------------------------------------------- 1 | #ifndef STATIC_TRACING_TRACEPOINT_H 2 | #define STATIC_TRACING_TRACEPOINT_H 3 | 4 | #include "usdt.h" 5 | 6 | #include "ruby_static_tracing.h" 7 | 8 | // FIXME move this to shared header 9 | typedef union { 10 | unsigned long long intval; 11 | char *strval; 12 | } Tracepoint_fire_arg; 13 | 14 | typedef struct { 15 | char *name; 16 | usdt_probedef_t *usdt_tracepoint_def; 17 | usdt_probe_t *usdt_tracepoint; 18 | // Tracepoint_arg_types *args; 19 | } static_tracing_tracepoint_t; 20 | 21 | /* 22 | * call-seq: 23 | * StaticTracing::Tracepoint.new(provider, id, *vargs) -> tracepoint 24 | * 25 | * Creates a new tracepoint on a provider 26 | */ 27 | VALUE 28 | tracepoint_initialize(VALUE self, VALUE provider, VALUE id, VALUE vargs); 29 | 30 | /* 31 | * call-seq: 32 | * tracepoint.fire(*vargs) -> true 33 | * 34 | * Fires data for the tracepoint to be probed 35 | */ 36 | VALUE 37 | tracepoint_fire(VALUE self, VALUE vargs); 38 | 39 | /* 40 | * call-seq: 41 | * tracepoint.enabled? -> true 42 | * 43 | * Checks if the tracepoint is enabled, indicating it is being traced 44 | */ 45 | VALUE 46 | tracepoint_enabled(VALUE self); 47 | 48 | // Allocate a static_tracing_tracepoint_type struct for ruby memory management 49 | VALUE 50 | static_tracing_tracepoint_alloc(VALUE klass); 51 | 52 | #endif // STATIC_TRACING_TRACEPOINT_H 53 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/darwin/provider.h: -------------------------------------------------------------------------------- 1 | #ifndef STATIC_TRACING_PROVIDER_H 2 | #define STATIC_TRACING_PROVIDER_H 3 | 4 | #include "usdt.h" 5 | 6 | #include "ruby_static_tracing.h" 7 | #include "tracepoint.h" 8 | 9 | typedef struct { 10 | char *name; 11 | usdt_provider_t *usdt_provider; 12 | } static_tracing_provider_t; 13 | 14 | /* 15 | * call-seq: 16 | * StaticTracing::Provider.new(id) -> provider 17 | * 18 | * Creates a new Provider. 19 | */ 20 | VALUE 21 | provider_initialize(VALUE self, VALUE id); 22 | 23 | /* 24 | * call-seq: 25 | * provider.enable() -> true 26 | * 27 | * Enable this provider 28 | * 29 | * Return true if the enable operation succeeded 30 | */ 31 | VALUE 32 | provider_enable(VALUE self); 33 | 34 | /* 35 | * call-seq: 36 | * provider.disable() -> true 37 | * 38 | * Disable this provider 39 | * 40 | * Return true if the disable operation succeeded 41 | */ 42 | VALUE 43 | provider_disable(VALUE self); 44 | /* 45 | * call-seq: 46 | * provider.destroy() -> true 47 | * 48 | * Destroys this provider. 49 | * 50 | * Return true if the destory operation succeeded 51 | */ 52 | VALUE 53 | provider_destroy(VALUE self); 54 | 55 | // Allocate a static_tracing_provider_type struct for ruby memory management 56 | VALUE 57 | static_tracing_provider_alloc(VALUE klass); 58 | 59 | int provider_add_tracepoint_internal(VALUE self, usdt_probedef_t *probedef); 60 | 61 | #endif // STATIC_TRACING_PROVIDER_H 62 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/tracers.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | # Tracers are a layer of abstraction above tracepoints. They are opinionated 5 | # and contextual ways of applying tracepoints to an application. 6 | class Tracers 7 | # Error for an invalid tracer 8 | class InvalidTracerError < StandardError 9 | def initialize 10 | msg = <<~MSG 11 | You need to add a valid tracer. 12 | 13 | To create a valid tracer please inherit from StaticTracing::Tracer::Base 14 | and follow the guide on how to create tracers 15 | MSG 16 | super(msg) 17 | end 18 | end 19 | 20 | class << self 21 | def add(tracer) 22 | raise InvalidTracerError unless tracer < StaticTracing::Tracer::Base 23 | 24 | tracers << tracer 25 | end 26 | 27 | # Enables each tracer, overriding original 28 | # method definition with traced one 29 | def enable! 30 | tracers.each(&:enable!) 31 | end 32 | 33 | # Disables each tracer, replacing the method definition 34 | def disable! 35 | tracers.each(&:disable!) 36 | end 37 | 38 | # Clears all tracers 39 | def clean 40 | # FIXME: - actuallly ensure destroyed to avoid memory leaks 41 | @tracers = [] 42 | end 43 | 44 | private 45 | 46 | def tracers 47 | @tracers ||= [] 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /test/ruby-static-tracing/tracepoint_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module StaticTracing 6 | class TracepointTest < MiniTest::Test 7 | def setup 8 | @tracepoint = Tracepoint.new('test', 'my_method', Integer, String) 9 | end 10 | 11 | def test_initialize_raises_when_args_is_not_supported 12 | assert_raises(Tracepoint::InvalidArgType) do 13 | Tracepoint.new('test', 'my_method', Integer, Float) 14 | end 15 | end 16 | 17 | def test_fire_match_the_right_args 18 | assert_raises(Tracepoint::InvalidArgumentError) do 19 | @tracepoint.fire('hello', 1) 20 | end 21 | 22 | @tracepoint.expects(:fire).once 23 | @tracepoint.fire(1, 'hello') 24 | end 25 | 26 | def test_tracepoint_implicitly_declare_provider 27 | p = StaticTracing::Provider.fetch(@tracepoint.provider_name) 28 | assert_equal(p.namespace, 'test') 29 | end 30 | 31 | def test_access_provider_through_tracepoint 32 | assert_equal(@tracepoint.provider.namespace, 'test') 33 | end 34 | 35 | def test_get_returns_tracepoint 36 | tracepoint = Provider.fetch('test').tracepoints['my_method'] 37 | assert_instance_of Tracepoint, tracepoint 38 | end 39 | 40 | def test_raises_error_if_tracepoint_does_not_exists 41 | assert_raises(StaticTracing::Tracepoint::TracepointMissingError) do 42 | Tracepoint.fetch('test', 'noop') 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | ruby-static-tracing (0.0.15) 5 | unmixer 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | ast (2.4.0) 11 | byebug (11.0.1) 12 | coderay (1.1.2) 13 | docile (1.3.2) 14 | jaro_winkler (1.5.3) 15 | json (2.2.0) 16 | metaclass (0.0.4) 17 | method_source (0.9.2) 18 | minitest (5.11.3) 19 | mocha (1.9.0) 20 | metaclass (~> 0.0.1) 21 | parallel (1.17.0) 22 | parser (2.6.3.0) 23 | ast (~> 2.4.0) 24 | pry (0.12.2) 25 | coderay (~> 1.1.0) 26 | method_source (~> 0.9.0) 27 | pry-byebug (3.7.0) 28 | byebug (~> 11.0) 29 | pry (~> 0.10) 30 | rainbow (3.0.0) 31 | rake (10.5.0) 32 | rake-compiler (0.9.9) 33 | rake 34 | rubocop (0.73.0) 35 | jaro_winkler (~> 1.5.1) 36 | parallel (~> 1.10) 37 | parser (>= 2.6) 38 | rainbow (>= 2.2.2, < 4.0) 39 | ruby-progressbar (~> 1.7) 40 | unicode-display_width (>= 1.4.0, < 1.7) 41 | ruby-progressbar (1.10.1) 42 | simplecov (0.17.0) 43 | docile (~> 1.1) 44 | json (>= 1.8, < 3) 45 | simplecov-html (~> 0.10.0) 46 | simplecov-html (0.10.2) 47 | unicode-display_width (1.6.0) 48 | unmixer (0.5.0) 49 | 50 | PLATFORMS 51 | ruby 52 | 53 | DEPENDENCIES 54 | minitest 55 | mocha 56 | pry-byebug 57 | rake (< 11.0) 58 | rake-compiler (~> 0.9) 59 | rubocop 60 | ruby-static-tracing! 61 | simplecov 62 | 63 | BUNDLED WITH 64 | 2.0.1 65 | -------------------------------------------------------------------------------- /test/ruby-static-tracing/tracer/latency_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module StaticTracing 6 | module Tracer 7 | class LatencyTest < MiniTest::Test 8 | class Example 9 | def noop; end 10 | Tracer::Latency.register(self, :noop) 11 | 12 | def noop_with_args(*args, arg1:) 13 | Array(args).map { |arg| arg1 + arg } 14 | end 15 | Tracer::Latency.register(self, :noop_with_args) 16 | end 17 | 18 | def setup 19 | @example = Example.new 20 | Tracer::Latency.enable! 21 | end 22 | 23 | def teardown 24 | Tracer::Latency.disable! 25 | Tracers.clean 26 | end 27 | 28 | def test_noop_will_fire_an_event_when 29 | StaticTracing::Tracepoint.any_instance.expects(:fire).once 30 | @example.noop 31 | end 32 | 33 | def test_disable_will_prevent_firing_an_event 34 | Tracer::Latency.disable! 35 | StaticTracing::Tracepoint.any_instance.expects(:fire).never 36 | @example.noop 37 | end 38 | 39 | def test_noop_with_args_will_fire_events 40 | StaticTracing::Tracepoint.any_instance.expects(:fire).once 41 | result = @example.noop_with_args(2, 3, arg1: 1) 42 | assert_equal([3, 4], result) 43 | end 44 | 45 | def test_noop_with_args_works_correctly_when_disabled 46 | StaticTracing::Tracepoint.any_instance.expects(:fire).never 47 | Tracer::Latency.disable! 48 | result = @example.noop_with_args(2, 3, arg1: 1) 49 | 50 | assert_equal([3, 4], result) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /docker/Dockerfile.ci: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.10 as builder 2 | ENV RUN_TESTS=0 3 | 4 | RUN apt-get update && apt-get install -y git wget gnupg && rm -rf /var/lib/apt/lists/* && apt-get clean 5 | 6 | RUN wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | apt-key add - 7 | COPY sources.list /etc/apt/sources.list.d/llvm.list 8 | 9 | RUN wget -O - https://repo.iovisor.org/GPG-KEY | apt-key add - 10 | RUN echo "deb https://repo.iovisor.org/apt/bionic bionic main" > /etc/apt/sources.list.d/iovisor.list 11 | 12 | RUN apt-get update 13 | 14 | RUN apt-get install -y bison cmake flex vim g++ libelf-dev zlib1g-dev libfl-dev curl git bc && apt-get clean 15 | RUN apt-get install -y systemtap-sdt-dev && apt-get clean 16 | RUN apt-get install -y clang-5.0 libclang-5.0-dev libclang-common-5.0-dev libclang1-5.0 libllvm5.0 llvm-5.0 llvm-5.0-dev llvm-5.0-runtime && apt-get clean 17 | RUN apt-get install -y clang-format-6.0 && ln -s /usr/bin/clang-format-6.0 /usr/bin/clang-format && apt-get clean 18 | RUN apt-get install -y libbcc=0.9.0-1 bcc-tools && apt-get clean 19 | 20 | RUN echo 'PATH="$PATH:/usr/share/bcc/tools"' >> /etc/bash.bashrc 21 | 22 | RUN apt-get install -y ruby ruby-dev && apt-get clean 23 | RUN gem install bundler 24 | 25 | COPY scripts/fetch-linux-headers.sh /tmp/fetch-linux-headers.sh 26 | RUN /tmp/fetch-linux-headers.sh 27 | 28 | ADD https://github.com/iovisor/bpftrace/archive/v0.9.tar.gz /bpftrace.tar.gz 29 | RUN tar -xvf /bpftrace.tar.gz 30 | 31 | RUN mv bpftrace-0.9 /bpftrace 32 | 33 | RUN mkdir /bpftrace/build 34 | 35 | WORKDIR /bpftrace/build 36 | 37 | RUN cmake -DCMAKE_BUILD_TYPE=DEBUG -DCMAKE_INSTALL_PREFIX=/usr/local/ .. 38 | RUN make -j9 39 | RUN make install 40 | 41 | WORKDIR /app 42 | -------------------------------------------------------------------------------- /ruby-static-tracing.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('lib', __dir__) 4 | 5 | require 'ruby-static-tracing/version' 6 | require 'ruby-static-tracing/platform' 7 | 8 | post_install_message = <<~eof 9 | This is alpha quality and not suitable for production use 10 | ... usless you're feeling bold ;) 11 | 12 | If you find any bugs, please file them at: 13 | github.com/shopify/ruby-static-tracing 14 | eof 15 | 16 | Gem::Specification.new do |s| 17 | s.name = 'ruby-static-tracing' 18 | s.version = StaticTracing::VERSION 19 | s.summary = 'USDT tracing for Ruby' 20 | s.post_install_message = post_install_message 21 | s.description = <<-DOC 22 | A Ruby C extension that enables defining static tracepoints 23 | from within a ruby context. 24 | DOC 25 | s.homepage = 'https://github.com/dalehamel/ruby-static-tracing' 26 | s.authors = ['Dale Hamel'] 27 | s.email = 'dale.hamel@srvthe.net' 28 | s.license = 'MIT' 29 | 30 | s.files = Dir['{lib,ext}/**/**/*.{rb,h,c,s}'] + 31 | Dir['ext/ruby-static-tracing/lib/libusdt/Makefile'] + 32 | Dir['ext/ruby-static-tracing/lib/libstapsdt/Makefile'] + 33 | s.extensions = ['ext/ruby-static-tracing/lib/deps-extconf.rb', 34 | 'ext/ruby-static-tracing/extconf.rb', 35 | 'ext/ruby-static-tracing/lib/post-extconf.rb'] 36 | s.add_dependency('unmixer') 37 | s.add_development_dependency 'minitest' 38 | s.add_development_dependency 'mocha' 39 | s.add_development_dependency 'pry-byebug' 40 | s.add_development_dependency 'rake', '< 11.0' 41 | s.add_development_dependency 'rake-compiler', '~> 0.9' 42 | s.add_development_dependency 'rubocop' 43 | s.add_development_dependency 'simplecov' 44 | end 45 | -------------------------------------------------------------------------------- /test/integration/README.md: -------------------------------------------------------------------------------- 1 | # Running integration tests 2 | 3 | Just use rake: 4 | 5 | `bundle exec rake integration` 6 | 7 | # Integration test anatomy 8 | 9 | This is the structure of the integration test suite: 10 | 11 | ``` 12 | integration 13 | ├── integration_helper.rb 14 | └── test_hello 15 | ├── hello.bt 16 | ├── hello.out 17 | ├── hello.rb 18 | └── hello_test.rb 19 | ``` 20 | 21 | Such that there is one test per subdirectory of the `integration` folder. 22 | 23 | Each correspond to: 24 | 25 | - `hello_test.rb`: A `_test.rb` file, the control logic for this test that will be executed by minitest. 26 | - `hello.rb` : A `.rb` file, which is a script that will be the target of our probe. 27 | - `hello.bt` : A `.bt` file, which is a bpf trace script containing our test probe. 28 | - `hello.out` : A text file, containing the expected output of running the probe script against the target file. 29 | 30 | # Adding tests 31 | 32 | We have a rake test that will create all the files needed for a new integration test. 33 | 34 | `bundle exec rake 'new:integration[new_of_test]'` 35 | 36 | # Tips 37 | 38 | Each of of the methods is `CommandRunner` takes an optional argument for the amount of time to sleep. 39 | 40 | This is a bit of a hack to avoid race conditions. If you can, you should ideally try and check in a blocking manner if the condition you would have expected has succeeded. 41 | 42 | Often though, you just need to wait for a buffer to flush, or a system call to completed. Typically 1 second is more than enough, and anything more than 5 seconds should be highly discouraged as it will make the suite slower. 43 | 44 | You may also want to force some synchrony between the tracer and the tracee using a signal, which is why i added the `usr2` helper. This can be used to induce the tracee to fire a probe at a predictable time. 45 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/darwin/ruby_static_tracing.c: -------------------------------------------------------------------------------- 1 | #include "ruby_static_tracing.h" 2 | 3 | VALUE eUSDT, eInternal; 4 | 5 | void Init_ruby_static_tracing() { 6 | VALUE cStaticTracing, cProvider, cTracepoint; 7 | 8 | cStaticTracing = rb_const_get(rb_cObject, rb_intern("StaticTracing")); 9 | 10 | /* 11 | * Document-class: StaticTracing::Provider 12 | * 13 | * Provider is the wrapper around libstapsdt 14 | */ 15 | cProvider = rb_const_get(cStaticTracing, rb_intern("Provider")); 16 | 17 | /* 18 | * Document-class: Statictracing::Tracepoint 19 | * 20 | * A Tracepoint is a wrapper around an SDTProbe 21 | */ 22 | cTracepoint = rb_const_get(cStaticTracing, rb_intern("Tracepoint")); 23 | 24 | /* Document-class: StaticTracing::SyscallError 25 | * 26 | * Represents failures to fire a tracepoint or register a provider 27 | */ 28 | eUSDT = rb_const_get(cStaticTracing, rb_intern("USDTError")); 29 | 30 | /* Document-class: StaticTracing::InternalError 31 | * 32 | * An internal StaticTracing error. These errors may constitute bugs. 33 | */ 34 | eInternal = rb_const_get(cStaticTracing, rb_intern("InternalError")); 35 | 36 | rb_define_alloc_func(cProvider, static_tracing_provider_alloc); 37 | rb_define_method(cProvider, "provider_initialize", provider_initialize, 1); 38 | rb_define_method(cProvider, "enable", provider_enable, 0); 39 | rb_define_method(cProvider, "disable", provider_disable, 0); 40 | rb_define_method(cProvider, "destroy", provider_destroy, 0); 41 | 42 | rb_define_alloc_func(cTracepoint, static_tracing_tracepoint_alloc); 43 | rb_define_method(cTracepoint, "tracepoint_initialize", tracepoint_initialize, 44 | 3); 45 | rb_define_method(cTracepoint, "_fire_tracepoint", tracepoint_fire, 1); 46 | rb_define_method(cTracepoint, "enabled?", tracepoint_enabled, 0); 47 | } 48 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/linux/provider.h: -------------------------------------------------------------------------------- 1 | #ifndef STATIC_TRACING_PROVIDER_H 2 | #define STATIC_TRACING_PROVIDER_H 3 | 4 | // Include libstapsdt.h to wrap 5 | #include // FIXME use local 6 | 7 | #include "ruby_static_tracing.h" 8 | #include "types.h" 9 | 10 | typedef struct { 11 | char *name; 12 | SDTProvider_t *sdt_provider; 13 | VALUE tracepoints; 14 | } static_tracing_provider_t; 15 | 16 | /* 17 | * call-seq: 18 | * StaticTracing::Provider.new(id) -> provider 19 | * 20 | * Creates a new Provider. 21 | */ 22 | VALUE 23 | provider_initialize(VALUE self, VALUE id); 24 | 25 | /* 26 | * call-seq: 27 | * provider.enable() -> true 28 | * 29 | * Enable this provider 30 | * 31 | * Return true if the enable operation succeeded 32 | */ 33 | VALUE 34 | provider_enable(VALUE self); 35 | 36 | /* 37 | * call-seq: 38 | * provider.disable() -> true 39 | * 40 | * Disable this provider 41 | * 42 | * Return true if the disable operation succeeded 43 | */ 44 | VALUE 45 | provider_disable(VALUE self); 46 | /* 47 | * call-seq: 48 | * provider.destroy() -> true 49 | * 50 | * Destroys this provider. 51 | * 52 | * Return true if the destory operation succeeded 53 | */ 54 | VALUE 55 | provider_destroy(VALUE self); 56 | 57 | /* 58 | * call-seq: 59 | * provider.path() -> string 60 | * 61 | * Get a path to this provider, if the platform supports it 62 | * 63 | * Returns string for path, or empty string if failed 64 | */ 65 | VALUE 66 | provider_path(VALUE self); 67 | 68 | // Allocate a static_tracing_provider_type struct for ruby memory management 69 | VALUE 70 | static_tracing_provider_alloc(VALUE klass); 71 | 72 | SDTProbe_t *provider_add_tracepoint_internal(VALUE self, const char *name, 73 | int argc, 74 | Tracepoint_arg_types *args); 75 | 76 | #endif // STATIC_TRACING_PROVIDER_H 77 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/lib/deps-extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) 4 | 5 | require 'mkmf' 6 | require 'ruby-static-tracing/platform' 7 | 8 | BASE_DIR = __dir__ 9 | LIB_DIR = File.expand_path('../../../lib/ruby-static-tracing', __dir__) 10 | 11 | # FIXME: have this install libstapsdt 12 | if StaticTracing::Platform.linux? 13 | # This is a bit of a hack to compile libstapsdt.so 14 | # and "trick" extconf into thinking it's just another .so 15 | File.write 'Makefile', <<~MAKEFILE 16 | all: 17 | cd #{File.join(BASE_DIR, 'libstapsdt')} && make CFLAGS_EXTRA=-DLIBSTAPSDT_MEMORY_BACKED_FD 18 | touch deps.so # HACK 19 | cp #{File.join(BASE_DIR, 'libstapsdt', 'out/libstapsdt.so.0')} #{LIB_DIR} 20 | cd #{LIB_DIR} && ln -sf libstapsdt.so.0 libstapsdt.so 21 | clean: 22 | cd #{File.join(BASE_DIR, 'libstapsdt')} && make clean 23 | install: 24 | MAKEFILE 25 | exit 26 | # We'll build libusdt and install and update linker info 27 | elsif StaticTracing::Platform.darwin? 28 | # This is a bit of a hack to compile libusdt.dylib 29 | # and "trick" extconf into thinking it's just another .bundle 30 | # After installing it (in post-extconf), we forcefully update the load path for 31 | # ruby_static_tracing.bundle to find it in the same directory 32 | File.write 'Makefile', <<~MAKEFILE 33 | all: 34 | cd #{File.join(BASE_DIR, 'libusdt')} && make libusdt.dylib 35 | touch deps.bundle # HACK 36 | cp #{File.join(BASE_DIR, 'libusdt', 'libusdt.dylib')} #{LIB_DIR} 37 | clean: 38 | cd #{File.join(BASE_DIR, 'libusdt')} && make clean 39 | install: 40 | MAKEFILE 41 | exit 42 | else 43 | # - Stub, for other platforms that we don't support, we write an empty makefile 44 | File.write 'Makefile', <<~MAKEFILE 45 | all: 46 | clean: 47 | install: 48 | MAKEFILE 49 | exit 50 | end 51 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/linux/ruby_static_tracing.c: -------------------------------------------------------------------------------- 1 | #include "ruby_static_tracing.h" 2 | 3 | VALUE eUSDT, eInternal; 4 | 5 | void Init_ruby_static_tracing() { 6 | VALUE cStaticTracing, cProvider, cTracepoint; 7 | 8 | cStaticTracing = rb_const_get(rb_cObject, rb_intern("StaticTracing")); 9 | 10 | /* 11 | * Document-class: StaticTracing::Provider 12 | * 13 | * Provider is the wrapper around libstapsdt 14 | */ 15 | cProvider = rb_const_get(cStaticTracing, rb_intern("Provider")); 16 | 17 | /* 18 | * Document-class: Statictracing::Tracepoint 19 | * 20 | * A Tracepoint is a wrapper around an SDTProbe 21 | */ 22 | cTracepoint = rb_const_get(cStaticTracing, rb_intern("Tracepoint")); 23 | 24 | /* Document-class: StaticTracing::SyscallError 25 | * 26 | * Represents failures to fire a tracepoint or register a provider 27 | */ 28 | eUSDT = rb_const_get(cStaticTracing, rb_intern("USDTError")); 29 | 30 | /* Document-class: StaticTracing::InternalError 31 | * 32 | * An internal StaticTracing error. These errors may constitute bugs. 33 | */ 34 | eInternal = rb_const_get(cStaticTracing, rb_intern("InternalError")); 35 | 36 | rb_define_alloc_func(cProvider, static_tracing_provider_alloc); 37 | rb_define_method(cProvider, "provider_initialize", provider_initialize, 1); 38 | rb_define_method(cProvider, "_enable_provider", provider_enable, 0); 39 | rb_define_method(cProvider, "_disable_provider", provider_disable, 0); 40 | rb_define_method(cProvider, "destroy", provider_destroy, 0); 41 | rb_define_method(cProvider, "path", provider_path, 0); 42 | 43 | rb_define_alloc_func(cTracepoint, static_tracing_tracepoint_alloc); 44 | rb_define_method(cTracepoint, "tracepoint_initialize", tracepoint_initialize, 45 | 3); 46 | rb_define_method(cTracepoint, "_fire_tracepoint", tracepoint_fire, 1); 47 | rb_define_method(cTracepoint, "enabled?", tracepoint_enabled, 0); 48 | } 49 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/configuration.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | class Configuration 5 | # Modes of operation for tracers 6 | module Modes 7 | ON = 'ON' 8 | OFF = 'OFF' 9 | SIGNAL = 'SIGNAL' 10 | 11 | module SIGNALS 12 | SIGPROF = 'PROF' 13 | end 14 | end 15 | 16 | class << self 17 | def instance 18 | @instance ||= new 19 | end 20 | end 21 | 22 | attr_reader :mode, :signal 23 | 24 | # A new configuration instance 25 | def initialize 26 | @mode = Modes::SIGNAL 27 | @signal = Modes::SIGNALS::SIGPROF 28 | enable_trap 29 | end 30 | 31 | # Sets the mode [ON, OFF, SIGNAL] 32 | # Default is SIGNAL 33 | def mode=(new_mode) 34 | handle_old_mode 35 | @mode = new_mode 36 | handle_new_mode 37 | end 38 | 39 | # Sets the SIGNAL to listen to, 40 | # Default is SIGPROF 41 | def signal=(new_signal) 42 | disable_trap 43 | @signal = new_signal 44 | enable_trap 45 | end 46 | 47 | # Adds a new tracer globally 48 | def add_tracer(tracer) 49 | Tracers.add(tracer) 50 | end 51 | 52 | private 53 | 54 | # Clean up trap handlers if mode changed to not need it 55 | def handle_old_mode 56 | disable_trap if @mode == Modes::SIGNAL 57 | end 58 | 59 | # Enable trap handlers if needed 60 | def handle_new_mode 61 | if @mode == Modes::SIGNAL 62 | enable_trap 63 | elsif @mode == Modes::ON 64 | StaticTracing.enable! 65 | elsif @mode == Modes::OFF 66 | StaticTracing.disable! 67 | end 68 | end 69 | 70 | # Disables trap handler 71 | def disable_trap 72 | Signal.trap(@signal, 'DEFAULT') 73 | end 74 | 75 | # Enables a new trap handler 76 | def enable_trap 77 | Signal.trap(@signal) { StaticTracing.toggle_tracing! } 78 | end 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /test/ruby-static-tracing/tracer/stack_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module StaticTracing 6 | module Tracer 7 | class StackTest < MiniTest::Test 8 | class Example 9 | def noop 10 | true 11 | end 12 | 13 | def noop_with_arg(foo) 14 | foo 15 | end 16 | 17 | def noop_with_block 18 | yield 19 | end 20 | 21 | def noop_with_arg_and_block(foo) 22 | yield foo 23 | end 24 | 25 | StaticTracing::Tracer::Stack 26 | .register(self, :noop, :noop_with_arg, :noop_with_block, 27 | :noop_with_arg_and_block) 28 | end 29 | 30 | def teardown 31 | Tracer::Stack.disable! 32 | Tracers.clean 33 | end 34 | 35 | def test_basic_methods_fire_tracepoints 36 | Tracer::Stack.enable! 37 | StaticTracing::Tracepoint.any_instance.expects(:fire).once 38 | 39 | @example = Example.new 40 | 41 | assert_equal(true, @example.noop) 42 | end 43 | 44 | def test_methods_with_args_still_work 45 | Tracer::Stack.enable! 46 | StaticTracing::Tracepoint.any_instance.expects(:fire).once 47 | 48 | @example = Example.new 49 | 50 | assert_equal(1, @example.noop_with_arg(1)) 51 | end 52 | 53 | def test_methods_with_blocks_still_work 54 | Tracer::Stack.enable! 55 | StaticTracing::Tracepoint.any_instance.expects(:fire).once 56 | 57 | @example = Example.new 58 | 59 | assert_equal(1, @example.noop_with_block { 1 }) 60 | end 61 | 62 | def test_methods_with_blocks_and_args_still_work 63 | Tracer::Stack.enable! 64 | StaticTracing::Tracepoint.any_instance.expects(:fire).once 65 | 66 | @example = Example.new 67 | 68 | assert_equal(1, @example.noop_with_arg_and_block(1) { |a| a }) 69 | end 70 | end 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | services: 2 | - docker 3 | language: ruby 4 | before_install: 5 | - bundle install 6 | - bundle exec rake docker:pull 7 | - bundle exec rake docker:run 8 | - bundle exec rake docker:install 9 | script: 10 | - bundle exec rake rdoc 11 | - bundle exec rake docker:rubocop 12 | - bundle exec rake docker:clangfmt 13 | - bundle exec rake docker:test 14 | - rm .gitignore 15 | - sudo mv html /html && sudo mv coverage /html/coverage && sudo mv README.md /html && sudo rm -rf * && sudo mv /html/* . 16 | env: 17 | matrix: 18 | - secure: usNTQBSlH7EhIn4NMynem1TZ01DMLZ23nZF3XSbDIcJC7FxfoImn7pvHHVXD98NKlzrV6Iz4BiuuhfuzAKlQE4k5p74Vv8kPL4catSX07r4dmb6UL62LDv2Ra3tRDCLCvSGYm3+hhxoGjOupnaTFwVyIR1yF9dSD+PkxXgWSyhbCrXBXXe23l1ew8PXRW2KiYGqk4z/Zjq6np10xkwo/mmHmJw0u3R7Bwwv7Qm6pG+IKHFWv53fRESi+XXntGWmzXdkymLCd6umQnvt2lOxbQnJuwx7tZcGZcl4ldGnXD98LIyQQyY+8vZzBAO7shywVGx6sxfMFwmBwMyBTEKG6GnJ37EBfmIZ6ybovt7Xdzar2RcDE67NSkoxsOvi7g8fNoClF8QLUCXG7+AhfTYzVwyGEiB76W+JvzDsh8DfQJ171UHCrWDdHIquSgzrIqPhzf3z0Vn+nztpJkp7ZHrTZhBH6kqx48q+CqAt/fgX8DkdXOlPNndXK+Ab7/vFhO9IVFF7Po7vTim+C6l/+Y4VngNGLFaQpMNtoeymOA+4AclL0d+IvmLtP+dpS7BuMeWGcmgxnxulYYrFxcQOWuntQ85NxWNs4in+E7meWYylAz4lkOVKDSOr1qSa5g4IxH8XjLY8YVJ1QfH/v3w1yyeE24xNRLLhYtumIHXqtZdu+w70= 19 | deploy: 20 | - provider: rubygems 21 | api_key: 22 | secure: ivejPWs0XTIEtc1D9suDnusfoP3UnqGSNdfWd8socndHt9dF777CZzUmbCIZe1cypQWW+cNOt2v+hSbPat0ID80axOV1zkinfehy5uG4vGdCZ7MtcIwjmYOp9LJoTJuN/jM8skrudJGwGJUuRtXybpd4F5s6YjF7QwCikjDdTMxMqquVYT8R4Qw5ZqRedINIhnRHnAAJD6l/U8u+0BmqJMC1UyiIihoc3QdquQquu0gY6c6shQHR0J2wFB+KTviV6IGp/fsSY4nBwZaECAAI/xoHpQOhUY61ejRbu+bRa3xiQe+WCBlij3PxROSlGZUasyWDHYqmgrCN/4pHVaUQCLibYicD8OUw/L0Pn4gptgXMb8xklFPiHbLPayHLmAmtmhyNZpovg0kJ2jFYJgxlmvLfiyrnAwUQrWun6WGhWM9YZU6SfZXS7oBkK8RKFdgXpQyI5gN3mtpc8XykmuLLCd6J6oAJWg6B3rsCbM5jrmA2JKLNAm2R7tqlWU5c7+bKQ9gecS8lQO5ZAx6GRSo1qoFXQoilYQMur2X3SlGfaY2NjN056/cgRkqdoYMkMkwp96/ixXE+u2jHKUAx7i2NMERMOzWeu/Ir/LC/RjidODTY9Fw44y6rd/cqmvxXCbMSMkqHgxkzu+XrYArxCvDuM3AvBVurbPM7JPZgcFmxz9M= 23 | gem: ruby-static-tracing 24 | on: 25 | tags: true 26 | repo: dalehamel/ruby-static-tracing 27 | - provider: pages 28 | skip_cleanup: true 29 | github_token: "$GITHUB_TOKEN" 30 | target_branch: gh-pages 31 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/tracer/base.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'unmixer' 4 | using Unmixer 5 | 6 | require 'ruby-static-tracing/tracer/helpers' 7 | 8 | module StaticTracing 9 | module Tracer 10 | class Base 11 | class << self 12 | include Tracer::Helpers 13 | 14 | def register(klass, *method_names, provider: nil) 15 | provider_name ||= underscore(klass.name) 16 | provider = Provider.register(provider_name) 17 | method_overrides = function_wrapper.new(provider, @wrapping_function, @data_types) 18 | modified_classes[klass] ||= method_overrides 19 | modified_classes[klass].add_override(method_names.flatten) 20 | end 21 | 22 | def enable! 23 | modified_classes.each do |klass, wrapped_methods| 24 | klass.prepend(wrapped_methods) 25 | end 26 | end 27 | 28 | def disable! 29 | modified_classes.each do |klass, wrapped_methods| 30 | klass.instance_eval { unprepend(wrapped_methods) } 31 | end 32 | end 33 | 34 | private 35 | 36 | def function_wrapper 37 | Class.new(Module) do 38 | def initialize(provider, wrapping_function, data_types) 39 | @provider = provider 40 | @wrapping_function = wrapping_function 41 | @data_types = data_types 42 | end 43 | 44 | def add_override(methods) 45 | methods.each do |method| 46 | Tracepoint.new(@provider.namespace, method.to_s, *@data_types) 47 | define_method(method.to_s, @wrapping_function) 48 | end 49 | end 50 | 51 | attr_reader :provider 52 | end 53 | end 54 | 55 | def modified_classes 56 | @modified_classes ||= {} 57 | end 58 | 59 | def set_tracepoint_data_types(*args) 60 | @data_types = *args 61 | end 62 | 63 | def tracepoint_data_types 64 | @data_types 65 | end 66 | 67 | def set_wrapping_function(callable) 68 | @wrapping_function = callable 69 | end 70 | end 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /test/ruby-static-tracing/provider_test.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'test_helper' 4 | 5 | module StaticTracing 6 | class ProviderTest < MiniTest::Test 7 | def setup 8 | @namespace = 'tracing' 9 | @provider = Provider.register(@namespace) 10 | end 11 | 12 | def test_instance_not_found 13 | assert_raises Provider::ProviderMissingError do 14 | Provider.fetch('not_registered') 15 | end 16 | end 17 | 18 | def test_provider 19 | assert_equal @provider, Provider.fetch(@namespace) 20 | end 21 | 22 | def test_add_tracepoint 23 | tracepoint = @provider.add_tracepoint('my_method', Integer, String) 24 | assert_instance_of Tracepoint, tracepoint 25 | assert_equal @provider.tracepoints.length, 1 26 | end 27 | 28 | def test_provider_starts_disabled 29 | p = Provider.register('starts_disabled') 30 | refute p.enabled? 31 | refute @provider.enabled? 32 | end 33 | 34 | def test_new_provider_empty_path 35 | assert_empty(@provider.path) 36 | end 37 | 38 | def test_enable_provider 39 | refute(@provider.enabled?) 40 | assert(@provider.enable) 41 | assert(@provider.enabled?) 42 | @provider.disable 43 | end 44 | 45 | # FIXME: this is expected to fail on darwin 46 | def test_enabled_provider_has_nonempty_path 47 | refute(@provider.enabled?) 48 | assert(@provider.enable) 49 | assert(@provider.enabled?) 50 | refute_empty(@provider.path) 51 | @provider.disable 52 | end 53 | 54 | def test_disabled_provider_has_empty_path 55 | refute(@provider.enabled?) 56 | assert(@provider.enable) 57 | assert(@provider.enabled?) 58 | @provider.disable 59 | assert_empty(@provider.path) 60 | end 61 | 62 | def test_disable_provider 63 | refute(@provider.enabled?) 64 | assert(@provider.enable) 65 | assert(@provider.enabled?) 66 | @provider.disable 67 | refute(@provider.enabled?) 68 | end 69 | 70 | def test_raises_error_if_provider_does_not_exists 71 | Tracepoint.new('test', 'my_method', Integer, String) 72 | assert_raises(StaticTracing::Provider::ProviderMissingError) do 73 | Provider.fetch('noop') 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/tracepoint.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | class Tracepoint 5 | class TracepointMissingError < StandardError; end 6 | 7 | class << self 8 | # Gets a trace instance by provider name and name 9 | def fetch(provider, name) 10 | Provider.fetch(provider).tracepoints.fetch(name.to_s) do 11 | raise TracepointMissingError 12 | end 13 | end 14 | end 15 | 16 | class InvalidArgumentError < StandardError 17 | def initialize(argument, expected_type) 18 | error_message = <<~ERROR_MESSAGE 19 | 20 | We expected the fire arguments to match with the ones specified on the creation of the Tracepoint 21 | 22 | You passed #{argument} => #{argument.class} and we expected the argument to be type #{expected_type} 23 | ERROR_MESSAGE 24 | super(error_message) 25 | end 26 | end 27 | class InvalidArgType < StandardError; end 28 | 29 | VALID_ARGS_TYPES = [Integer, String].freeze 30 | 31 | attr_reader :provider_name, :name, :args 32 | 33 | # Creates a new tracepoint. 34 | # If a provider by the name specified doesn't exist already, 35 | # one will be added implicitly. 36 | def initialize(provider_name, name, *args) 37 | @provider_name = provider_name 38 | @name = name 39 | validate_args(args) 40 | @args = args 41 | 42 | if StaticTracing::Platform.supported_platform? 43 | tracepoint_initialize(provider_name, name, args) 44 | provider.add_tracepoint(self) 45 | else 46 | StaticTracing.issue_disabled_tracepoints_warning 47 | end 48 | end 49 | 50 | # Fire a tracepoint, sending the data off to be received by 51 | # a tracing program like dtrace 52 | def fire(*values) 53 | values.each_with_index do |arg, i| 54 | raise InvalidArgumentError.new(arg, args[i]) unless arg.is_a?(args[i]) 55 | end 56 | _fire_tracepoint(values) 57 | end 58 | 59 | # The provider this tracepoint is defined on 60 | def provider 61 | Provider.fetch(@provider_name) 62 | end 63 | 64 | # Returns true if a tracepoint is currently 65 | # attached to, indicating we should fire it 66 | def enabled?; end 67 | 68 | private 69 | 70 | def validate_args(values) 71 | raise InvalidArgType unless values.all? { |value| VALID_ARGS_TYPES.include?(value) } 72 | end 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /docker/Dockerfile.old: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | # THIS DOCKERFILE IS DEPRECATED 4 | # But it does show how to build a ruby image with dtrace probes enabled using ruby-install further down so i'm keeping it for posterity, for now 5 | 6 | 7 | 8 | # This dockerfile is multi-purpose, to serve as a complete development environment for the projcet. 9 | # It will set up an environment te build against ruby 2.4, with dtrace probes enabled for some functional tests. 10 | # To actually run USDT probes, this container will probably need to be run privileged in CI. 11 | 12 | # Add the repository 13 | RUN apt-get update && apt-get install -y wget gnupg && gpg --keyserver keyserver.ubuntu.com --recv ED0AF6FB72C51845 && rm -rf /var/lib/apt/lists/* && apt-get clean 14 | RUN gpg --export --armor ED0AF6FB72C51845 | apt-key add - 15 | RUN echo 'deb http://ppa.launchpad.net/sthima/oss/ubuntu xenial main' >> /etc/apt/sources.list.d/libstapsdt.list 16 | 17 | # Install dependencies 18 | # systemtap-sdt-dev is to have stubs for ruby dtrace probes 19 | # libstapsdt is sfor wrapping for gem 20 | RUN apt-get update && apt-get install -y systemtap-sdt-dev libstapsdt0 libstapsdt-dev && rm -rf /var/lib/apt/lists/* && apt-get clean 21 | 22 | # Install ruby build deps FIXME - abstract this away into builder? 23 | RUN apt-get update && apt-get install -y build-essential \ 24 | bison \ 25 | zlib1g-dev \ 26 | libyaml-dev \ 27 | libssl-dev \ 28 | libgdbm-dev \ 29 | libreadline-dev \ 30 | libncurses5-dev \ 31 | libffi-dev && rm -rf /var/lib/apt/lists/* && apt-get clean 32 | 33 | # Install a ruby with dtrace probes enabled 34 | # this is to ensure that we have a fully-featured ruby 35 | RUN wget -O ruby-install-0.7.0.tar.gz https://github.com/postmodern/ruby-install/archive/v0.7.0.tar.gz && \ 36 | tar -xzvf ruby-install-0.7.0.tar.gz && \ 37 | cd ruby-install-0.7.0 && \ 38 | make install && rm -rf /ruby-install* 39 | RUN ruby-install ruby 2.5.5 -- --enable-dtrace && rm -rf /usr/local/src/ruby* 40 | ENV PATH=${PATH}:/opt/rubies/ruby-2.5.5/bin/ 41 | RUN echo 'PATH=${PATH}:/opt/rubies/ruby-2.5.5/bin/' >> /etc/bash.bashrc 42 | RUN gem install bundler:1.17.3 43 | WORKDIR /app 44 | COPY . . 45 | RUN bundle install 46 | -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | # Linux 2 | 3 | On linux, we depend on eBPF. 4 | 5 | This functionality depends on having both a linux machine, and a recent linux kernel (4.18+ ideally). 6 | 7 | This means we must set up a virtual machine to run linux for us. 8 | 9 | ## Vagrant 10 | 11 | We'll use vagrant to get a VM up and running. To provision the vagrant VM, you should just need to: 12 | 13 | * Install vagrant for mac from https://www.vagrantup.com/downloads.html 14 | * Install virtualbox for mac from https://www.virtualbox.org/wiki/Downloads 15 | 16 | If you've previously installed vagrant, blow away any old gems if you need to. 17 | 18 | Once vagrant is installed, calling `vagrant up` from inside this repository should get everything set up. 19 | 20 | You must run `vagrant ssh` to connect from your laptop to tho development VM. 21 | 22 | Vagrant will share you application source directory at /vagrant, so you can use a local editor and the changes should be reflected in the VM. 23 | 24 | Vagrant is just used to get us access to: 25 | 26 | * A modern kernel (ubuntu cosmic ships with 4.18) 27 | * A working docker daemon 28 | 29 | ## Docker 30 | 31 | The development environment is packaged up as a docker container, which you can access from vagrant. 32 | 33 | `vagrant up` should already have set up the docker container for you, so you should just be able to run: 34 | 35 | ``` 36 | bundle exec rake docker:shell 37 | ``` 38 | 39 | To get access to a shell. 40 | 41 | Or, individually: 42 | 43 | ``` 44 | bundle exec rake docker:build 45 | bundle exec rake docker:run 46 | bundle exec rake docker:shell 47 | ``` 48 | 49 | From within this shell, you will now be running with your current working directory mounted within a linux environment. 50 | 51 | This should build you a container with suitable deps to get going to be able to build the gem and run unit tests. 52 | 53 | # Darwin 54 | 55 | On darwin, you use dtrace. 56 | 57 | # Testing 58 | 59 | We use minitest for this gem's tests. 60 | 61 | ## Unit tests 62 | 63 | We have unit tests, they can be run with: 64 | 65 | ``` 66 | bundle exec rake test 67 | ``` 68 | 69 | ## Integration tests 70 | 71 | We have integration tests, they can be run with: 72 | 73 | ``` 74 | bundle exec rake integration 75 | ``` 76 | 77 | You will need a system that can actually support probes (new enough kernel/eBPF support, dtrace) in order to run integration tests. 78 | 79 | The integration tests are [described further in their README](./test/integration/README.md) 80 | 81 | # Dependencies 82 | 83 | ## Linux 84 | 85 | To build libstapsdt, you must have libelf. Install it for your system, along with related development packages. 86 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'logger' 4 | 5 | require 'ruby-static-tracing/version' 6 | require 'ruby-static-tracing/platform' 7 | require 'ruby-static-tracing/provider' 8 | require 'ruby-static-tracing/tracepoint' 9 | require 'ruby-static-tracing/configuration' 10 | require 'ruby-static-tracing/tracer' 11 | require 'ruby-static-tracing/tracers' 12 | 13 | # FIXME: Including StaticTracing should cause every method in a module or class to be registered 14 | # Implement this by introspecting all methods on the includor, and wrapping them. 15 | module StaticTracing 16 | extend self 17 | 18 | BaseError = Class.new(StandardError) 19 | USDTError = Class.new(BaseError) 20 | InternalError = Class.new(BaseError) 21 | 22 | attr_accessor :logger 23 | 24 | self.logger = Logger.new(STDERR) 25 | 26 | # This will print a message indicating that tracepoints are disabled on this platform 27 | def issue_disabled_tracepoints_warning 28 | return if defined?(@warning_issued) 29 | 30 | @warning_issued = true 31 | logger.info("USDT tracepoints are not presently supported supported on #{RUBY_PLATFORM} - all operations will no-op") 32 | end 33 | 34 | # Efficiently return the current monotonic clocktime. 35 | # Wraps libc clock_gettime 36 | # The overhead of this is tested to be on the order of 10 microseconds under normal conditions 37 | # You should inline this method in your tracer to avoid an extra method call. 38 | def nsec 39 | Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond) 40 | end 41 | 42 | # Should indicate if static tracing is enabled - a global constant 43 | def enabled? 44 | !!@enabled 45 | end 46 | 47 | # Overwrite the definition of all functions that are enabled 48 | # with a wrapped version that has tracing enabled 49 | def enable! 50 | StaticTracing::Tracers.enable! 51 | StaticTracing::Provider.enable! # FIXME individually call enable 52 | @enabled = true 53 | end 54 | 55 | # Overwrite the definition of all functions to their original definition, 56 | # no longer wrapping them 57 | def disable! 58 | StaticTracing::Tracers.disable! 59 | StaticTracing::Provider.disable! # FIXME dangerous 60 | @enabled = false 61 | end 62 | 63 | # Toggles between enabling and disabling tracepoints declared through tracers 64 | def toggle_tracing! 65 | enabled? ? disable! : enable! 66 | end 67 | 68 | # Block to configure static tracing, eg: 69 | # 70 | # StaticTracing.configure do |config| 71 | # config.add_tracer(StaticTracing::Tracer::Latency) 72 | # end 73 | def configure 74 | yield Configuration.instance 75 | end 76 | end 77 | 78 | require 'ruby-static-tracing/ruby_static_tracing' if StaticTracing::Platform.supported_platform? 79 | -------------------------------------------------------------------------------- /lib/ruby-static-tracing/provider.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module StaticTracing 4 | # A wrapper for a USDT tracepoint provider 5 | # This corresponds to a namespace of tracepoints 6 | # By convention, we will often create one per 7 | # class or module. 8 | class Provider 9 | attr_accessor :name 10 | 11 | # Provider couldn't be found in collection 12 | class ProviderMissingError < StandardError; end 13 | 14 | class << self 15 | # Gets a provider by name 16 | # or creates one if not exists 17 | def register(namespace) 18 | providers[namespace] ||= new(namespace) 19 | end 20 | 21 | # Gets a provider instance by name 22 | def fetch(namespace) 23 | providers.fetch(namespace) do 24 | raise ProviderMissingError 25 | end 26 | end 27 | 28 | # Enables each provider, ensuring 29 | # it is loaded into memeory 30 | def enable! 31 | providers.values.each(&:enable) 32 | end 33 | 34 | # Forcefully disables all providers, 35 | # unloading them from memory 36 | def disable! 37 | providers.values.each(&:disable) 38 | end 39 | 40 | private 41 | 42 | # A global collection of providers 43 | def providers 44 | @providers ||= {} 45 | end 46 | end 47 | 48 | attr_reader :namespace, :tracepoints 49 | 50 | # Adds a new tracepoint to this provider 51 | # FIXME - should this be a dictionary, or are duplicate names allowed? 52 | def add_tracepoint(tracepoint, *args) 53 | if tracepoint.is_a?(String) 54 | tracepoint = Tracepoint.new(namespace, tracepoint, *args) 55 | elsif tracepoint.is_a?(Tracepoint) 56 | @tracepoints[tracepoint.name] = tracepoint 57 | end 58 | end 59 | 60 | # Enable the provider, loading it into memory 61 | def enable 62 | @enabled = _enable_provider 63 | end 64 | 65 | # Disables the provider, unloading it from memory 66 | def disable 67 | @enabled = !_disable_provider 68 | end 69 | 70 | # Returns true if the provider is enabled, 71 | # meaning it is loaded into memory 72 | def enabled? 73 | @enabled 74 | end 75 | 76 | # Only supported on systems (linux) where backed by file 77 | def path; end 78 | 79 | # Completely removes the provider 80 | def destroy; end 81 | 82 | private 83 | 84 | # ALWAYS use register, never call .new dilectly 85 | def initialize(namespace) 86 | if StaticTracing::Platform.supported_platform? 87 | provider_initialize(namespace) 88 | @enabled = false 89 | else 90 | StaticTracing.issue_disabled_tracepoints_warning 91 | end 92 | @namespace = namespace 93 | @tracepoints = {} 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/extconf.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | $LOAD_PATH.unshift File.expand_path('../../lib', __dir__).freeze 4 | 5 | require 'mkmf' 6 | require 'ruby-static-tracing/platform' 7 | 8 | BASE_DIR = __dir__ 9 | LIB_DIR = File.expand_path('../../lib/ruby-static-tracing', __dir__) 10 | 11 | MKMF_TARGET = 'ruby-static-tracing/ruby_static_tracing' 12 | 13 | def platform_dir(platform) 14 | File.expand_path("../../../ext/ruby-static-tracing/#{platform}/", __FILE__) 15 | end 16 | 17 | def lib_dir 18 | File.expand_path('../../lib/ruby-static-tracing', __dir__) 19 | end 20 | # - Linux, via libstapsdt 21 | if StaticTracing::Platform.linux? 22 | 23 | LIB_DIRS = [LIB_DIR, RbConfig::CONFIG['libdir']].freeze 24 | HEADER_DIRS = [ 25 | File.join(BASE_DIR, 'include'), 26 | File.join(BASE_DIR, 'lib', 'libstapsdt', 'src'), 27 | RbConfig::CONFIG['includedir'] 28 | ].freeze 29 | 30 | puts HEADER_DIRS.inspect 31 | dir_config(MKMF_TARGET, HEADER_DIRS, LIB_DIRS) 32 | 33 | abort 'libstapsdt.h is missing, please install libstapsdt' unless find_header('libstapsdt.h') 34 | have_header 'libstapsdt.h' 35 | have_header 'ruby_static_tracing.h' 36 | 37 | unless have_library('stapsdt') 38 | abort 'libstapsdt is missing, please install it' 39 | end 40 | 41 | $CFLAGS = '-D_GNU_SOURCE -Wall ' # -Werror complaining 42 | $CFLAGS += if ENV.key?('DEBUG') 43 | '-O0 -g -DDEBUG' 44 | else 45 | '-O3' 46 | end 47 | 48 | $LDFLAGS += " -Wl,-rpath='\$\$ORIGIN/../ruby-static-tracing' " 49 | 50 | create_makefile(MKMF_TARGET, platform_dir('linux')) 51 | 52 | # - Darwin/BSD and other dtrace platforms, via libusdt 53 | elsif StaticTracing::Platform.darwin? 54 | abort 'dtrace is missing, this platform is not supported' unless have_library('dtrace', 'dtrace_open') 55 | 56 | LIB_DIRS = [LIB_DIR, RbConfig::CONFIG['libdir']].freeze 57 | puts LIB_DIRS.inspect 58 | HEADER_DIRS = [ 59 | File.join(BASE_DIR, 'include'), 60 | File.join(BASE_DIR, 'lib', 'libusdt'), 61 | RbConfig::CONFIG['includedir'] 62 | ].freeze 63 | 64 | dir_config(MKMF_TARGET, HEADER_DIRS, LIB_DIRS) 65 | 66 | have_header('usdt.h') 67 | abort 'ERROR: libusdt is required. It is included, so this failure is an error.' unless have_library('usdt') 68 | 69 | $CFLAGS = '-D_GNU_SOURCE -Wall ' # -Werror complaining 70 | $CFLAGS << if ENV.key?('DEBUG') 71 | '-O0 -g -DDEBUG' 72 | else 73 | '-O3' 74 | end 75 | 76 | create_makefile(MKMF_TARGET, platform_dir('darwin')) 77 | else 78 | # - Stub, for other platforms that support neither 79 | # for now, we will yolo stub this to leave room to handle platforms 80 | # that support properly support conventional dtrace 81 | File.write 'Makefile', <<~MAKEFILE 82 | all: 83 | clean: 84 | install: 85 | MAKEFILE 86 | exit 87 | end 88 | -------------------------------------------------------------------------------- /lib/tasks/docker.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # Quick helpers to get a dev env set up 4 | namespace :docker do 5 | desc 'Builds the development docker image' 6 | task :build do 7 | system("docker build -f #{File.join(DOCKER_DIR, 'Dockerfile.ci')} #{DOCKER_DIR} -t quay.io/dalehamel/ruby-static-tracing") 8 | end 9 | 10 | desc 'Runs the development docker image' 11 | task :run do 12 | `docker run --privileged --name ruby-static-tracing-#{Time.now.getutc.to_i} -v $(pwd):/app -d quay.io/dalehamel/ruby-static-tracing:latest /bin/sh -c "sleep infinity"`.strip 13 | system("docker exec -ti #{latest_running_container_id} /app/vagrant/debugfs.sh") 14 | end 15 | 16 | desc 'Provides a shell within the development docker image' 17 | task :shell do 18 | system("docker exec -ti #{latest_running_container_id} bash") 19 | end 20 | 21 | desc 'Build and install the gem' 22 | task :install do 23 | system("docker exec -ti #{latest_running_container_id} bash -c 'bundle install && bundle exec rake install'") 24 | end 25 | desc 'Runs integration tests within the development docker image' 26 | task :integration do 27 | system("docker exec -ti #{latest_running_container_id} bash -c 'bundle install && bundle exec rake clean && bundle exec rake build && bundle exec rake integration'") 28 | end 29 | 30 | desc 'Wrap running test in docker' 31 | task :test do 32 | exit system("docker exec -ti #{latest_running_container_id} \ 33 | bash -c 'mv vendor vendor.bak; bundle install && \ 34 | bundle exec rake test; err=$?; 35 | rm -rf vendor; mv vendor.bak vendor; 36 | exit $err'") 37 | end 38 | 39 | desc 'Wrap running Rubocop in docker' 40 | task :rubocop do 41 | exit system("docker exec -ti #{latest_running_container_id} \ 42 | bash -c 'mv vendor ../vendor.bak; bundle install && \ 43 | bundle exec rake clean; 44 | bundle exec rake rubocop; err=$?; 45 | rm -rf vendor; mv ../vendor.bak vendor; 46 | exit $err'") 47 | end 48 | 49 | desc 'Check C files for linting issues' 50 | task :clangfmt do 51 | exit system("docker exec -ti #{latest_running_container_id} \ 52 | bash -c 'mv vendor vendor.bak; bundle install && \ 53 | bundle exec rake clangfmt; err=$?; 54 | rm -rf vendor; mv vendor.bak vendor; 55 | exit $err'") 56 | end 57 | 58 | desc 'Cleans up all development docker images for this project' 59 | task :clean do 60 | system('docker container ls --quiet --filter name=ruby-static-tracing* | xargs -I@ docker container kill @') 61 | end 62 | 63 | desc 'Pulls development image' 64 | task :pull do 65 | system('docker pull quay.io/dalehamel/ruby-static-tracing') 66 | end 67 | 68 | desc 'Push development image' 69 | task :push do 70 | system('docker push quay.io/dalehamel/ruby-static-tracing') 71 | end 72 | 73 | desc 'Fully set up a development docker image, and get a shell' 74 | task up: %i[build run shell] 75 | 76 | def latest_running_container_id 77 | container_id = `docker container ls --latest --quiet --filter status=running --filter name=ruby-static-tracing*`.strip 78 | if container_id.empty? 79 | raise 'No containers running, please run rake docker:run and then retry this task' 80 | else 81 | container_id 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/integration/integration_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'minitest/autorun' 4 | require 'pry-byebug' if ENV['PRY'] 5 | 6 | require 'tempfile' 7 | 8 | CACHED_DTRACE_PATH = File.expand_path("../../../.bin/dtrace", __FILE__).freeze 9 | PIDS = [] 10 | def cleanup_pids 11 | PIDS.each do |p| 12 | Process.kill('KILL', p) 13 | rescue Errno::EPERM 14 | end 15 | end 16 | 17 | MiniTest.after_run { cleanup_pids } 18 | 19 | module TraceRunner 20 | module_function 21 | 22 | def trace(*flags, script: nil, wait: nil) 23 | cmd = '' 24 | if StaticTracing::Platform.linux? 25 | cmd = 'bpftrace' 26 | cmd = [cmd, "#{script}.bt"] if script 27 | elsif StaticTracing::Platform.darwin? 28 | cmd = [CACHED_DTRACE_PATH, '-q'] 29 | cmd = [cmd, '-s', "#{script}.dt"] if script 30 | else 31 | puts 'WARNING: no supported tracer for this platform' 32 | return 33 | end 34 | 35 | cmd = [cmd, flags] 36 | 37 | command = cmd.flatten.join(' ') 38 | puts command if ENV['DEBUG'] 39 | CommandRunner.new(command, wait) 40 | end 41 | end 42 | 43 | # FIXME: add a "fixtures record" helper to facilitate adding tests / updating fixtures 44 | class CommandRunner 45 | TRACE_ENV_DEFAULT = { 46 | 'BPFTRACE_STRLEN' => '100' # workaround for https://github.com/iovisor/bpftrace/issues/305 47 | }.freeze 48 | 49 | attr_reader :pid, :path 50 | 51 | def initialize(command, wait = nil) 52 | outfile = Tempfile.new('ruby-static-tracing_tmp_out') 53 | @path = outfile.path 54 | outfile.unlink 55 | at_exit { File.unlink(@path) if File.exist?(@path) } 56 | 57 | @pid = Process.spawn(TRACE_ENV_DEFAULT, command, out: [@path, 'w'], err: '/dev/null') 58 | PIDS << @pid 59 | sleep wait if wait 60 | end 61 | 62 | def output 63 | output = File.read(@path) 64 | output 65 | end 66 | 67 | def interrupt(wait = nil) 68 | Process.kill('INT', @pid) 69 | sleep wait if wait 70 | end 71 | 72 | def kill(wait = nil) 73 | Process.kill('KILL', @pid) 74 | sleep wait if wait 75 | end 76 | 77 | def prof(wait = nil) 78 | Process.kill('SIGPROF', @pid) 79 | sleep wait if wait 80 | end 81 | 82 | def usr2(wait = nil) 83 | Process.kill('USR2', @pid) 84 | sleep wait if wait 85 | end 86 | end 87 | 88 | class IntegrationTestCase < MiniTest::Test 89 | def run 90 | file_directory = location.split('#').last 91 | test_dir = File.expand_path(file_directory, File.dirname(__FILE__)) 92 | Dir.chdir(test_dir) do 93 | super 94 | end 95 | end 96 | 97 | def command(command, wait: nil) 98 | CommandRunner.new(command, wait) 99 | end 100 | 101 | def read_probe_file(file) 102 | File.read(file) 103 | end 104 | 105 | def assert_tracer_output(outout, expected_ouput) 106 | msg = <<~EOF 107 | Output from tracer: 108 | #{mu_pp(outout)} 109 | 110 | Expected output: 111 | #{mu_pp(expected_ouput)} 112 | EOF 113 | assert(outout == expected_ouput, msg) 114 | end 115 | end 116 | 117 | def cache_dtrace 118 | puts <<-eof 119 | In order to run integration tests on OS X, we need to run 120 | dtrace with root permissions. To do this, we will ask you for 121 | sudo access to grant SETUID to a copy of the dtrace binary that 122 | we will cache in this project directory. 123 | 124 | Once this is done, any time you run integration tests dtrace will 125 | run as root, but the test suite won't. 126 | 127 | Please enter your sudo password to continue. 128 | eof 129 | FileUtils.mkdir_p(File.dirname(CACHED_DTRACE_PATH)) 130 | FileUtils.cp('/usr/sbin/dtrace', CACHED_DTRACE_PATH) 131 | system("sudo chown root #{CACHED_DTRACE_PATH} && sudo chmod a+s #{CACHED_DTRACE_PATH}") 132 | end 133 | 134 | if StaticTracing::Platform.darwin? 135 | cache_dtrace unless File.exists?(CACHED_DTRACE_PATH) 136 | end 137 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/darwin/provider.c: -------------------------------------------------------------------------------- 1 | #include "provider.h" 2 | 3 | static const rb_data_type_t static_tracing_provider_type; 4 | 5 | // Forward decls 6 | static const char *check_name_arg(VALUE name); 7 | 8 | /* 9 | Wraps usdt_create_provider from libusdt.h 10 | */ 11 | VALUE 12 | provider_initialize(VALUE self, VALUE name) { 13 | const char *c_name_str = NULL; 14 | static_tracing_provider_t *res = NULL; 15 | 16 | c_name_str = check_name_arg(name); 17 | 18 | TypedData_Get_Struct(self, static_tracing_provider_t, 19 | &static_tracing_provider_type, res); 20 | res->usdt_provider = usdt_create_provider( 21 | c_name_str, 22 | c_name_str); // FIXME make module from name and just prepend "_module_" 23 | return self; 24 | } 25 | 26 | // // Internal function used to register a tracepoint against a provider 27 | // instance 28 | int provider_add_tracepoint_internal(VALUE self, usdt_probedef_t *probedef) { 29 | static_tracing_provider_t *res = NULL; 30 | TypedData_Get_Struct(self, static_tracing_provider_t, 31 | &static_tracing_provider_type, res); 32 | return usdt_provider_add_probe(res->usdt_provider, probedef) == 0 ? Qtrue 33 | : Qfalse; 34 | } 35 | 36 | /* 37 | Wraps usdt_provider_enable from libusdt.h 38 | */ 39 | VALUE 40 | provider_enable(VALUE self) { 41 | static_tracing_provider_t *res = NULL; 42 | TypedData_Get_Struct(self, static_tracing_provider_t, 43 | &static_tracing_provider_type, res); 44 | return usdt_provider_enable(res->usdt_provider) == 0 ? Qtrue : Qfalse; 45 | } 46 | 47 | /* 48 | Wraps usdt_provider_disable from libusdt.h 49 | */ 50 | VALUE 51 | provider_disable(VALUE self) { 52 | static_tracing_provider_t *res = NULL; 53 | TypedData_Get_Struct(self, static_tracing_provider_t, 54 | &static_tracing_provider_type, res); 55 | return usdt_provider_disable(res->usdt_provider) == 0 ? Qtrue : Qfalse; 56 | } 57 | 58 | /* 59 | Wraps usdt_provider_free from libusdt.h 60 | */ 61 | VALUE 62 | provider_destroy(VALUE self) { 63 | static_tracing_provider_t *res = NULL; 64 | TypedData_Get_Struct(self, static_tracing_provider_t, 65 | &static_tracing_provider_type, res); 66 | usdt_provider_free(res->usdt_provider); 67 | return Qnil; 68 | } 69 | 70 | // Allocate a static_tracing_provider_type struct for ruby memory management 71 | VALUE 72 | static_tracing_provider_alloc(VALUE klass) { 73 | static_tracing_provider_t *res; 74 | VALUE obj = TypedData_Make_Struct(klass, static_tracing_provider_t, 75 | &static_tracing_provider_type, res); 76 | return obj; 77 | } 78 | 79 | static const char *check_name_arg(VALUE name) { 80 | const char *c_name_str = NULL; 81 | 82 | if (TYPE(name) != T_SYMBOL && TYPE(name) != T_STRING) { 83 | rb_raise(rb_eTypeError, "name must be a symbol or string"); 84 | } 85 | if (TYPE(name) == T_SYMBOL) { 86 | c_name_str = rb_id2name(rb_to_id(name)); 87 | } else if (TYPE(name) == T_STRING) { 88 | c_name_str = RSTRING_PTR(name); 89 | } 90 | 91 | return c_name_str; 92 | } 93 | 94 | static inline void static_tracing_provider_mark(void *ptr) { /* noop */ 95 | } 96 | 97 | static inline void static_tracing_provider_free(void *ptr) { 98 | static_tracing_provider_t *res = (static_tracing_provider_t *)ptr; 99 | // if (res->name) { 100 | // free(res->name); 101 | // res->name = NULL; 102 | //} 103 | xfree(res); 104 | } 105 | 106 | static inline size_t static_tracing_provider_memsize(const void *ptr) { 107 | return sizeof(static_tracing_provider_t); 108 | } 109 | 110 | static const rb_data_type_t static_tracing_provider_type = { 111 | "static_tracing_provider", 112 | {static_tracing_provider_mark, static_tracing_provider_free, 113 | static_tracing_provider_memsize}, 114 | NULL, 115 | NULL, 116 | RUBY_TYPED_FREE_IMMEDIATELY}; 117 | -------------------------------------------------------------------------------- /docker/scripts/fetch-linux-headers.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | 5 | LSB_FILE="/etc/lsb-release.host" 6 | OS_RELEASE_FILE="/etc/os-release.host" 7 | TARGET_DIR="/usr/src" 8 | HOST_MODULES_DIR="/lib/modules.host" 9 | KERNEL_VERSION=$(uname -r) 10 | 11 | generate_headers() 12 | { 13 | echo "Generating kernel headers" 14 | cd ${BUILD_DIR} 15 | zcat /proc/config.gz > .config 16 | make ARCH=x86 oldconfig > /dev/null 17 | make ARCH=x86 prepare > /dev/null 18 | 19 | # Clean up abundant non-header files to speed-up copying 20 | find ${BUILD_DIR} -regex '.*\.c\|.*\.txt\|.*Makefile\|.*Build\|.*Kconfig' -type f -delete 21 | } 22 | 23 | fetch_cos_linux_sources() 24 | { 25 | echo "Fetching upstream kernel sources." 26 | mkdir -p ${BUILD_DIR} 27 | curl -s "https://storage.googleapis.com/cos-tools/${BUILD_ID}/kernel-src.tar.gz" | tar -xzf - -C ${BUILD_DIR} 28 | } 29 | 30 | fetch_generic_linux_sources() 31 | { 32 | kernel_version=$(echo "${KERNEL_VERSION}" | tr -d '+' | cut -d - -f 1) 33 | major_version=$(echo "${KERNEL_VERSION}" | cut -d . -f 1) 34 | echo "Fetching upstream kernel sources for ${kernel_version}." 35 | mkdir -p ${BUILD_DIR} 36 | curl -sL https://www.kernel.org/pub/linux/kernel/v${major_version}.x/linux-$kernel_version.tar.gz | tar --strip-components=1 -xzf - -C ${BUILD_DIR} 37 | 38 | } 39 | 40 | install_cos_linux_headers() 41 | { 42 | if grep -q CHROMEOS_RELEASE_VERSION ${LSB_FILE} >/dev/null; then 43 | BUILD_ID=$(grep CHROMEOS_RELEASE_VERSION ${LSB_FILE} | cut -d = -f 2) 44 | BUILD_DIR="/linux-lakitu-${BUILD_ID}" 45 | SOURCES_DIR="${TARGET_DIR}/linux-lakitu-${BUILD_ID}" 46 | 47 | if [ ! -e "${SOURCES_DIR}/.installed" ];then 48 | echo "Installing kernel headers for COS build ${BUILD_ID}" 49 | time fetch_cos_linux_sources 50 | time generate_headers 51 | time mv ${BUILD_DIR} ${TARGET_DIR} 52 | touch "${SOURCES_DIR}/.installed" 53 | fi 54 | fi 55 | } 56 | 57 | install_generic_linux_headers() 58 | { 59 | BUILD_DIR="/linux-generic-${KERNEL_VERSION}" 60 | SOURCES_DIR="${TARGET_DIR}/linux-generic-${KERNEL_VERSION}" 61 | 62 | if [ ! -e "${SOURCES_DIR}/.installed" ];then 63 | echo "Installing kernel headers for generic kernel" 64 | time fetch_generic_linux_sources 65 | time generate_headers 66 | time mv ${BUILD_DIR} ${TARGET_DIR} 67 | touch "${SOURCES_DIR}/.installed" 68 | fi 69 | } 70 | 71 | install_headers() 72 | { 73 | distro=$(grep ^NAME ${OS_RELEASE_FILE} >/dev/null | cut -d = -f 2) 74 | 75 | case $distro in 76 | *"Container-Optimized OS"*) 77 | install_cos_linux_headers 78 | HEADERS_TARGET=${SOURCES_DIR} 79 | ;; 80 | *) 81 | echo "WARNING: Cannot find distro-specific headers for ${distro}. Fetching generic headers." 82 | install_generic_linux_headers 83 | HEADERS_TARGET=${SOURCES_DIR} 84 | ;; 85 | esac 86 | } 87 | 88 | check_headers() 89 | { 90 | modules_path=$1 91 | arch=$(uname -m) 92 | kdir="${modules_path}/${KERNEL_VERSION}" 93 | 94 | [ "${arch}" == "x86_64" ] && arch="x86" 95 | 96 | [ ! -e ${kdir} ] && return 1 97 | [ ! -e "${kdir}/source" ] && [ ! -e "${kdir}/build" ] && return 1 98 | 99 | header_dir=$([ -e "${kdir}/source" ] && echo "${kdir}/source" || echo "${kdir}/build") 100 | 101 | [ ! -e "${header_dir}/include/linux/kconfig.h" ] && return 1 102 | [ ! -e "${header_dir}/include/generated/uapi" ] && return 1 103 | [ ! -e "${header_dir}/arch/${arch}/include/generated/uapi" ] && return 1 104 | 105 | return 0 106 | } 107 | 108 | if [ ! -e /lib/modules/.installed ];then 109 | if ! check_headers ${HOST_MODULES_DIR}; then 110 | install_headers 111 | else 112 | HEADERS_TARGET=${HOST_MODULES_DIR}/source 113 | fi 114 | 115 | mkdir -p "/lib/modules/${KERNEL_VERSION}" 116 | ln -sf ${HEADERS_TARGET} "/lib/modules/${KERNEL_VERSION}/source" 117 | ln -sf ${HEADERS_TARGET} "/lib/modules/${KERNEL_VERSION}/build" 118 | touch /lib/modules/.installed 119 | exit 0 120 | else 121 | echo "Headers already installed" 122 | exit 0 123 | fi 124 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rake/testtask' 4 | require 'rubocop/rake_task' 5 | require 'bundler/gem_tasks' 6 | 7 | require 'tasks/docker' 8 | require 'tasks/vagrant' 9 | 10 | GEMSPEC = eval(File.read('ruby-static-tracing.gemspec')) 11 | BASE_DIR = __dir__ 12 | DOCKER_DIR = File.join(BASE_DIR, 'docker') 13 | EXT_DIR = File.join(BASE_DIR, 'ext/ruby-static-tracing') 14 | LIB_DIR = File.join(BASE_DIR, 'lib', 'ruby-static-tracing') 15 | # ========================================================== 16 | # Packaging 17 | # ========================================================== 18 | 19 | require 'rubygems/package_task' 20 | Gem::PackageTask.new(GEMSPEC) do |_pkg| 21 | end 22 | 23 | # ========================================================== 24 | # Ruby Extension 25 | # ========================================================== 26 | 27 | $LOAD_PATH.unshift File.expand_path('lib', __dir__) 28 | require 'ruby-static-tracing/platform' 29 | require 'ruby-static-tracing/version' 30 | if StaticTracing::Platform.supported_platform? 31 | require 'rake/extensiontask' 32 | 33 | # Task to compile external dep, but let them use their own makefiles 34 | Rake::ExtensionTask.new do |ext| 35 | ext.name = 'deps' 36 | ext.ext_dir = 'ext/ruby-static-tracing/lib' 37 | ext.lib_dir = 'lib/ruby-static-tracing' 38 | ext.config_script = 'deps-extconf.rb' 39 | end 40 | 41 | Rake::ExtensionTask.new('ruby_static_tracing', GEMSPEC) do |ext| 42 | ext.ext_dir = 'ext/ruby-static-tracing' 43 | ext.lib_dir = 'lib/ruby-static-tracing' 44 | end 45 | 46 | # Task for "post install" of libraries 47 | Rake::ExtensionTask.new do |ext| 48 | ext.name = 'post' 49 | ext.ext_dir = 'ext/ruby-static-tracing/lib' 50 | ext.lib_dir = 'lib/ruby-static-tracing' 51 | ext.config_script = 'post-extconf.rb' 52 | end 53 | 54 | task fresh: ['deps:clean', :clean] 55 | task compile: [:fresh, 'compile:deps', 'compile:ruby_static_tracing', 'compile:post'] 56 | task build: %i[fresh compile] 57 | else 58 | task :build do 59 | end 60 | end 61 | 62 | # ========================================================== 63 | # Development 64 | # ========================================================== 65 | 66 | namespace :deps do 67 | task :get do 68 | system('git submodule init') 69 | system('git submodule update') 70 | end 71 | 72 | task :clean do 73 | system("cd #{File.join(EXT_DIR, 'lib', 'libusdt')} && make clean") 74 | system("cd #{File.join(EXT_DIR, 'lib', 'libstapsdt')} && make clean") 75 | end 76 | end 77 | 78 | namespace :new do 79 | desc 'Scaffold a new integration test' 80 | task :integration_test, [:test] do |_t, args| 81 | test_name = args[:test] 82 | integration_test_directory = 'test/integration' 83 | 84 | Dir.chdir(integration_test_directory) do 85 | test_folder = "test_#{test_name}" 86 | FileUtils.mkdir("test_#{test_name}") 87 | 88 | Dir.chdir(test_folder) do 89 | File.open("#{test_folder}.rb", 'w') do |file| 90 | file.write(test_scaffold(test_name)) 91 | end 92 | FileUtils.touch("#{test_name}.bt") 93 | FileUtils.touch("#{test_name}.out") 94 | File.open("#{test_name}.rb", 'w') do |file| 95 | file.write(basic_script) 96 | end 97 | end 98 | end 99 | end 100 | 101 | def test_scaffold(test_name) 102 | <<~TEST 103 | require 'integration_helper' 104 | class #{test_name.capitalize}Test < IntegrationTestCase 105 | def test_#{test_name} 106 | end 107 | end 108 | TEST 109 | end 110 | 111 | def basic_script 112 | <<~SCRIPT 113 | require 'ruby-static-tracing' 114 | STDOUT.sync = true 115 | 116 | SCRIPT 117 | end 118 | end 119 | 120 | Rake::TestTask.new do |t| 121 | t.libs << 'test' 122 | t.test_files = FileList['test/**/*_test.rb'].exclude(/integration/) 123 | t.verbose = true 124 | end 125 | 126 | Rake::TestTask.new do |t| 127 | t.name = 'integration' 128 | t.libs << 'test/integration' 129 | t.test_files = FileList['test/integration/**/*_test.rb'] 130 | t.verbose = true 131 | end 132 | 133 | RuboCop::RakeTask.new 134 | 135 | task :clangfmt do 136 | diffs = [] 137 | %w[linux darwin include].each do |dir| 138 | Dir["#{File.join(EXT_DIR, dir)}/*.{h,c}"].each do |src| 139 | tmp = "/tmp/#{src.tr('/', '_')}" 140 | system("clang-format #{src} > #{tmp}") 141 | # system("clang-format -i #{src}") 142 | diff = `diff #{src} #{tmp}` 143 | system("rm -f #{tmp}") 144 | unless diff.lines.empty? 145 | puts "Diff on #{tmp} #{diff}" 146 | diffs << diff.lines 147 | end 148 | end 149 | end 150 | diffcount = diffs.flatten.length 151 | if diffcount > 0 152 | puts "clang-format check failed, #{diffcount} differences" 153 | exit 1 154 | else 155 | exit 0 156 | end 157 | end 158 | 159 | # ========================================================== 160 | # Documentation 161 | # ========================================================== 162 | require 'rdoc/task' 163 | RDoc::Task.new do |rdoc| 164 | # FIXME: add darwin docs 165 | rdoc.rdoc_files.include('lib/**/*.rb', 166 | 'ext/ruby-static-tracing/linux/*.c', 167 | 'ext/ruby-static-tracing/linux/*.h') 168 | end 169 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/linux/provider.c: -------------------------------------------------------------------------------- 1 | #include "provider.h" 2 | 3 | #include 4 | 5 | static const rb_data_type_t static_tracing_provider_type; 6 | 7 | // Forward decls 8 | static const char *check_name_arg(VALUE name); 9 | 10 | /* 11 | Wraps ProviderInit from libstapsdt 12 | */ 13 | VALUE 14 | provider_initialize(VALUE self, VALUE name) { 15 | const char *c_name_str = NULL; 16 | static_tracing_provider_t *res = NULL; 17 | 18 | // Check and cast arguments 19 | c_name_str = check_name_arg(name); 20 | 21 | // Build provider structure 22 | TypedData_Get_Struct(self, static_tracing_provider_t, 23 | &static_tracing_provider_type, res); 24 | res->sdt_provider = providerInit(c_name_str); 25 | return self; 26 | } 27 | 28 | // Internal function used to register a tracepoint against a provider instance 29 | SDTProbe_t *provider_add_tracepoint_internal(VALUE self, const char *name, 30 | int argc, 31 | Tracepoint_arg_types *args) { 32 | static_tracing_provider_t *res = NULL; 33 | SDTProbe_t *probe; 34 | 35 | TypedData_Get_Struct(self, static_tracing_provider_t, 36 | &static_tracing_provider_type, res); 37 | 38 | switch (argc) { 39 | case 0: 40 | probe = providerAddProbe(res->sdt_provider, name, 0); 41 | break; 42 | case 1: 43 | probe = providerAddProbe(res->sdt_provider, name, argc, args[0]); 44 | break; 45 | case 2: 46 | probe = providerAddProbe(res->sdt_provider, name, argc, args[0], args[1]); 47 | break; 48 | case 3: 49 | probe = providerAddProbe(res->sdt_provider, name, argc, args[0], args[1], 50 | args[2]); 51 | break; 52 | case 4: 53 | probe = providerAddProbe(res->sdt_provider, name, argc, args[0], args[1], 54 | args[2], args[3]); 55 | break; 56 | case 5: 57 | probe = providerAddProbe(res->sdt_provider, name, argc, args[0], args[1], 58 | args[2], args[3], args[4]); 59 | break; 60 | case 6: 61 | probe = providerAddProbe(res->sdt_provider, name, argc, args[0], args[1], 62 | args[2], args[3], args[4], args[5]); 63 | break; 64 | default: 65 | probe = providerAddProbe(res->sdt_provider, name, 0); 66 | break; 67 | } 68 | 69 | return probe; 70 | } 71 | 72 | /* 73 | Wraps providerLoad from libstapsdt 74 | */ 75 | VALUE 76 | provider_enable(VALUE self) { 77 | static_tracing_provider_t *res = NULL; 78 | TypedData_Get_Struct(self, static_tracing_provider_t, 79 | &static_tracing_provider_type, res); 80 | return providerLoad(res->sdt_provider) == 0 ? Qtrue : Qfalse; 81 | } 82 | 83 | /* 84 | Wraps providerUnload from libstapsdt 85 | */ 86 | VALUE 87 | provider_disable(VALUE self) { 88 | static_tracing_provider_t *res = NULL; 89 | TypedData_Get_Struct(self, static_tracing_provider_t, 90 | &static_tracing_provider_type, res); 91 | res->sdt_provider->_filename = NULL; // FIXME upstream should do this 92 | return providerUnload(res->sdt_provider) == 0 ? Qtrue : Qfalse; 93 | } 94 | 95 | /* 96 | Wraps providerDestroy from libstapsdt 97 | */ 98 | VALUE 99 | provider_destroy(VALUE self) { 100 | static_tracing_provider_t *res = NULL; 101 | TypedData_Get_Struct(self, static_tracing_provider_t, 102 | &static_tracing_provider_type, res); 103 | providerDestroy(res->sdt_provider); 104 | return Qnil; 105 | } 106 | 107 | VALUE 108 | provider_path(VALUE self) { 109 | VALUE path; 110 | char *_path; 111 | static_tracing_provider_t *res = NULL; 112 | TypedData_Get_Struct(self, static_tracing_provider_t, 113 | &static_tracing_provider_type, res); 114 | 115 | if (res != NULL && res->sdt_provider != NULL && 116 | res->sdt_provider->_filename != NULL) { 117 | _path = res->sdt_provider->_filename; 118 | path = strlen(_path) > 0 ? rb_str_new_cstr(_path) : rb_str_new_cstr(""); 119 | } else { 120 | path = rb_str_new_cstr(""); 121 | } 122 | return path; 123 | } 124 | 125 | // Allocate a static_tracing_provider_type struct for ruby memory management 126 | VALUE 127 | static_tracing_provider_alloc(VALUE klass) { 128 | static_tracing_provider_t *res; 129 | VALUE obj = TypedData_Make_Struct(klass, static_tracing_provider_t, 130 | &static_tracing_provider_type, res); 131 | return obj; 132 | } 133 | 134 | static const char *check_name_arg(VALUE name) { 135 | const char *c_name_str = NULL; 136 | 137 | if (TYPE(name) != T_SYMBOL && TYPE(name) != T_STRING) { 138 | rb_raise(rb_eTypeError, "name must be a symbol or string"); 139 | } 140 | if (TYPE(name) == T_SYMBOL) { 141 | c_name_str = rb_id2name(rb_to_id(name)); 142 | } else if (TYPE(name) == T_STRING) { 143 | c_name_str = RSTRING_PTR(name); 144 | } 145 | 146 | return c_name_str; 147 | } 148 | 149 | static inline void static_tracing_provider_mark(void *ptr) { /* noop */ 150 | } 151 | 152 | static inline void static_tracing_provider_free(void *ptr) { 153 | static_tracing_provider_t *res = (static_tracing_provider_t *)ptr; 154 | // if (res->name) { 155 | // free(res->name); 156 | // res->name = NULL; 157 | //} 158 | xfree(res); 159 | } 160 | 161 | static inline size_t static_tracing_provider_memsize(const void *ptr) { 162 | return sizeof(static_tracing_provider_t); 163 | } 164 | 165 | static const rb_data_type_t static_tracing_provider_type = { 166 | "static_tracing_provider", 167 | {static_tracing_provider_mark, static_tracing_provider_free, 168 | static_tracing_provider_memsize}, 169 | NULL, 170 | NULL, 171 | RUBY_TYPED_FREE_IMMEDIATELY}; 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/dalehamel/ruby-static-tracing.svg?branch=master)](https://travis-ci.org/dalehamel/ruby-static-tracing) 2 | 3 | [![Docker Repository on Quay](https://quay.io/repository/dalehamel/ruby-static-tracing/status "Docker Repository on Quay")](https://quay.io/repository/dalehamel/ruby-static-tracing) 4 | 5 | ![ponycorn](http://www.brendangregg.com/blog/images/2015/pony_ebpf01_small.png) 6 | 7 | # USDT Report 8 | 9 | This gem is described in greater detail in this [USDT report](https://blog.srvthe.net/usdt-report-doc/), and has rdoc for [master](https://blog.srvthe.net/ruby-static-tracing/index.html) and the [latest release ](https://www.rubydoc.info/gems/ruby-static-tracing/0.0.13), as well as [code coverage from last master build ](https://blog.srvthe.net/ruby-static-tracing/coverage/index.html) 10 | 11 | # Tracing ruby in Development and Production 12 | 13 | Add tracepoints for any question you need answered about your code. 14 | 15 | Until you enable them, these tracepoints will have 0 overhead, and after you enable them, these tracepoints will have almost no overhead. 16 | 17 | No need to continuously log print statements, as output is only generated when tracepoints are actually being traced. 18 | 19 | Write and test tracepoints on any Mac or Linux workstation, and have them be accessible to you in production in the exact same way. 20 | 21 | This should be useful for: 22 | 23 | * Generating latency histograms for methods, code blocks, or entire libraries. 24 | * Comparing performance characteristics in development versus production. 25 | * Tracing the wall-lock time of calls to external services. 26 | * Diagnostic prints (the world's fanciest `printf`). 27 | * Collecting stack traces with surgical precision. 28 | * Exposing other ruby VM characteristics, such as for runtime heap analysis. 29 | 30 | Both Darwin/OSX and Linux are supported. 31 | 32 | There is a [Development guide](./DEVELOPMENT.md) to show how to set up a dev env and test the gem and try out the linux examples. 33 | 34 | # Goals 35 | 36 | ## Easy 37 | 38 | Make registering static tracepoints in ruby as simple as possible with the lowest overhead possible. 39 | 40 | Ideally, you shouldn't have to know anything about static tracing to add probes to your library. 41 | 42 | We want to cater to frameworks like rails, to provide tracing patterns that will work well for both simple and advanced use cases. 43 | 44 | ## Fast 45 | 46 | Registering lots of tracepoints should be encouraged. To support this, the overhead of unused tracepoints should be: 47 | 48 | 1. Minimized or eliminated if tracing is not enabled 49 | 1. Measured with runtime complexity understood 50 | 1. Simple, avoiding loops O(n) and using as many constant-time O(1) operations, such as (hopefully) table lookups 51 | 52 | The approaches that eBPF itself takes to achieve this are: 53 | 54 | - Using a verifier before running, to perform static analysis on code. For this reason, loops are only allowed if fully unrolled. 55 | - To cause no overhead on a tracepoint if it is not being traced 56 | 57 | Dtrace is able to overwrite the memory of the target process when a probe is enabled, and only then is assembly related to the tracing added to processing overhead. 58 | 59 | The approach that eBPF on linux takes is similar, using uprobes to inform a process that it has a userspace probe enabled. 60 | 61 | // TO DO add state diagrams for enabled/disabled flows of dtrace vs bpftrace, and how this maps to ruby 62 | 63 | ## Powerful 64 | 65 | Ideally, the values being probed should already be present in the execution context. Any code within the context of a probe will need to be executed each time it is fired. 66 | 67 | If additional context needs to be gathered, this is possible through the use of ruby code. For instance, a code block can be passed to the tracepoint which is used as the wrapper code, which then yields the values to be fired. This allows for powerful dynamic tracing capabilities, being able to pull any relevant information in the execution context to deliver to the probe. 68 | 69 | ## Safe 70 | 71 | Where possible, table lookups or variables in local context should be preferred in order to gather the data to fire off in the probe. Any helper functions in ruby space should carefully describe their worst-case runtime complexity, and bound this at O(n), where n is a known small integer. 72 | 73 | # Latency tracer 74 | 75 | Here's a demonstration of the latency tracer working end-to-end: 76 | 77 | ![latencytracer.gif](./docs/latency_tracer.gif) 78 | 79 | # Alternatives 80 | 81 | ## Print statements and metrics 82 | 83 | Ultimately, tracing is just a fancy `printf` in a lot of ways. If plain ol' `puts` and log statements get the job done with an acceptable performance overhead, use'm! 84 | 85 | The same is true for metrics, if you have something like statsd that might be a better way to get the data you're looking for. 86 | 87 | This type of tracing access at performance analysis, as you can easily generate latency histograms and perform other aggregation functions on scalar data. 88 | 89 | ## Ruby tracing 90 | 91 | Ruby has its own tracing support, but it theoretically has a higher overhead as requires running logic on every function call. 92 | 93 | USDT tracing should solve this, by explicitly registering trace points and only adding complexity and extra instructions to 94 | execute if the tracepoint is actually enabled. 95 | 96 | ## Ruby dtrace hooks 97 | 98 | Ruby provides its [own built-in dtrace probes](https://github.com/ruby/ruby/blob/4444025d16ae1a586eee6a0ac9bdd09e33833f3c/probes.d), which can be used similarly to ruby's built-in tracing, to simply probe all executed functions. This has the same issue of running on every function, instead of just the ones you want, but it's really great for simple invocations: 99 | 100 | ``` 101 | bpftrace -e 'usdt::ruby:method__entry { @[str(arg1)]++ }' -p ${UNICORN_PID} 102 | ``` 103 | 104 | ## ruby-usdt 105 | 106 | https://github.com/thekvn/ruby-usdt 107 | 108 | This repo provides much of the inspiration for this project, but it appears lost to the sands of time. 109 | 110 | We wrap a newer version of libusdt, and support both Darwin and Linux. 111 | 112 | ## libusdt 113 | 114 | https://github.com/chrisa/libusdt 115 | 116 | This is used by this project to provide Darwin support, but libusdt doesn't support linux. 117 | 118 | Works by writing a `dof` note dynamically into the processes heap. 119 | 120 | ## libstapsdt 121 | 122 | https://github.com/sthima/libstapsdt 123 | 124 | This is used by this project to provide Linux support, but libusdt doesn't support Darwin. 125 | 126 | Works by writing a `ELF` note to a stub library, and `dlopen`'ing it. 127 | 128 | ## lttng-ust 129 | 130 | There is an existing (apparently unmaintained) [gem](https://github.com/riddochc/lttng-agent-ruby) for ruby that utilizes lttng's userspace support. 131 | 132 | It is possible that this could provide improved performance by way of lover overhead as compared to USDT probes. We may support lttng-ust probes in the future, 133 | as they appear to conform to the same interface. They could be used instead of USDT probes for linux. 134 | 135 | lttng-ust does tracing 100% in userspace, as opposed to uprobes which are executed within a kernel context. It may be worthwile to benchmark agints lttng-ust libraries, to ascertain what the magnitude of the difference is overhead is, particularly if USDT probe overhead becomes observable and detrimental. 136 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/linux/tracepoint.c: -------------------------------------------------------------------------------- 1 | #include "tracepoint.h" 2 | 3 | static const rb_data_type_t static_tracing_tracepoint_type; 4 | 5 | static const char *check_name_arg(VALUE provider); 6 | 7 | static const char *check_provider_arg(VALUE provider); 8 | 9 | static Tracepoint_arg_types *check_vargs(int *argc, VALUE vargs); 10 | 11 | static Tracepoint_fire_arg *check_fire_args(int *argc, VALUE vargs); 12 | 13 | VALUE 14 | tracepoint_initialize(VALUE self, VALUE provider, VALUE name, VALUE vargs) { 15 | VALUE cStaticTracing, cProvider, cProviderInst; 16 | static_tracing_tracepoint_t *tracepoint = NULL; 17 | const char *c_name_str = NULL; 18 | int argc = 0; 19 | 20 | c_name_str = check_name_arg(name); 21 | check_provider_arg(provider); // FIXME should only accept string 22 | Tracepoint_arg_types *args = check_vargs(&argc, vargs); 23 | 24 | /// Get a handle to global provider list for lookup 25 | cStaticTracing = rb_const_get(rb_cObject, rb_intern("StaticTracing")); 26 | cProvider = rb_const_get(cStaticTracing, rb_intern("Provider")); 27 | cProviderInst = rb_funcall(cProvider, rb_intern("register"), 1, provider); 28 | 29 | // Use the provider to register a tracepoint 30 | SDTProbe_t *probe = 31 | provider_add_tracepoint_internal(cProviderInst, c_name_str, argc, args); 32 | TypedData_Get_Struct(self, static_tracing_tracepoint_t, 33 | &static_tracing_tracepoint_type, tracepoint); 34 | 35 | // Stare the tracepoint handle in our struct 36 | tracepoint->sdt_tracepoint = probe; 37 | tracepoint->args = args; 38 | 39 | return self; 40 | } 41 | 42 | VALUE 43 | tracepoint_fire(VALUE self, VALUE vargs) { 44 | static_tracing_tracepoint_t *res = NULL; 45 | TypedData_Get_Struct(self, static_tracing_tracepoint_t, 46 | &static_tracing_tracepoint_type, res); 47 | int argc = 0; 48 | 49 | Tracepoint_fire_arg *args = check_fire_args(&argc, vargs); 50 | switch (argc) { 51 | case 0: 52 | probeFire(res->sdt_tracepoint); 53 | break; 54 | case 1: 55 | probeFire(res->sdt_tracepoint, args[0]); 56 | break; 57 | case 2: 58 | probeFire(res->sdt_tracepoint, args[0], args[1]); 59 | break; 60 | case 3: 61 | probeFire(res->sdt_tracepoint, args[0], args[1], args[2]); 62 | break; 63 | case 4: 64 | probeFire(res->sdt_tracepoint, args[0], args[1], args[2], args[3]); 65 | break; 66 | case 5: 67 | probeFire(res->sdt_tracepoint, args[0], args[1], args[2], args[3], args[4]); 68 | break; 69 | case 6: 70 | probeFire(res->sdt_tracepoint, args[0], args[1], args[2], args[3], args[4], 71 | args[5]); 72 | break; 73 | default: 74 | probeFire(res->sdt_tracepoint); 75 | break; 76 | } 77 | 78 | return Qnil; 79 | } 80 | 81 | VALUE 82 | tracepoint_enabled(VALUE self) { 83 | static_tracing_tracepoint_t *res = NULL; 84 | TypedData_Get_Struct(self, static_tracing_tracepoint_t, 85 | &static_tracing_tracepoint_type, res); 86 | return probeIsEnabled(res->sdt_tracepoint) == 1 ? Qtrue : Qfalse; 87 | } 88 | 89 | static const char *check_name_arg(VALUE name) { 90 | const char *c_name_str = NULL; 91 | 92 | if (TYPE(name) != T_SYMBOL && TYPE(name) != T_STRING) { 93 | rb_raise(rb_eTypeError, "name must be a symbol or string"); 94 | } 95 | if (TYPE(name) == T_SYMBOL) { 96 | c_name_str = rb_id2name(rb_to_id(name)); 97 | } else if (TYPE(name) == T_STRING) { 98 | c_name_str = RSTRING_PTR(name); 99 | } 100 | 101 | return c_name_str; 102 | } 103 | 104 | static const char *check_provider_arg(VALUE provider) { 105 | const char *c_provider_str = NULL; 106 | 107 | if (TYPE(provider) != T_SYMBOL && TYPE(provider) != T_STRING) { 108 | rb_raise(rb_eTypeError, "provider must be a symbol or string"); 109 | } 110 | if (TYPE(provider) == T_SYMBOL) { 111 | c_provider_str = rb_id2name(rb_to_id(provider)); 112 | } else if (TYPE(provider) == T_STRING) { 113 | c_provider_str = RSTRING_PTR(provider); 114 | } 115 | 116 | return c_provider_str; 117 | } 118 | 119 | static Tracepoint_arg_types *check_vargs(int *argc, VALUE vargs) { 120 | if (TYPE(vargs) == T_ARRAY) { 121 | VALUE rLength = rb_funcall(vargs, rb_intern("length"), 0, Qnil); 122 | *argc = NUM2INT(rLength); 123 | 124 | if (*argc > 6) { 125 | printf("ERROR - passed %i args, maximum 6 argument types can be passed", 126 | *argc); 127 | return NULL; 128 | } 129 | int i; 130 | Tracepoint_arg_types *args = malloc(*argc * sizeof(Tracepoint_arg_types)); 131 | for (i = 0; i < *argc; i++) { 132 | VALUE str = 133 | rb_funcall(rb_ary_entry(vargs, i), rb_intern("to_s"), 0, Qnil); 134 | const char *cStr = RSTRING_PTR(str); 135 | if (strcmp(cStr, "Integer")) { 136 | args[i] = Integer; 137 | } else if (strcmp(cStr, "String")) { 138 | args[i] = String; 139 | } else { 140 | printf("ERROR - type \"%s\" is unsupported\n", cStr); 141 | } 142 | } 143 | return args; 144 | } else { 145 | printf("ERROR - array was expected\n"); 146 | return NULL; 147 | } 148 | } 149 | 150 | static Tracepoint_fire_arg *check_fire_args(int *argc, VALUE vargs) { 151 | if (TYPE(vargs) == T_ARRAY) { 152 | VALUE rLength = rb_funcall(vargs, rb_intern("length"), 0, Qnil); 153 | *argc = NUM2INT(rLength); 154 | 155 | if (*argc > 6) { 156 | printf("ERROR - passed %i args, maximum 6 argument types can be passed", 157 | *argc); 158 | return NULL; 159 | } 160 | 161 | Tracepoint_fire_arg *args = malloc(*argc * sizeof(Tracepoint_fire_arg)); 162 | // printf("SIZE: %i ARGC: %i \n", sizeof(Tracepoint_fire_arg), *argc); 163 | int i; 164 | for (i = 0; i < *argc; i++) { 165 | VALUE val = rb_ary_entry(vargs, i); 166 | switch (TYPE(val)) { 167 | case T_FIXNUM: 168 | args[i].intval = FIX2LONG(val); 169 | break; 170 | case T_STRING: 171 | args[i].strval = RSTRING_PTR(val); 172 | break; 173 | default: 174 | printf("ERROR unsupported type passed for argument %i to fire\n", i); 175 | break; 176 | } 177 | } 178 | return args; 179 | } else { 180 | printf("ERROR - array was expected\n"); 181 | return NULL; 182 | } 183 | } 184 | 185 | // Allocate a static_tracing_tracepoint_type struct for ruby memory management 186 | VALUE 187 | static_tracing_tracepoint_alloc(VALUE klass) { 188 | static_tracing_tracepoint_t *res; 189 | VALUE obj = TypedData_Make_Struct(klass, static_tracing_tracepoint_t, 190 | &static_tracing_tracepoint_type, res); 191 | return obj; 192 | } 193 | 194 | static inline void static_tracing_tracepoint_mark(void *ptr) { /* noop */ 195 | } 196 | 197 | static inline void static_tracing_tracepoint_free(void *ptr) { 198 | static_tracing_tracepoint_t *res = (static_tracing_tracepoint_t *)ptr; 199 | // if (res->name) { 200 | // free(res->name); 201 | // res->name = NULL; 202 | //} 203 | xfree(res); 204 | } 205 | 206 | static inline size_t static_tracing_tracepoint_memsize(const void *ptr) { 207 | return sizeof(static_tracing_provider_t); 208 | } 209 | 210 | static const rb_data_type_t static_tracing_tracepoint_type = { 211 | "static_tracing_tracepoint", 212 | {static_tracing_tracepoint_mark, static_tracing_tracepoint_free, 213 | static_tracing_tracepoint_memsize}, 214 | NULL, 215 | NULL, 216 | RUBY_TYPED_FREE_IMMEDIATELY}; 217 | -------------------------------------------------------------------------------- /ext/ruby-static-tracing/darwin/tracepoint.c: -------------------------------------------------------------------------------- 1 | #include "tracepoint.h" 2 | 3 | static const rb_data_type_t static_tracing_tracepoint_type; 4 | 5 | static const char *check_name_arg(VALUE provider); 6 | 7 | static const char *check_provider_arg(VALUE provider); 8 | 9 | static char **check_vargs(int *argc, VALUE vargs); 10 | 11 | static void **check_fire_args(int *argc, VALUE vargs); 12 | 13 | VALUE 14 | tracepoint_initialize(VALUE self, VALUE provider, VALUE name, VALUE vargs) { 15 | VALUE cStaticTracing, cProvider, cProviderInst; 16 | static_tracing_tracepoint_t *tracepoint = NULL; 17 | const char *c_name_str = NULL; 18 | int argc = 0; 19 | 20 | c_name_str = check_name_arg(name); 21 | check_provider_arg(provider); // FIXME should only accept string 22 | char **args = check_vargs(&argc, vargs); 23 | 24 | /// Get a handle to global provider list for lookup 25 | cStaticTracing = rb_const_get(rb_cObject, rb_intern("StaticTracing")); 26 | cProvider = rb_const_get(cStaticTracing, rb_intern("Provider")); 27 | cProviderInst = rb_funcall(cProvider, rb_intern("register"), 1, provider); 28 | 29 | // Create a probe 30 | usdt_probedef_t *probe = 31 | usdt_create_probe(c_name_str, c_name_str, argc, args); 32 | 33 | // Use the provider to register a tracepoint 34 | int success = provider_add_tracepoint_internal( 35 | cProviderInst, probe); // FIXME handle error checking here and throw an 36 | // exception if failure 37 | TypedData_Get_Struct(self, static_tracing_tracepoint_t, 38 | &static_tracing_tracepoint_type, tracepoint); 39 | 40 | // FIXME check for nulls 41 | // FIXME Do we really need to store both references? refactor this. 42 | // Store the tracepoint handle in our struct 43 | tracepoint->usdt_tracepoint_def = probe; 44 | 45 | return self; 46 | } 47 | 48 | VALUE 49 | tracepoint_fire(VALUE self, VALUE vargs) { 50 | static_tracing_tracepoint_t *res = NULL; 51 | TypedData_Get_Struct(self, static_tracing_tracepoint_t, 52 | &static_tracing_tracepoint_type, res); 53 | int argc = 0; 54 | void *args = check_fire_args(&argc, vargs); 55 | 56 | usdt_fire_probe(res->usdt_tracepoint_def->probe, argc, args); 57 | return Qnil; 58 | } 59 | 60 | VALUE 61 | tracepoint_enabled(VALUE self) { 62 | static_tracing_tracepoint_t *res = NULL; 63 | TypedData_Get_Struct(self, static_tracing_tracepoint_t, 64 | &static_tracing_tracepoint_type, res); 65 | return usdt_is_enabled(res->usdt_tracepoint_def->probe) == 0 ? Qfalse : Qtrue; 66 | } 67 | 68 | static const char *check_name_arg(VALUE name) { 69 | const char *c_name_str = NULL; 70 | 71 | if (TYPE(name) != T_SYMBOL && TYPE(name) != T_STRING) { 72 | rb_raise(rb_eTypeError, "name must be a symbol or string"); 73 | } 74 | if (TYPE(name) == T_SYMBOL) { 75 | c_name_str = rb_id2name(rb_to_id(name)); 76 | } else if (TYPE(name) == T_STRING) { 77 | c_name_str = RSTRING_PTR(name); 78 | } 79 | 80 | return c_name_str; 81 | } 82 | 83 | static const char *check_provider_arg(VALUE provider) { 84 | const char *c_provider_str = NULL; 85 | 86 | if (TYPE(provider) != T_SYMBOL && TYPE(provider) != T_STRING) { 87 | rb_raise(rb_eTypeError, "provider must be a symbol or string"); 88 | } 89 | if (TYPE(provider) == T_SYMBOL) { 90 | c_provider_str = rb_id2name(rb_to_id(provider)); 91 | } else if (TYPE(provider) == T_STRING) { 92 | c_provider_str = RSTRING_PTR(provider); 93 | } 94 | 95 | return c_provider_str; 96 | } 97 | 98 | static char **check_vargs(int *argc, VALUE vargs) { 99 | if (TYPE(vargs) == T_ARRAY) { 100 | VALUE rLength = rb_funcall(vargs, rb_intern("length"), 0, Qnil); 101 | *argc = NUM2INT(rLength); 102 | 103 | if (*argc > 6) { 104 | printf("ERROR - passed %i args, maximum 6 argument types can be passed", 105 | *argc); 106 | return NULL; 107 | } 108 | // FIXME ensure this is freed 109 | char **args = malloc(*argc * sizeof(char *)); 110 | for (int i = 0; i < *argc; i++) { 111 | VALUE str = 112 | rb_funcall(rb_ary_entry(vargs, i), rb_intern("to_s"), 0, Qnil); 113 | const char *cStr = RSTRING_PTR(str); 114 | if (strcmp(cStr, "Integer")) { 115 | args[i] = strdup(cStr); 116 | } else if (strcmp(cStr, "String")) { 117 | args[i] = strdup(cStr); 118 | } else { 119 | printf("ERROR - type \"%s\" is unsupported\n", cStr); 120 | } 121 | } 122 | return args; 123 | } else { 124 | printf("ERROR - array was expected\n"); 125 | return NULL; 126 | } 127 | } 128 | 129 | /* 130 | * Yeah, returning a void ** array is a bit sketchy, but since 131 | * usdt_fire_probe takes a void **, that's what we'll give it. 132 | * 133 | * The structure of the data is actually an array of 64 bit unions 134 | * So the size of the data should be argc * 8 bytes. 135 | * We assume that reads will be aligned on 64 bits in the tracer. 136 | * 137 | * The approach libstapsdt takes is pretty similar, just not an array. 138 | */ 139 | static void **check_fire_args(int *argc, VALUE vargs) { 140 | if (TYPE(vargs) == T_ARRAY) { 141 | VALUE rLength = rb_funcall(vargs, rb_intern("length"), 0, Qnil); 142 | *argc = NUM2INT(rLength); 143 | 144 | if (*argc > 6) { 145 | printf("ERROR - passed %i args, maximum 6 argument types can be passed", 146 | *argc); 147 | return NULL; 148 | } 149 | 150 | Tracepoint_fire_arg *args = malloc(*argc * sizeof(Tracepoint_fire_arg)); 151 | // printf("SIZE: %i ARGC: %i \n", sizeof(Tracepoint_fire_arg), *argc); 152 | // FIXME check against registered argtypes here 153 | for (int i = 0; i < *argc; i++) { 154 | VALUE val = rb_ary_entry(vargs, i); 155 | switch (TYPE(val)) { 156 | case T_FIXNUM: 157 | args[i].intval = FIX2LONG(val); 158 | break; 159 | case T_STRING: 160 | args[i].strval = RSTRING_PTR(val); 161 | break; 162 | default: 163 | printf("ERROR unsupported type passed for argument %i to fire\n", i); 164 | break; 165 | } 166 | } 167 | // This downcast is explained the comment section of this function. 168 | return (void **)args; 169 | } else { 170 | printf("ERROR - array was expected\n"); 171 | return NULL; 172 | } 173 | } 174 | 175 | // Allocate a static_tracing_tracepoint_type struct for ruby memory management 176 | VALUE 177 | static_tracing_tracepoint_alloc(VALUE klass) { 178 | static_tracing_tracepoint_t *res; 179 | VALUE obj = TypedData_Make_Struct(klass, static_tracing_tracepoint_t, 180 | &static_tracing_tracepoint_type, res); 181 | return obj; 182 | } 183 | 184 | static inline void static_tracing_tracepoint_mark(void *ptr) { /* noop */ 185 | } 186 | 187 | static inline void static_tracing_tracepoint_free(void *ptr) { 188 | static_tracing_tracepoint_t *res = (static_tracing_tracepoint_t *)ptr; 189 | // if (res->name) { 190 | // free(res->name); 191 | // res->name = NULL; 192 | //} 193 | xfree(res); 194 | } 195 | 196 | static inline size_t static_tracing_tracepoint_memsize(const void *ptr) { 197 | return sizeof(static_tracing_provider_t); 198 | } 199 | 200 | static const rb_data_type_t static_tracing_tracepoint_type = { 201 | "static_tracing_tracepoint", 202 | {static_tracing_tracepoint_mark, static_tracing_tracepoint_free, 203 | static_tracing_tracepoint_memsize}, 204 | NULL, 205 | NULL, 206 | RUBY_TYPED_FREE_IMMEDIATELY}; 207 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2019-04-22 16:20:25 -0400 using RuboCop version 0.67.2. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 1 10 | # Cop supports --auto-correct. 11 | Lint/DeprecatedClassMethods: 12 | Exclude: 13 | - 'test/integration/integration_helper.rb' 14 | 15 | # Offense count: 1 16 | Lint/HandleExceptions: 17 | Exclude: 18 | - 'test/integration/integration_helper.rb' 19 | 20 | # Offense count: 1 21 | # Configuration parameters: IgnoreImplicitReferences. 22 | Lint/ShadowedArgument: 23 | Exclude: 24 | - 'lib/ruby-static-tracing/tracer/base.rb' 25 | 26 | # Offense count: 1 27 | Lint/UselessAssignment: 28 | Exclude: 29 | - 'lib/ruby-static-tracing/provider.rb' 30 | 31 | # Offense count: 1 32 | Metrics/AbcSize: 33 | Max: 18 34 | 35 | # Offense count: 2 36 | # Configuration parameters: CountComments, ExcludedMethods. 37 | # ExcludedMethods: refine 38 | Metrics/BlockLength: 39 | Max: 68 40 | 41 | # Offense count: 4 42 | # Configuration parameters: CountComments, ExcludedMethods. 43 | Metrics/MethodLength: 44 | Max: 15 45 | 46 | # Offense count: 4 47 | Naming/AccessorMethodName: 48 | Exclude: 49 | - 'lib/ruby-static-tracing/tracer/base.rb' 50 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracer/base.rb' 51 | 52 | # Offense count: 6 53 | # Configuration parameters: ExpectMatchingDefinition, Regex, IgnoreExecutableScripts, AllowedAcronyms. 54 | # AllowedAcronyms: CLI, DSL, ACL, API, ASCII, CPU, CSS, DNS, EOF, GUID, HTML, HTTP, HTTPS, ID, IP, JSON, LHS, QPS, RAM, RHS, RPC, SLA, SMTP, SQL, SSH, TCP, TLS, TTL, UDP, UI, UID, UUID, URI, URL, UTF8, VM, XML, XMPP, XSRF, XSS 55 | Naming/FileName: 56 | Exclude: 57 | - 'ext/ruby-static-tracing/lib/deps-extconf.rb' 58 | - 'ext/ruby-static-tracing/lib/post-extconf.rb' 59 | - 'lib/ruby-static-tracing.rb' 60 | - 'pkg/ruby-static-tracing-0.0.11/ext/ruby-static-tracing/lib/deps-extconf.rb' 61 | - 'pkg/ruby-static-tracing-0.0.11/ext/ruby-static-tracing/lib/post-extconf.rb' 62 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing.rb' 63 | 64 | # Offense count: 2 65 | # Configuration parameters: EnforcedStyle. 66 | # SupportedStyles: lowercase, uppercase 67 | Naming/HeredocDelimiterCase: 68 | Exclude: 69 | - 'ruby-static-tracing.gemspec' 70 | - 'test/integration/integration_helper.rb' 71 | 72 | # Offense count: 1 73 | # Configuration parameters: Blacklist. 74 | # Blacklist: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$)) 75 | Naming/HeredocDelimiterNaming: 76 | Exclude: 77 | - 'test/integration/integration_helper.rb' 78 | 79 | # Offense count: 1 80 | # Configuration parameters: EnforcedStyleForLeadingUnderscores. 81 | # SupportedStylesForLeadingUnderscores: disallowed, required, optional 82 | Naming/MemoizedInstanceVariableName: 83 | Exclude: 84 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracepoints.rb' 85 | 86 | # Offense count: 1 87 | Security/Eval: 88 | Exclude: 89 | - 'Rakefile' 90 | 91 | # Offense count: 2 92 | # Cop supports --auto-correct. 93 | # Configuration parameters: Keywords. 94 | # Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW 95 | Style/CommentAnnotation: 96 | Exclude: 97 | - 'lib/ruby-static-tracing.rb' 98 | 99 | # Offense count: 17 100 | Style/Documentation: 101 | Exclude: 102 | - 'spec/**/*' 103 | - 'test/**/*' 104 | - 'lib/ruby-static-tracing/configuration.rb' 105 | - 'lib/ruby-static-tracing/tracepoint.rb' 106 | - 'lib/ruby-static-tracing/tracer/base.rb' 107 | - 'lib/ruby-static-tracing/tracer/helpers.rb' 108 | - 'lib/ruby-static-tracing/tracer/latency.rb' 109 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/configuration.rb' 110 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/platform.rb' 111 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracepoint.rb' 112 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracepoints.rb' 113 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracer/base.rb' 114 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracer/concerns/latency_tracer.rb' 115 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracer/helpers.rb' 116 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracer/latency.rb' 117 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracer/stack.rb' 118 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing/tracers.rb' 119 | 120 | # Offense count: 2 121 | Style/DoubleNegation: 122 | Exclude: 123 | - 'lib/ruby-static-tracing.rb' 124 | - 'pkg/ruby-static-tracing-0.0.11/lib/ruby-static-tracing.rb' 125 | 126 | # Offense count: 1 127 | # Cop supports --auto-correct. 128 | Style/ExpandPathArguments: 129 | Exclude: 130 | - 'test/integration/integration_helper.rb' 131 | 132 | # Offense count: 10 133 | # Configuration parameters: AllowedVariables. 134 | Style/GlobalVars: 135 | Exclude: 136 | - 'ext/ruby-static-tracing/extconf.rb' 137 | - 'pkg/ruby-static-tracing-0.0.11/ext/ruby-static-tracing/extconf.rb' 138 | 139 | # Offense count: 1 140 | # Configuration parameters: MinBodyLength. 141 | Style/GuardClause: 142 | Exclude: 143 | - 'lib/tasks/docker.rb' 144 | 145 | # Offense count: 12 146 | Style/IdenticalConditionalBranches: 147 | Exclude: 148 | - 'ext/ruby-static-tracing/lib/deps-extconf.rb' 149 | - 'ext/ruby-static-tracing/lib/post-extconf.rb' 150 | - 'pkg/ruby-static-tracing-0.0.11/ext/ruby-static-tracing/lib/deps-extconf.rb' 151 | - 'pkg/ruby-static-tracing-0.0.11/ext/ruby-static-tracing/lib/post-extconf.rb' 152 | 153 | # Offense count: 2 154 | # Cop supports --auto-correct. 155 | Style/IfUnlessModifier: 156 | Exclude: 157 | - 'ext/ruby-static-tracing/extconf.rb' 158 | - 'pkg/ruby-static-tracing-0.0.11/ext/ruby-static-tracing/extconf.rb' 159 | 160 | # Offense count: 1 161 | # Cop supports --auto-correct. 162 | # Configuration parameters: EnforcedStyle, Autocorrect. 163 | # SupportedStyles: module_function, extend_self 164 | Style/ModuleFunction: 165 | Exclude: 166 | - 'lib/ruby-static-tracing.rb' 167 | 168 | # Offense count: 1 169 | # Cop supports --auto-correct. 170 | # Configuration parameters: EnforcedStyle. 171 | # SupportedStyles: literals, strict 172 | Style/MutableConstant: 173 | Exclude: 174 | - 'test/integration/integration_helper.rb' 175 | 176 | # Offense count: 1 177 | # Cop supports --auto-correct. 178 | # Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods. 179 | # SupportedStyles: predicate, comparison 180 | Style/NumericPredicate: 181 | Exclude: 182 | - 'spec/**/*' 183 | - 'Rakefile' 184 | 185 | # Offense count: 1 186 | # Cop supports --auto-correct. 187 | # Configuration parameters: AllowAsExpressionSeparator. 188 | Style/Semicolon: 189 | Exclude: 190 | - 'test/integration/test_hello/hello.rb' 191 | 192 | # Offense count: 1 193 | # Cop supports --auto-correct. 194 | # Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. 195 | # SupportedStyles: single_quotes, double_quotes 196 | Style/StringLiterals: 197 | Exclude: 198 | - 'test/integration/integration_helper.rb' 199 | 200 | # Offense count: 49 201 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. 202 | # URISchemes: http, https 203 | Metrics/LineLength: 204 | Max: 181 205 | -------------------------------------------------------------------------------- /docs/ruby-interface.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | The gem should store these in a config object, that it will check to determine its behavior. 4 | 5 | Any 'magic numbers', tuneables, or other values that could be useful to be configurable should live here. 6 | 7 | To override behavior, a user just needs to redefine the constants they want to tune after importing. 8 | 9 | Eg, possible implementation could look like this: 10 | ```ruby 11 | StaticTracing::Configuration.some_config = "My Awesome Value" 12 | ``` 13 | 14 | ## Method overriding 15 | 16 | ```ruby 17 | StaticTracing::Configuration.mode = StaticTracing::Configuration::Modes::SIGNAL 18 | StaticTracing::Configuration.signal = StaticTracing::Configuration::Modes::SIGNALS::SIGPROF 19 | ``` 20 | 21 | Where a tracepoint accepts a method as an argument, there are three options: 22 | - "ON": Immediately replace the definition of the original method with a wrapped version that adds the tracepoint. 23 | - "OFF": Require manually toggling tracepoints on and off within source code 24 | - "SIGNAL": Activate behavior of "ON" upon receiving `SIGPROF`, or a user-defined signal to replace this with a configuration value. Upon receiving the signal again, it will be toggled back to "OFF". 25 | 26 | SIGNAL should be the default behavior. `SIGPROF` seems to be unused (and uncaught) by either ruby or unicorn, so it *should* be fine to trap it. SIGPROF seems like the proper signal to trap on. 27 | 28 | The extra processing overhead added by "ON" should always be limited to checking if a probe is enabled, and the cost of returning from a wrapper methed - both of which should be known and low overhead. 29 | 30 | # Tracing providers 31 | 32 | ## Registering a tracing provider 33 | 34 | ``` 35 | provider = StaticTracing::Provider.register(namespace_name) 36 | ``` 37 | 38 | A provider corresponds to a probe namespace, and a stub library for systemtap USDT probe points will be generated for each provider that is declared. 39 | 40 | The provider handle is what will be used to register tracepoints against a particular namespace. 41 | 42 | In is impossible to have provider conflicts, as all providers are singletons. Attempting to create a conflicting provider will fail, and return the original. 43 | All providers registered are accessible via global map of registered providers: 44 | 45 | ``` 46 | provider = StaticTracing.providers[namespace_name] # This should get a handle to an existing registered provider, or throw an error if none exists 47 | ``` 48 | 49 | ``` 50 | StaticTracing::Provider.load(namespace_name) # loads a provider, attaching the tracepoints to the ruby process for a single namespace. Returns the provider. 51 | provider.load # if a handle to the provider is aready known, load it directly 52 | ``` 53 | 54 | This should unregister a provider, disabling any tracepoints that it provided. 55 | 56 | If a provider name is not specified, the default name of `global` should be used. 57 | 58 | # Tracepoints 59 | 60 | ## Creating a single tracepoint 61 | 62 | ## Adding to a provider directly 63 | ``` 64 | tracepoint = provider.add_tracepoint(method_name, *vargs) (vargs should be a list of basic types) 65 | ``` 66 | 67 | ## Implicitly create a tracepoint against a provider 68 | 69 | Since providers are globally unique, the initializer can find or build a provider by, accepting the provider name as a parameter to the initializer. 70 | If a provider of that name already exists, it should be registered against that. If no provider by that name is found, a new one should be registered. 71 | 72 | ``` 73 | tracepoint = StaticTracing::Tracepoint.new(provider_name, name, *vargs) 74 | tracepoint.fire(*vargs) 75 | ``` 76 | 77 | Resulting in the following tracepoints upon inspecting the ELF notes of the process for tracepoints: 78 | 79 | ``` 80 | usdt:/tmp/XXXX.so:provider_name:name 81 | ``` 82 | 83 | Tracepoints can also be directly registered against the global provider by omitting the provider name: 84 | 85 | ``` 86 | usdt:/tmp/XXXX.so:global:name 87 | ``` 88 | 89 | * `provider_name`: string or symbol identifier of a provider 90 | * `name`: string or symbol name to use for this tracepoint. If it is a symbol of a method that exists, this method may be wrapped in a tracepoint 91 | * `*vargs`: List of basic C Enum types for the tracepoint's argument signature. 92 | 93 | ## Tracepoint API 94 | 95 | The tracepoint will have some basic methods: 96 | 97 | ```ruby 98 | tracepoint.fire(*vargs) # verify that they match the types and order match what was given when the tracepoint was registered 99 | # a tracepoint wil only fire if both it and its provider are enabled, and it is attached to by a tracer. 100 | tracepoint.load # this will replace the method or block to be probed with a wrapped version. If the probe is attached to, it will fire. 101 | tracepoint.unload # this will remove a probe from the request flow, replacing wrapped methods or blocks with their original implementations 102 | 103 | tracepoint.enabled? # used to check if a tracepoint is currently attached to by a tracer. 104 | ``` 105 | 106 | For `tracepoint.fire` `*vargs` *must* match the argument signature that was specified when the tracepoint was initialized. 107 | 108 | ## Firing a tracepoint 109 | 110 | "Firing" is emitting data to be traced/probed by an external observer such as bpftrace or dtrace. 111 | 112 | Firing a tracepoint will cause execution to switch to kernel space if the tracepoint is attached. The kernel will then be able to copy 113 | the arguments specified in the tracepoint into kernel space, such as with `perf` or an eBPF program generated by `bpftrace`. 114 | 115 | Once the arguments have been copied, execution is handed back to the ruby userspace code and continues as normal. 116 | 117 | Firing a probe briefly causes the kernel to steal time from the userspace ruby code. 118 | 119 | If nothing is attached to a tracepoint, then a probe won't and cannot be fired, and is basically a no-op. 120 | 121 | ### Argument types 122 | 123 | Only basic data types can be fired off to a tracepoint, such as integers and strings. 124 | 125 | Under the hood, arguments are stored in at most a 64 bit type. Integers and basic data types 126 | can be stored as-is. 127 | 128 | Integers are most likely to be useful for latency measurements. Floating point should also be possible to support within the same 64 bit storage class, though the convention does not appear standardized. 129 | 130 | Strings are supported by passing a pointer to a byte array, but must be interpreted by the tracing program. 131 | In bpftrace, this is achieved with the `str()` built-in function. It should be usable for printing userspace 132 | stack traces, which could be used for flamegraph generation and stack analysis. 133 | 134 | It should also be possible to introspect request metadata in some cases, and use a probe to emit span-tagged 135 | data that could be attached to existing trace data aggregation sources and analyzed. This could be used to 136 | augment distributed tracing data, providing deeper insight into existing trace flows. 137 | 138 | 139 | # Built-in Tracers 140 | 141 | ## Tracers 142 | 143 | A tracer is an abstraction one level up from a tracepoint, providing 144 | 145 | A library of built-in tracers should wrap the basic tracepoint object, so that the user will not have to write tracepoints themselves directly. 146 | 147 | ### Internal latency 148 | 149 | It behooves us to keep trace of the overhead we are introducing by tracing. 150 | 151 | As a best practice, all built-in tracers should fire an integer value corresponding to the number of nanoseconds spent in the tracer itself. This will allow for us to attempt to keep trace of what the total ruby/cruby overhead of our tracepoints are. It will not show time stolen by the kernel while the uprobe is attached, that must be calculated or traced separately. For each USDT tracepoint created then, the argument signature should at minimum look like this: 152 | 153 | Argument signature: 154 | ```C 155 | (long long internal_latency, ...) 156 | ``` 157 | * arg0 - `internal_latency`: 64 bit integer holding nsecs spent in execution, calculated against a monotonic source. 158 | 159 | Followed by any other arguments. `long long` here refers to the C storage class, and is important to specify this way in order to understand how the available 8 bytes for each argument are used. 160 | 161 | ## Latency probes 162 | 163 | Block format: 164 | 165 | ```ruby 166 | StaticTracing::Latency.register(name: my_awesome_block) do 167 | ... 168 | # Some existing application code that we want to wrap in a one-off 169 | ... 170 | end 171 | ``` 172 | 173 | Method format: 174 | 175 | ```ruby 176 | def my_func 177 | sleep 1 178 | end 179 | 180 | StaticTracing.Latency.register(:my_func) 181 | ``` 182 | 183 | This should generate a probe using the existing class/module as a provider (and accept an optional parameter to specify it explicitly). 184 | 185 | Registering a tracepoint against an existing method name should cause the original name to be replaced by the traced version depending on configuration. 186 | 187 | Argument signature: 188 | 189 | ```C 190 | (long long internal_latency, long long run_latency) 191 | ``` 192 | * arg0 - `internal_latency`: 64 bit integer holding nsecs spent in executing this probe, calculated against a monotonic source. 193 | * arg1 - `run_latency`: 64 bit integer holding nsecs spent executing the tracepoint's target method or block. 194 | 195 | It may be possible to for us to user Latency tracers for our own debugging purposes around other `fire` events, as this should show us the time that the kernel has stolen when the uprobe is executed. It may also be possible 196 | 197 | ## Stack probes 198 | 199 | Method format: 200 | 201 | ```ruby 202 | StaticTracing::Stacktracer.register(:my_func) 203 | ``` 204 | 205 | Manually specified name: 206 | ```ruby 207 | t = StaticTracing::Stack.register(name: my_awesome_block) 208 | 209 | t.fire 210 | ``` 211 | * arg0 - `internal_latency`: 64 bit integer holding nsecs spent in executing this probe, calculated against a monotonic source. 212 | * arg1 - `run_latency`: 64 bit integer holding nsecs spent executing the tracepoint's target method or block. 213 | 214 | These tracers would fire the stack trace when the symbol for a method is entered or block would be executed. 215 | 216 | It would be great to be able to fire off the current call stack, eg by wrapping [code from vm\_backtrace](https://github.com/ruby/ruby/blob/a8695d5022d7afbf004765bfb86457fbb9d56457/vm_backtrace.c#L987) 217 | 218 | This could provide something like “StaticTracing::Stack”, but we would need to be very conscious of the overhead of grabbing these stack traces, and profile the underlying ruby C code for this. This is certain to be more expensive than just calculating latency. 219 | 220 | The overhead here seems to all be in `ec_backtrace_to_ary` which does same processing of the execution context pointer from `GET_EC`. We should profile this function a lot to see if it results in observable overhead, and examine the implementation closely to determine the worst case runtime complexity. 221 | 222 | This functionality could allow for building flamegraphs and doing stack profiling, but only for those methods who are participating in tracing, so this is an incomplete view as compared to other profiling techniques already available. 223 | 224 | ## MRI probes 225 | 226 | This is a general term for a suite of possible tools that could provide deeper insight into MRI. 227 | 228 | Especially if the tracers are written in C, they can be used to analyze a number of ruby internals directly at the source-code level. 229 | 230 | This could provide deaper insight into heap and stack usage, as well as object allocations and garbage collection. 231 | 232 | These could be tailored to a particular function, and then placing a Tracepoint would allow reading the desired 233 | internal state at a particular point of execution. 234 | 235 | Eg, placing a `Heaptracer` at the start and end of a request could give insight into how a given request may have caused the characteristics of the heap to change. 236 | 237 | ## Custom probes 238 | 239 | This is a general catch-all for defining new tracepoints on-the-fly. 240 | 241 | Defining custom probes in a block format will require that the block return the values to be fired. 242 | 243 | This is probably the most robust usage of the custom tracepoint, and potentially the most dangerous. 244 | 245 | 246 | ```ruby 247 | StaticTracing.tracepoint(:my_provider, :my_custom_probe, Float, String) do 248 | start = StaticTracing.nsec 249 | string_value = get_value 250 | finish = StaticTracing.nsec 251 | (finish-start, string_value) 252 | end 253 | ``` 254 | 255 | # Usage 256 | 257 | ## Including on a class or module 258 | 259 | For each type of built-in tracer, a helper module should exist to trivially wrap all methods defined directly on (not inheriton on) a class 260 | or module that includes the tracer to have a tracepoint automatically wrap each method. 261 | 262 | For example: 263 | 264 | ```ruby 265 | 266 | class MyController 267 | include StaticTracing::Helper::Latency 268 | def index; end 269 | def create; end 270 | def default_url_options; end 271 | def current_user; end 272 | end 273 | ``` 274 | 275 | This should result in the following tracepoints being added to the process: 276 | 277 | ``` 278 | usdt:/tmp/XXXX.so:my_controller:show 279 | usdt:/tmp/XXXX.so:my_controller:index 280 | usdt:/tmp/XXXX.so:my_controller:create 281 | usdt:/tmp/XXXX.so:my_controller:default_url_options 282 | usdt:/tmp/XXXX.so:my_controller:current_user 283 | ``` 284 | 285 | And, using `bpftrace`, we can attach to each of these and summarize the total time in nanoseconds spent executing each: 286 | 287 | ``` 288 | bpftrace -e 'usdt::my_controller:* { @[probe] = sum(arg1); }' -p ${UNICORN_PID} 289 | Attaching 1 probe... 290 | ^C 291 | 292 | @[usdt:my_controller:current_user]: 51 293 | @[usdt:my_controller:index]: 355 294 | @[usdt:my_controller:create]: 5043 295 | ``` 296 | --------------------------------------------------------------------------------