bpfql 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []