bpfql 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: bd92fdeedd7501b48699058bf9a466287d511beb9ebebaa865dd53f171f4f0e7
4
+ data.tar.gz: 485ece1c48041ea20293838d9bfc6c5ee88fac2b6edfffccc6d696de6f425520
5
+ SHA512:
6
+ metadata.gz: 852f267f0bc56295861c35eb7a18fce76c228f4a896589cf5f51ecf3e69e9b1d0ee19b78192e8e45a1a801b9b778e121af8dc43e9f0eabf20076efa935c28d8a
7
+ data.tar.gz: 1d013db8052f28175fde7f166e3afa350b74a840a2b4012ba4435d629140352a10a01da5c94787c5f1185a92a26805a91d867d416b2f7fd0c31a198bd2141165
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.0
6
+ before_install: gem install bundler -v 2.1.2
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in bpfql.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
8
+ gem "pry"
@@ -0,0 +1,29 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ bpfql (0.1.0)
5
+ rbbcc
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ coderay (1.1.2)
11
+ method_source (0.9.2)
12
+ minitest (5.14.0)
13
+ pry (0.12.2)
14
+ coderay (~> 1.1.0)
15
+ method_source (~> 0.9.0)
16
+ rake (12.3.3)
17
+ rbbcc (0.3.1)
18
+
19
+ PLATFORMS
20
+ ruby
21
+
22
+ DEPENDENCIES
23
+ bpfql!
24
+ minitest (~> 5.0)
25
+ pry
26
+ rake (~> 12.0)
27
+
28
+ BUNDLED WITH
29
+ 2.0.2
@@ -0,0 +1,73 @@
1
+ # BPFQL
2
+
3
+ eBPF query runner. Choose a format in:
4
+
5
+ * Ruby DSL
6
+ * YAML
7
+ * SQL-like query language (in the future)
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'bpfql'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle install
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install bpfql
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ BPFQL do
29
+ select "*"
30
+ from "tracepoint:random:urandom_read"
31
+ where "comm", is: "ruby"
32
+ _and "pid", is: 12345
33
+ end
34
+ ```
35
+
36
+ ```ruby
37
+ BPFQL do
38
+ select "count()"
39
+ from "tracepoint:syscalls:sys_clone_enter"
40
+ group_by "comm"
41
+ interval "15s"
42
+ end
43
+ ```
44
+
45
+ ### YAML format
46
+
47
+ ```yaml
48
+ BPFQL:
49
+ - select: count()
50
+ from: tracepoint:syscalls:sys_clone_enter
51
+ group_by: comm
52
+ stop_after: "30s"
53
+ ```
54
+
55
+ ```yaml
56
+ BPFQL:
57
+ - select: count()
58
+ from: tracepoint:syscalls:sys_clone_enter
59
+ where:
60
+ - comm is "ruby"
61
+ - pid is 12345
62
+ ```
63
+
64
+ ## Development
65
+
66
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
67
+
68
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
69
+
70
+ ## Contributing
71
+
72
+ Bug reports and pull requests are welcome on GitHub at https://github.com/udzura/bpfql.
73
+
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "bpfql"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,24 @@
1
+ require_relative 'lib/bpfql/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "bpfql"
5
+ spec.version = Bpfql::VERSION
6
+ spec.authors = ["Uchio Kondo"]
7
+ spec.email = ["udzura@udzura.jp"]
8
+
9
+ spec.summary = %q{eBPF query runner}
10
+ spec.description = %q{eBPF query runner. Use Ruby DSL / yaml / or plane text}
11
+ spec.homepage = "https://github.com/udzura/bpfql"
12
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
13
+
14
+ # Specify which files should be added to the gem when it is released.
15
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
16
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
17
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18
+ end
19
+ spec.bindir = "exe"
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "rbbcc"
24
+ end
@@ -0,0 +1,3 @@
1
+ BPFQL:
2
+ - select: [pid, ts, comm, got_bits]
3
+ from: tracepoint:random:urandom_read
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bpfql/cli"
4
+
5
+ Bpfql::Cli.run(ARGV)
@@ -0,0 +1,6 @@
1
+ require "bpfql/version"
2
+ require "bpfql/query"
3
+ require "bpfql/runner"
4
+
5
+ module Bpfql
6
+ end
@@ -0,0 +1,23 @@
1
+ require 'bpfql'
2
+
3
+ module Bpfql
4
+ module Cli
5
+ USAGE = <<~USAGE
6
+ #{File.basename $0} version #{Bpfql::VERSION}
7
+ usage:
8
+ \t#{File.basename $0} <YAML_FILE>
9
+ USAGE
10
+
11
+ def self.run(argv)
12
+ if argv.size != 1
13
+ $stderr.puts USAGE
14
+ exit 1
15
+ end
16
+
17
+ yaml_file = argv[0]
18
+ q = Bpfql::Query.parse_yaml(File.read yaml_file)
19
+ r = Bpfql::Runner.new(q[0])
20
+ r.run
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,116 @@
1
+ require 'yaml'
2
+
3
+ module Bpfql
4
+ class Query
5
+ def self.parse_yaml(yaml)
6
+ data = YAML.load(yaml)
7
+ data['BPFQL'].map do |query|
8
+ Query.new do |builder|
9
+ builder.select = SelectOption.new(query['select'])
10
+ builder.from = ProbeOption.new(query['from'])
11
+ if query['where']
12
+ builder.where = FilterOption.parse(query['where'])
13
+ end
14
+ builder.group_by = query['group_by'] # Accepts nil
15
+ builder.interval = query['interval'] # Accepts nil
16
+ if query['stop_after']
17
+ builder.stop = StopOption.new(:after, query['stop_after'])
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def initialize(&b)
24
+ b.call(self)
25
+ end
26
+ attr_accessor :select, :from, :where, :group_by, :interval, :stop
27
+ alias probe from
28
+
29
+ class SelectOption < Struct.new(:members, :type)
30
+ def initialize(query)
31
+ case query
32
+ when String, Symbol
33
+ if query.include? ','
34
+ self.members = query.split(',').map{|v| v.strip }
35
+ else
36
+ self.members = [query]
37
+ end
38
+ when Array
39
+ self.members = query
40
+ end
41
+ end
42
+ end
43
+
44
+ class ProbeOption < Struct.new(:type, :arg1, :arg2)
45
+ def initialize(probe)
46
+ # FIXME: complcated probe, e.g. uprobe and USDT has 4 sections
47
+ super(*probe.split(':'))
48
+ end
49
+
50
+ def tracepoint?
51
+ self.type == "tracepoint"
52
+ end
53
+
54
+ def kprobe?
55
+ self.type == "kprobe"
56
+ end
57
+
58
+ def uprobe?
59
+ self.type == "uprobe"
60
+ end
61
+
62
+ def usdt?
63
+ self.type == "usdt"
64
+ end
65
+
66
+ def to_s
67
+ [type, arg1, arg2].join ":"
68
+ end
69
+ end
70
+
71
+ class FilterOption < Struct.new(:lhs, :op, :rhs)
72
+ def self.parse(where)
73
+ where_list = Array(where)
74
+ where_list.map do |whr|
75
+ FilterOption.new(*whr)
76
+ end
77
+ end
78
+
79
+ def initialize(lhs, op=nil, rhs=nil)
80
+ if !rhs # args.size < 3
81
+ m = /^([^\s]+)\s+([^\s]+)\s+([^\s]+|"[^"]+"|'[^']+')$/.match(lhs)
82
+ unless m
83
+ raise "Failed to parse where clause: #{lhs}"
84
+ end
85
+ if m2 = /^['"](.+)['"]$/.match(m[3])
86
+ rhs = m2[1]
87
+ else
88
+ rhs = m[3]
89
+ end
90
+ super(m[1], m[2].to_sym, rhs)
91
+ else
92
+ super(lhs, op.to_sym, rhs)
93
+ end
94
+ end
95
+ end
96
+
97
+ class StopOption < Struct.new(:timing, :seconds)
98
+ def initialize(timing, secstr)
99
+ seconds = 0
100
+ m = /^(\d+)(\w+)?$/.match(secstr.to_s)
101
+ unless m
102
+ raise "Failed to parse stop option clause: #{secstr}"
103
+ end
104
+ case m[2]
105
+ when nil, /^s.*/
106
+ seconds = m[1].to_i
107
+ when /^m.*/
108
+ seconds = m[1].to_i * 60
109
+ when /^h.*/
110
+ seconds = m[1].to_i * 60 * 60
111
+ end
112
+ super(timing, seconds)
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,174 @@
1
+ require 'rbbcc'
2
+
3
+ module Bpfql
4
+ # TODO: REFACTOR ME!!
5
+ class Runner
6
+ def initialize(qobj)
7
+ @query_builder = qobj
8
+ @fmt = gen_fmt
9
+ @start_ts = 0
10
+ end
11
+
12
+ def run
13
+ @module = RbBCC::BCC.new(text: bpf_source)
14
+ puts(@fmt.gsub(/(\.\d+f|d)/, 's') % tracepoint_fields_sorted.map(&:upcase))
15
+
16
+ @module["events"].open_perf_buffer do |cpu, data, size|
17
+ event = @module["events"].event(data)
18
+ puts(@fmt % gen_extract_data(event))
19
+ end
20
+ loop {
21
+ begin
22
+ @module.perf_buffer_poll()
23
+ rescue Interrupt
24
+ break
25
+ end
26
+ }
27
+ puts "Exiting bpfql..."
28
+ end
29
+
30
+ def gen_fmt
31
+ fmt = []
32
+ fmt << '%-18.9f' if tracepoint_fields.include?("ts")
33
+ fmt << '%-16s' if tracepoint_fields.include?("comm")
34
+ fmt << '%-6d' if tracepoint_fields.include?("pid")
35
+ fields_noncommon.each do |f|
36
+ if field_maps[f] =~ /^char \w+\[\w+\]$/
37
+ fmt << '%-16s'
38
+ else
39
+ fmt << '%-8d'
40
+ end
41
+ end
42
+ fmt.join ' '
43
+ end
44
+
45
+ def gen_extract_data(event)
46
+ ret = []
47
+ if tracepoint_fields.include?("ts")
48
+ @start_ts = event.ts if @start_ts == 0
49
+ time_s = ((event.ts - @start_ts).to_f) / 1000000000
50
+ ret << time_s
51
+ end
52
+
53
+ tracepoint_fields_sorted.each do |f|
54
+ next if f == 'ts'
55
+ ret << event.send(f)
56
+ end
57
+ ret
58
+ end
59
+
60
+ def bpf_source
61
+ <<~SOURCE
62
+ #include <linux/sched.h>
63
+ #define BPFQL_ARY_MAX 64
64
+ #define BPFQL_STR_MAX 64
65
+ #{data_struct_source}
66
+
67
+ BPF_PERF_OUTPUT(events);
68
+
69
+ #{trace_func_source}
70
+ SOURCE
71
+ end
72
+
73
+ def data_struct_source
74
+ <<~STRUCT
75
+ struct data_t {
76
+ #{tracepoint_fields.map{|k| field_maps[k] + ";"}.join("\n ")}
77
+ };
78
+ STRUCT
79
+ end
80
+
81
+ def trace_func_source
82
+ if qb.probe.tracepoint?
83
+ src = <<~FUNCTION
84
+ TRACEPOINT_PROBE(#{qb.probe.arg1}, #{qb.probe.arg2}) {
85
+ struct data_t data = {};
86
+ __ASSIGN_PID__
87
+ __ASSIGN_TS__
88
+ __ASSIGN_COMM__
89
+
90
+ __ASSIGN_FIELDS__
91
+ events.perf_submit(args, &data, sizeof(data));
92
+ return 0;
93
+ }
94
+ FUNCTION
95
+ src.sub!('__ASSIGN_PID__', tracepoint_fields.include?("pid") ? 'data.pid = bpf_get_current_pid_tgid();' : '')
96
+ src.sub!('__ASSIGN_TS__', tracepoint_fields.include?("ts") ? 'data.ts = bpf_ktime_get_ns();' : '')
97
+ src.sub!('__ASSIGN_COMM__', tracepoint_fields.include?("comm") ? 'bpf_get_current_comm(&data.comm, sizeof(data.comm));' : '')
98
+
99
+ if fields_noncommon.empty?
100
+ src.sub!('__ASSIGN_FIELDS__', '')
101
+ else
102
+ assigner = fields_noncommon.map { |field|
103
+ "data.#{field} = args->#{field};"
104
+ }.join("\n")
105
+ src.sub!('__ASSIGN_FIELDS__', assigner)
106
+ end
107
+ src
108
+ else
109
+ raise NotImplementedError, "unsupported probe: #{qb.probe.to_s}"
110
+ end
111
+ end
112
+
113
+ def tracepoint_fields
114
+ @fields ||= begin
115
+ if qb.select.members[0] == '*'
116
+ field_maps.keys
117
+ else
118
+ qb.select.members
119
+ end
120
+ end
121
+ end
122
+
123
+ def tracepoint_fields_sorted
124
+ [].tap do |a|
125
+ a << 'ts' if tracepoint_fields.include?("ts")
126
+ a << 'comm' if tracepoint_fields.include?("comm")
127
+ a << 'pid' if tracepoint_fields.include?("pid")
128
+ a.concat fields_noncommon
129
+ end
130
+ end
131
+
132
+ def fields_noncommon
133
+ tracepoint_fields - %w(pid ts comm)
134
+ end
135
+
136
+ def tracepoint_field_maps_from_format
137
+ @_tfmap ||= begin
138
+ fmt = File.read "/sys/kernel/debug/tracing/events/#{qb.probe.arg1}/#{qb.probe.arg2}/format"
139
+ dst = {}
140
+ fmt.each_line do |l|
141
+ next unless l.include?("field:")
142
+ next if l.include?("common_")
143
+ kv = l.split(";").map{|elm| elm.split(":").map(&:strip)}.reject{|e| e.size != 2 }
144
+ kv = Hash[kv]
145
+ field_name = kv['field'].split.last
146
+ field_type = if kv['field'] =~ /^const char \* #{field_name}$/
147
+ "char #{field_name}[BPFQL_STR_MAX]"
148
+ elsif kv['field'] =~ /^const char \*const \* #{field_name}$/
149
+ warn("not yet fully unsupported field type")
150
+ "char #{field_name}[BPFQL_ARY_MAX][BPFQL_STR_MAX]"
151
+ else
152
+ kv['field']
153
+ end
154
+ dst[field_name] = field_type
155
+ end
156
+ dst
157
+ end
158
+ end
159
+
160
+ def field_maps
161
+ {
162
+ "pid" => "u32 pid",
163
+ "ts" => "u64 ts",
164
+ "comm" => "char comm[TASK_COMM_LEN]"
165
+ }.merge(tracepoint_field_maps_from_format)
166
+ end
167
+
168
+ private
169
+ def qb
170
+ @query_builder
171
+ end
172
+
173
+ end
174
+ end
@@ -0,0 +1,3 @@
1
+ module Bpfql
2
+ VERSION = "0.1.0"
3
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bpfql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Uchio Kondo
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-03-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rbbcc
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ description: eBPF query runner. Use Ruby DSL / yaml / or plane text
28
+ email:
29
+ - udzura@udzura.jp
30
+ executables:
31
+ - bpfql
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - ".travis.yml"
37
+ - Gemfile
38
+ - Gemfile.lock
39
+ - README.md
40
+ - Rakefile
41
+ - bin/console
42
+ - bin/setup
43
+ - bpfql.gemspec
44
+ - examples/random-simple.yml
45
+ - exe/bpfql
46
+ - lib/bpfql.rb
47
+ - lib/bpfql/cli.rb
48
+ - lib/bpfql/query.rb
49
+ - lib/bpfql/runner.rb
50
+ - lib/bpfql/version.rb
51
+ homepage: https://github.com/udzura/bpfql
52
+ licenses: []
53
+ metadata: {}
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.3.0
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.0.6
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: eBPF query runner
73
+ test_files: []