hutils 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +16 -0
- data/bin/lcut +43 -0
- data/bin/ltap +70 -0
- data/bin/lviz +71 -0
- data/lib/hutils/curses_visualizer.rb +207 -0
- data/lib/hutils/ltap/conf.rb +41 -0
- data/lib/hutils/ltap/paper_trail_drainer.rb +75 -0
- data/lib/hutils/ltap/splunk_drainer.rb +123 -0
- data/lib/hutils/ltap.rb +3 -0
- data/lib/hutils/node_navigator.rb +52 -0
- data/lib/hutils/stripper.rb +14 -0
- data/lib/hutils/text_visualizer.rb +61 -0
- data/lib/hutils.rb +177 -0
- metadata +120 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 430d7ed6697d57adb45026682ae10d17bddd9e58
|
4
|
+
data.tar.gz: 229970d4107ed3f39c4b837aafc60d834eb46905
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 43ea0dc9490a8f4fa51a719640e9d17afac0ca2d344aada8f9453e992ed23957b21e1ada30b415a2b7c7b7eed9c136fae9933079624998e87bfc16c86d9608db
|
7
|
+
data.tar.gz: 87aa2315323f14e683a96a9366c95189b2975530f3ab3ae9c7a465b3028b405b70727eed656deb508e9489abd44b519d69c93c4b88f9225eceb0b69b51106895
|
data/README.md
ADDED
data/bin/lcut
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
require_relative "../lib/hutils"
|
6
|
+
|
7
|
+
allow_empty = false
|
8
|
+
delimiter = "\t"
|
9
|
+
file = nil
|
10
|
+
verbose = false
|
11
|
+
|
12
|
+
opts = OptionParser.new do |opts|
|
13
|
+
opts.banner = "Usage: lsel [options] <fields>"
|
14
|
+
opts.on("-d", "--delimiter [DELIMITER]", "Delimiter separating output") { |d|
|
15
|
+
delimiter = d
|
16
|
+
}
|
17
|
+
opts.on("-e", "--allow-empty", "Output empty lines") { |e| allow_empty = e }
|
18
|
+
opts.on("-f", "--file [FILE]", "File to read") { |f| file = f }
|
19
|
+
opts.on("-h", "--help", "Show this help string") { |h|
|
20
|
+
if h
|
21
|
+
puts(opts.help)
|
22
|
+
exit(0)
|
23
|
+
end
|
24
|
+
}
|
25
|
+
opts.on("-v", "--verbose", "Verbose mode") { |v| verbose = v }
|
26
|
+
end
|
27
|
+
opts.parse!
|
28
|
+
|
29
|
+
if !file && $stdin.tty? || !ARGV.first
|
30
|
+
abort(opts.help)
|
31
|
+
end
|
32
|
+
|
33
|
+
file = file ? File.open(file) : $stdin
|
34
|
+
file.each_line do |line|
|
35
|
+
messages = Hutils::Parser.new(line).parse
|
36
|
+
messages.each do |message|
|
37
|
+
values = ARGV.map { |f| message[f] ? message[f] : nil }
|
38
|
+
if allow_empty || !values.all? { |v| v.nil? }
|
39
|
+
puts values.join(delimiter)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
file.close
|
data/bin/ltap
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
require_relative "../lib/hutils"
|
6
|
+
require_relative "../lib/hutils/ltap"
|
7
|
+
|
8
|
+
conf = Hutils::Ltap::Conf.new
|
9
|
+
conf.load
|
10
|
+
|
11
|
+
opts = OptionParser.new do |opts|
|
12
|
+
opts.banner = "Usage: ltap [options] <query>"
|
13
|
+
opts.on("-h", "--help", "Show this help string") { |h|
|
14
|
+
if h
|
15
|
+
puts(opts.help)
|
16
|
+
exit(0)
|
17
|
+
end
|
18
|
+
}
|
19
|
+
opts.on("-k", "--key [KEY]", "Service API key") { |t| conf.timeout = t }
|
20
|
+
opts.on("-p", "--profile [PROFILE]", "Conf profile") { |p| conf.profile = p }
|
21
|
+
opts.on("-t", "--timeout [TIMEOUT]", "Job timeout") { |t| conf.timeout = t }
|
22
|
+
opts.on("-y", "--type [TYPE]", "Service type") { |t| conf.timeout = t }
|
23
|
+
opts.on("-u", "--url [URL]", "Service API URL") { |u| conf.url = u }
|
24
|
+
opts.on("-v", "--verbose", "Verbose mode") { |v| conf.verbose = v }
|
25
|
+
end
|
26
|
+
opts.parse!
|
27
|
+
|
28
|
+
# load a profile from ~/.ltap if one was specified
|
29
|
+
if conf.profile
|
30
|
+
conf.load_section(conf.profile)
|
31
|
+
end
|
32
|
+
|
33
|
+
unless conf.type
|
34
|
+
abort("Must set a service type; use ~/.ltap or --type")
|
35
|
+
end
|
36
|
+
|
37
|
+
unless ARGV.first
|
38
|
+
abort(opts.help)
|
39
|
+
end
|
40
|
+
|
41
|
+
drainer = case conf.type
|
42
|
+
when "papertrail"
|
43
|
+
Hutils::Ltap::PaperTrailDrainer
|
44
|
+
when "splunk"
|
45
|
+
Hutils::Ltap::SplunkDrainer
|
46
|
+
else
|
47
|
+
abort("Unknown type: #{conf.type}")
|
48
|
+
end
|
49
|
+
|
50
|
+
drainer = drainer.new(
|
51
|
+
key: conf.key,
|
52
|
+
query: ARGV.first,
|
53
|
+
timeout: conf.timeout,
|
54
|
+
url: conf.url,
|
55
|
+
verbose: conf.verbose
|
56
|
+
)
|
57
|
+
|
58
|
+
thread = Thread.start do
|
59
|
+
$stdout.puts drainer.run
|
60
|
+
end
|
61
|
+
|
62
|
+
# cancel a running job on a SIGINT because Splunk search slots are apparently a
|
63
|
+
# very valuable limited resource
|
64
|
+
trap('SIGINT', 'SIGTERM') do
|
65
|
+
drainer.cancel_job
|
66
|
+
thread.terminate
|
67
|
+
abort("Caught deadly signal")
|
68
|
+
end
|
69
|
+
|
70
|
+
thread.join
|
data/bin/lviz
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "inifile"
|
4
|
+
require "optparse"
|
5
|
+
require_relative "../lib/hutils"
|
6
|
+
|
7
|
+
colors = $stdout.tty?
|
8
|
+
compact = false
|
9
|
+
highlights = %w(endpoint path route_signature user user_id)
|
10
|
+
ignore = %w(at)
|
11
|
+
interactive = false
|
12
|
+
|
13
|
+
ini = IniFile.load(ENV["HOME"] + "/.lviz")
|
14
|
+
if g = ini && ini["global"]
|
15
|
+
colors = g["colors"] if g["colors"]
|
16
|
+
compact = g["compact"] if g["compact"]
|
17
|
+
highlights = g["highlights"].split(",") if g["highlights"]
|
18
|
+
ignore = g["ignore"].split(",") if g["ignore"]
|
19
|
+
end
|
20
|
+
|
21
|
+
opts = OptionParser.new do |opts|
|
22
|
+
opts.banner = "Usage: lviz [options] <file> ..."
|
23
|
+
opts.on("--compact", "Compact display") { |c| compact = c }
|
24
|
+
opts.on("-h", "--help", "Show this help string") { |h|
|
25
|
+
if h
|
26
|
+
puts(opts.help)
|
27
|
+
exit(0)
|
28
|
+
end
|
29
|
+
}
|
30
|
+
opts.on("--highlights", "Keys to highlight (comma separated)") { |h|
|
31
|
+
highlights = h.split(",") rescue []
|
32
|
+
}
|
33
|
+
opts.on("--ignore", "Keys to ignore (comma separated)") { |i|
|
34
|
+
ignore = i.split(",") rescue []
|
35
|
+
}
|
36
|
+
opts.on("-i", "--interactive", "Interactive mode") { |i| interactive = i }
|
37
|
+
opts.on("--no-color", "Disable colors") { |c| colors = c }
|
38
|
+
end
|
39
|
+
opts.parse!
|
40
|
+
|
41
|
+
# ARGF will read from stdin if available, and otherwise fall back to files
|
42
|
+
# named as parameters (similar to the behavior of `cat`)
|
43
|
+
lines = Hutils::Parser.new(ARGF.read).parse
|
44
|
+
Hutils::Stripper.new(lines, ignore).run
|
45
|
+
root = Hutils::TreeBuilder.new(lines).build
|
46
|
+
|
47
|
+
if interactive
|
48
|
+
require_relative "../lib/hutils/curses_visualizer"
|
49
|
+
require_relative "../lib/hutils/node_navigator"
|
50
|
+
|
51
|
+
unless $stdin.tty?
|
52
|
+
abort("Can't start in interactive mode if STDIN isn't a TTY")
|
53
|
+
end
|
54
|
+
|
55
|
+
Hutils::CursesVisualizer.new(
|
56
|
+
colors: colors,
|
57
|
+
compact: compact,
|
58
|
+
highlights: highlights,
|
59
|
+
root: root
|
60
|
+
).run
|
61
|
+
else
|
62
|
+
require_relative "../lib/hutils/text_visualizer"
|
63
|
+
|
64
|
+
Hutils::TextVisualizer.new(
|
65
|
+
colors: colors,
|
66
|
+
compact: compact,
|
67
|
+
highlights: highlights,
|
68
|
+
out: $stdout,
|
69
|
+
root: root
|
70
|
+
).display
|
71
|
+
end
|
@@ -0,0 +1,207 @@
|
|
1
|
+
require "curses"
|
2
|
+
|
3
|
+
module Hutils
|
4
|
+
class CursesVisualizer
|
5
|
+
def initialize(colors:, compact:, highlights:, root:)
|
6
|
+
@colors = colors
|
7
|
+
@highlights = highlights
|
8
|
+
@line_buffer = []
|
9
|
+
@root = root
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
Curses.cbreak # no need for a newline to get type chars
|
14
|
+
Curses.curs_set(0) # invisible
|
15
|
+
Curses.noecho # don't echo character on a getch
|
16
|
+
|
17
|
+
Curses.start_color
|
18
|
+
Curses.use_default_colors
|
19
|
+
|
20
|
+
Curses.init_pair(COLOR_KEY, -1, Curses::COLOR_BLUE)
|
21
|
+
Curses.init_pair(COLOR_HIGHLIGHT, Curses::COLOR_BLACK, Curses::COLOR_YELLOW)
|
22
|
+
|
23
|
+
Curses.init_screen
|
24
|
+
|
25
|
+
trap("SIGINT", "SIGTERM") do
|
26
|
+
Curses.close_screen
|
27
|
+
# @todo: curses shutdown?
|
28
|
+
$stdout.puts "Caught deadly signal"
|
29
|
+
$stdout.flush
|
30
|
+
exit(0)
|
31
|
+
end
|
32
|
+
|
33
|
+
# set root node as "expanded"
|
34
|
+
@root.tags[:expanded] = true
|
35
|
+
|
36
|
+
@need_repaint = true
|
37
|
+
@selected_line = 0
|
38
|
+
build_line_buffer
|
39
|
+
|
40
|
+
loop do
|
41
|
+
if @need_repaint
|
42
|
+
build_line_buffer
|
43
|
+
paint
|
44
|
+
@need_repaint = false
|
45
|
+
end
|
46
|
+
handle_key
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
COLOR_KEY = 1
|
53
|
+
COLOR_HIGHLIGHT = 2
|
54
|
+
|
55
|
+
def build_line_buffer
|
56
|
+
@line_buffer = []
|
57
|
+
|
58
|
+
traverse_tree(@root) do |node|
|
59
|
+
@line_buffer << node
|
60
|
+
end
|
61
|
+
|
62
|
+
# remove the root node; it's not actually displayed
|
63
|
+
@line_buffer.shift
|
64
|
+
|
65
|
+
if @selected_line >= @line_buffer.count
|
66
|
+
@selected_line = @line_buffer.count - 1
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def traverse_tree(node, &block)
|
71
|
+
yield node
|
72
|
+
if node.tags[:expanded]
|
73
|
+
node.slots.each { |child| traverse_tree(child, &block) }
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def collapse_all
|
78
|
+
collapse_node(@root)
|
79
|
+
end
|
80
|
+
|
81
|
+
def collapse_node(node)
|
82
|
+
node.tags[:expanded] = false if node != @root
|
83
|
+
node.slots.each { |child| collapse_node(child) }
|
84
|
+
end
|
85
|
+
|
86
|
+
def expand_all(node = @root)
|
87
|
+
node.tags[:expanded] = true if node != @root
|
88
|
+
node.slots.each { |child| expand_all(child) }
|
89
|
+
end
|
90
|
+
|
91
|
+
def toggle_node
|
92
|
+
if node = @line_buffer[@selected_line]
|
93
|
+
node.tags[:expanded] = !node.tags[:expanded]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def expanded_str(node)
|
98
|
+
if node.slots.count > 0
|
99
|
+
node.tags[:expanded] ? "[-]" : "[+]"
|
100
|
+
else
|
101
|
+
" "
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def handle_key
|
106
|
+
case Curses.getch
|
107
|
+
when Curses::KEY_RESIZE then need_repaint
|
108
|
+
when ' ' then toggle_node && need_repaint
|
109
|
+
when ?c then collapse_all && need_repaint
|
110
|
+
when ?e then expand_all && need_repaint
|
111
|
+
when ?j then move_next
|
112
|
+
when ?k then move_prev
|
113
|
+
when ?o then toggle_node && need_repaint
|
114
|
+
when ?q then exit(0)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def move_next
|
119
|
+
old_selected_line = @selected_line
|
120
|
+
|
121
|
+
@selected_line += 1
|
122
|
+
if @selected_line == @line_buffer.count
|
123
|
+
@selected_line = @line_buffer.count - 1
|
124
|
+
end
|
125
|
+
|
126
|
+
paint_line(old_selected_line)
|
127
|
+
paint_line(@selected_line)
|
128
|
+
end
|
129
|
+
|
130
|
+
def move_prev
|
131
|
+
old_selected_line = @selected_line
|
132
|
+
|
133
|
+
@selected_line -= 1
|
134
|
+
if @selected_line < 0
|
135
|
+
@selected_line = 0
|
136
|
+
end
|
137
|
+
|
138
|
+
paint_line(old_selected_line)
|
139
|
+
paint_line(@selected_line)
|
140
|
+
end
|
141
|
+
|
142
|
+
def need_repaint
|
143
|
+
@need_repaint = true
|
144
|
+
end
|
145
|
+
|
146
|
+
def paint
|
147
|
+
Curses.clear
|
148
|
+
|
149
|
+
@line_buffer.each_with_index do |node, line|
|
150
|
+
break if line > Curses.lines
|
151
|
+
paint_node(node, line)
|
152
|
+
end
|
153
|
+
|
154
|
+
Curses.refresh
|
155
|
+
end
|
156
|
+
|
157
|
+
def paint_line(line)
|
158
|
+
paint_node(@line_buffer[line], line)
|
159
|
+
end
|
160
|
+
|
161
|
+
def safe_addstr(str, num_cols_written)
|
162
|
+
if num_cols_written + str.length > Curses.cols
|
163
|
+
left = Curses.cols - num_cols_written
|
164
|
+
str = str.dup[0, left]
|
165
|
+
end
|
166
|
+
Curses.addstr(str)
|
167
|
+
num_cols_written + str.length
|
168
|
+
end
|
169
|
+
|
170
|
+
# `line` should already be incremented to the correct position before
|
171
|
+
# entering this method
|
172
|
+
def paint_node(node, line)
|
173
|
+
n = 0
|
174
|
+
Curses.setpos(line, 0)
|
175
|
+
|
176
|
+
if @selected_line == line
|
177
|
+
Curses.attron(Curses::A_UNDERLINE)
|
178
|
+
end
|
179
|
+
|
180
|
+
n = safe_addstr("\t" * (node.depth - 1), n)
|
181
|
+
n = safe_addstr(expanded_str(node) + " ", n)
|
182
|
+
node.common.to_a.sort_by { |k, v| k }.each do |k, v|
|
183
|
+
if v == true
|
184
|
+
color(COLOR_KEY) { n = safe_addstr("#{k}", n) }
|
185
|
+
else
|
186
|
+
if @highlights.include?(k)
|
187
|
+
color(COLOR_HIGHLIGHT) { n = safe_addstr("#{k}=#{v}", n) }
|
188
|
+
else
|
189
|
+
color(COLOR_KEY) { n = safe_addstr("#{k}", n) }
|
190
|
+
n = safe_addstr("=#{v}", n)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
n = safe_addstr(" ", n)
|
194
|
+
end
|
195
|
+
|
196
|
+
Curses.attroff(Curses::A_UNDERLINE)
|
197
|
+
end
|
198
|
+
|
199
|
+
def color(key, &block)
|
200
|
+
if @colors
|
201
|
+
Curses.attron(Curses::color_pair(key) | Curses::A_NORMAL) { yield }
|
202
|
+
else
|
203
|
+
yield
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "inifile"
|
2
|
+
|
3
|
+
module Hutils::Ltap
|
4
|
+
class Conf
|
5
|
+
attr_accessor :key
|
6
|
+
attr_accessor :profile
|
7
|
+
attr_accessor :timeout
|
8
|
+
attr_accessor :type
|
9
|
+
attr_accessor :url
|
10
|
+
attr_accessor :verbose
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@ini = IniFile.load(ENV["HOME"] + "/.ltap")
|
14
|
+
self.timeout = 60
|
15
|
+
self.verbose = false
|
16
|
+
end
|
17
|
+
|
18
|
+
def load
|
19
|
+
load_section("global")
|
20
|
+
end
|
21
|
+
|
22
|
+
def load_section(name)
|
23
|
+
if section = @ini && @ini[name]
|
24
|
+
load_value(section, :key)
|
25
|
+
load_value(section, :profile)
|
26
|
+
load_value(section, :timeout)
|
27
|
+
load_value(section, :type)
|
28
|
+
load_value(section, :url)
|
29
|
+
load_value(section, :verbose)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def load_value(section, name)
|
36
|
+
if value = section[name.to_s]
|
37
|
+
send("#{name}=", value)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Hutils::Ltap
|
4
|
+
class PaperTrailDrainer
|
5
|
+
PAPER_TRAIL_URL = "https://papertrailapp.com"
|
6
|
+
|
7
|
+
def initialize(key:, timeout:, query:, url:, verbose:)
|
8
|
+
@api = Excon.new(PAPER_TRAIL_URL,
|
9
|
+
headers: {
|
10
|
+
"X-Papertrail-Token" => key
|
11
|
+
})
|
12
|
+
@query = query
|
13
|
+
@timeout = timeout
|
14
|
+
@verbose = verbose
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
messages = []
|
19
|
+
min_id = nil
|
20
|
+
start = Time.now
|
21
|
+
|
22
|
+
loop do
|
23
|
+
new_messages, reached_beginning, min_id = fetch_page(min_id)
|
24
|
+
messages += new_messages
|
25
|
+
|
26
|
+
# break if PaperTrail has indicated that we've reached the beginning of
|
27
|
+
# our results, or if we've approximately hit our timeout
|
28
|
+
if reached_beginning || (Time.now - start).to_i > @timeout
|
29
|
+
break
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
messages
|
34
|
+
rescue RateLimited
|
35
|
+
$stderr.puts "Papertrail rate limit reached"
|
36
|
+
messages
|
37
|
+
end
|
38
|
+
|
39
|
+
def cancel_job
|
40
|
+
debug("cancelled [noop]")
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
class RateLimited < StandardError
|
46
|
+
end
|
47
|
+
|
48
|
+
def debug(str)
|
49
|
+
if @verbose
|
50
|
+
puts str
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def fetch_page(max_id)
|
55
|
+
resp = @api.get(
|
56
|
+
path: "/api/v1/events/search.json",
|
57
|
+
expects: [200, 429],
|
58
|
+
query: {
|
59
|
+
max_id: max_id,
|
60
|
+
q: @query
|
61
|
+
}.reject { |k, v| v == nil })
|
62
|
+
|
63
|
+
if resp.status == 429
|
64
|
+
raise RateLimited
|
65
|
+
end
|
66
|
+
|
67
|
+
data = JSON.parse(resp.body)
|
68
|
+
debug("backend_timeout: #{data["backend_timeout"] || false} " +
|
69
|
+
"min_id: #{data["min_id"]} " +
|
70
|
+
"reached_beginning: #{data["reached_beginning"] || false}")
|
71
|
+
messages = data["events"].map { |e| e["message"].strip }
|
72
|
+
[messages, data["reached_beginning"], data["min_id"]]
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require "csv"
|
2
|
+
require "excon"
|
3
|
+
require "json"
|
4
|
+
require "uri"
|
5
|
+
|
6
|
+
module Hutils::Ltap
|
7
|
+
class SplunkDrainer
|
8
|
+
def initialize(key:, timeout:, query:, url:, verbose:)
|
9
|
+
@timeout = timeout
|
10
|
+
@query = query
|
11
|
+
@verbose = verbose
|
12
|
+
|
13
|
+
@user = URI.parse(url).user
|
14
|
+
@api = Excon.new(url)
|
15
|
+
end
|
16
|
+
|
17
|
+
def run
|
18
|
+
create_job(@query)
|
19
|
+
start = Time.now
|
20
|
+
|
21
|
+
loop do
|
22
|
+
sleep(2)
|
23
|
+
break if job_finished?
|
24
|
+
|
25
|
+
# finalize the job if we've broken our timeout point
|
26
|
+
if (Time.now - start).to_i > @timeout
|
27
|
+
finalize_job
|
28
|
+
break
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
get_job_results
|
33
|
+
end
|
34
|
+
|
35
|
+
def cancel_job
|
36
|
+
return unless @job_id
|
37
|
+
@api.post(
|
38
|
+
path: "/servicesNS/#{@user}/search/search/jobs/#{@job_id}/control",
|
39
|
+
expects: 200,
|
40
|
+
body: URI.encode_www_form({
|
41
|
+
action: "cancel"
|
42
|
+
})
|
43
|
+
)
|
44
|
+
debug("cancelled")
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def create_job(query)
|
50
|
+
resp = @api.post(
|
51
|
+
path: "/servicesNS/#{@user}/search/search/jobs",
|
52
|
+
expects: 201,
|
53
|
+
body: URI.encode_www_form({
|
54
|
+
output_mode: "json",
|
55
|
+
search: "search #{query}"
|
56
|
+
})
|
57
|
+
)
|
58
|
+
@job_id = JSON.parse(resp.body)["sid"]
|
59
|
+
debug "job: #{@job_id}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def debug(str)
|
63
|
+
if @verbose
|
64
|
+
puts str
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def finalize_job
|
69
|
+
@api.post(
|
70
|
+
path: "/servicesNS/#{@user}/search/search/jobs/#{@job_id}/control",
|
71
|
+
expects: 200,
|
72
|
+
body: URI.encode_www_form({
|
73
|
+
action: "finalize"
|
74
|
+
})
|
75
|
+
)
|
76
|
+
debug("finalized")
|
77
|
+
end
|
78
|
+
|
79
|
+
def get_job_results
|
80
|
+
# get results as CSV because the JSON version just mixes everything together
|
81
|
+
# into a giant difficult-to-use blob
|
82
|
+
resp = @api.get(
|
83
|
+
path: "/servicesNS/#{@user}/search/search/jobs/#{@job_id}/results",
|
84
|
+
# 204 if no results available
|
85
|
+
expects: [200, 204],
|
86
|
+
body: URI.encode_www_form({
|
87
|
+
action: "finalize",
|
88
|
+
output_mode: "csv"
|
89
|
+
})
|
90
|
+
)
|
91
|
+
|
92
|
+
return [] if resp.status == 204
|
93
|
+
|
94
|
+
rows = CSV.parse(resp.body)
|
95
|
+
return [] if rows.count < 1
|
96
|
+
field = rows[0].index("_raw") || raise("no _raw field detected in Splunk response")
|
97
|
+
|
98
|
+
# skip the first line as its used for CSV headers
|
99
|
+
rows[1..-1].
|
100
|
+
map { |l| l[field] }.
|
101
|
+
# 2014-08-15T19:01:15.476590+00:00 54.197.117.24 local0.notice
|
102
|
+
# api-web-1[23399]: - api.108080@heroku.com ...
|
103
|
+
map { |l| l.gsub(/^.*: - /, "") }.
|
104
|
+
map { |l| l.strip }.
|
105
|
+
# results come in from newest to oldest; flip that
|
106
|
+
reverse
|
107
|
+
end
|
108
|
+
|
109
|
+
def job_finished?
|
110
|
+
resp = @api.get(
|
111
|
+
path: "/servicesNS/#{@user}/search/search/jobs/#{@job_id}",
|
112
|
+
expects: 200,
|
113
|
+
body: URI.encode_www_form({
|
114
|
+
output_mode: "json"
|
115
|
+
})
|
116
|
+
)
|
117
|
+
# Splunk may not be winning any awards for cleanest API anytime soon
|
118
|
+
state = JSON.parse(resp.body)["entry"][0]["content"]["dispatchState"]
|
119
|
+
debug("state: #{state}")
|
120
|
+
state == "DONE"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/hutils/ltap.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Hutils
|
2
|
+
class NodeNavigator
|
3
|
+
def next_node(node, ignore_expanded: false)
|
4
|
+
# if expanded and we have children, move onto the first
|
5
|
+
if !ignore_expanded && node.tags[:expanded] && node.slots.count > 0
|
6
|
+
return node.slots[0]
|
7
|
+
end
|
8
|
+
|
9
|
+
index = node.parent.slots.index(node)
|
10
|
+
|
11
|
+
# Otherwise, if the node is the last in its slot, then move to the
|
12
|
+
# parent's next slot. If the parent is root, then we can go no further.
|
13
|
+
if index == node.parent.slots.count - 1
|
14
|
+
if node.parent.parent != nil
|
15
|
+
new_node = next_node(node.parent, ignore_expanded: true)
|
16
|
+
# if the sub-iteration couldn't find a next node, stay where we are
|
17
|
+
new_node != node.parent ? new_node : node
|
18
|
+
else
|
19
|
+
node
|
20
|
+
end
|
21
|
+
# otherwise, just move to the next slot
|
22
|
+
else
|
23
|
+
node.parent.slots[index + 1]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def prev_node(node)
|
28
|
+
index = node.parent.slots.index(node)
|
29
|
+
|
30
|
+
if index == 0
|
31
|
+
if node.parent.parent != nil
|
32
|
+
node.parent
|
33
|
+
else
|
34
|
+
# don't ever move up to root
|
35
|
+
return node
|
36
|
+
end
|
37
|
+
# otherwise, move to the previous node in the list
|
38
|
+
else
|
39
|
+
new_node = node.parent.slots[index - 1]
|
40
|
+
|
41
|
+
# But wait! We don't just want to move to the previous node directly, we
|
42
|
+
# actually want to move to the last child of its deepest expanded
|
43
|
+
# subnode.
|
44
|
+
while new_node.tags[:expanded] && new_node.slots.count > 0
|
45
|
+
new_node = new_node.slots.last
|
46
|
+
end
|
47
|
+
|
48
|
+
new_node
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
require "term/ansicolor"
|
2
|
+
|
3
|
+
module Hutils
|
4
|
+
class TextVisualizer
|
5
|
+
include Term::ANSIColor
|
6
|
+
|
7
|
+
def initialize(colors:, compact:, highlights:, root:, out:)
|
8
|
+
@colors = colors
|
9
|
+
@compact = compact
|
10
|
+
@highlights = highlights
|
11
|
+
@out = out
|
12
|
+
@root = root
|
13
|
+
end
|
14
|
+
|
15
|
+
def display
|
16
|
+
display_node(@root)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def colorize(method, str)
|
22
|
+
if @colors
|
23
|
+
send(method, str)
|
24
|
+
else
|
25
|
+
str
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def display_node(node)
|
30
|
+
if !node.common.empty?
|
31
|
+
# the "- 1" is because the root node is empty
|
32
|
+
indent = "\t" * (node.depth - 1)
|
33
|
+
node.common.to_a.sort_by { |k, v| k }.map { |k, v|
|
34
|
+
pair_to_string(k, v)
|
35
|
+
}.each_with_index { |display, i|
|
36
|
+
if @compact
|
37
|
+
@out.print(indent) if i == 0
|
38
|
+
@out.print("#{display} ")
|
39
|
+
else
|
40
|
+
marker = i == 0 ? "+ " : " "
|
41
|
+
@out.puts "#{indent}#{marker}#{display}"
|
42
|
+
end
|
43
|
+
}
|
44
|
+
@out.puts ""
|
45
|
+
end
|
46
|
+
node.slots.each { |slot| display_node(slot) }
|
47
|
+
end
|
48
|
+
|
49
|
+
def pair_to_string(k, v)
|
50
|
+
if v == true
|
51
|
+
colorize(:green, k)
|
52
|
+
else
|
53
|
+
if @highlights.include?(k)
|
54
|
+
colorize(:on_yellow, colorize(:black, "#{k}: #{v}"))
|
55
|
+
else
|
56
|
+
"#{colorize(:green, k)}: #{v}"
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/hutils.rb
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
require_relative "hutils/stripper"
|
2
|
+
|
3
|
+
module Hutils
|
4
|
+
class Node
|
5
|
+
def initialize(parent, common)
|
6
|
+
@common = common
|
7
|
+
@parent = parent
|
8
|
+
@slots = []
|
9
|
+
@tags = {}
|
10
|
+
end
|
11
|
+
|
12
|
+
# The set of common attributes that are shared by all slots in this branch of
|
13
|
+
# the tree.
|
14
|
+
attr_accessor :common
|
15
|
+
|
16
|
+
# Parent node.
|
17
|
+
attr_accessor :parent
|
18
|
+
|
19
|
+
# Ordered child nodes of this node.
|
20
|
+
attr_accessor :slots
|
21
|
+
|
22
|
+
# Arbitrary tags that applications can associate on a node.
|
23
|
+
attr_accessor :tags
|
24
|
+
|
25
|
+
# The set of common attributes that are shared by all slots in this branch of
|
26
|
+
# the tree, but accounting for all parent nodes as well.
|
27
|
+
def common_complete
|
28
|
+
@common.merge(@parent ? @parent.common_complete : {})
|
29
|
+
end
|
30
|
+
|
31
|
+
def depth
|
32
|
+
@parent ? @parent.depth + 1 : 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def print
|
36
|
+
indent = " " * depth
|
37
|
+
$stdout.puts "#{indent}[ #{depth} ]#{@common}"
|
38
|
+
slots.each { |node| node.print }
|
39
|
+
end
|
40
|
+
|
41
|
+
def replace_slot(old, new)
|
42
|
+
@slots.each_with_index do |node, i|
|
43
|
+
if old == node
|
44
|
+
@slots[i] = new
|
45
|
+
return
|
46
|
+
end
|
47
|
+
end
|
48
|
+
abort("bad replace")
|
49
|
+
end
|
50
|
+
|
51
|
+
def root
|
52
|
+
@parent ? @parent.root : self
|
53
|
+
end
|
54
|
+
|
55
|
+
def root?
|
56
|
+
@parent == nil
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class Parser
|
61
|
+
def initialize(str)
|
62
|
+
@str = str
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse
|
66
|
+
lines = @str.split("\n").map { |line| normalize(line) }
|
67
|
+
lines.map! do |line|
|
68
|
+
pairs = line.scan(/(?:['"](?:\\.|[^'"])*['"]|[^'" ])+/).map do |pair|
|
69
|
+
key, value = pair.split("=")
|
70
|
+
[key, value].each do |str|
|
71
|
+
str.gsub!(/^['"]?(.*?)['"]?$/, '\1') if str
|
72
|
+
end
|
73
|
+
[key, value || true]
|
74
|
+
end
|
75
|
+
Hash[pairs]
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def normalize(line)
|
82
|
+
line = line.strip
|
83
|
+
line.gsub(/^[T0-9\-:+.]+( [a-z]+\[[a-z0-9\-_.]+\])?: /, '')
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
class TreeBuilder
|
88
|
+
def initialize(lines)
|
89
|
+
@lines = lines
|
90
|
+
end
|
91
|
+
|
92
|
+
def build
|
93
|
+
root = Node.new(nil, {})
|
94
|
+
node = root
|
95
|
+
@lines.each do |pairs|
|
96
|
+
node = build_node(node, pairs)
|
97
|
+
end
|
98
|
+
root
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
def diff(hash1, hash2)
|
104
|
+
same = hash1.dup.delete_if { |k, v| hash2[k] != v }
|
105
|
+
|
106
|
+
extra1 = hash1.dup
|
107
|
+
same.each { |k, _| extra1.delete(k) }
|
108
|
+
|
109
|
+
extra2 = hash2.dup
|
110
|
+
same.each { |k, _| extra2.delete(k) }
|
111
|
+
|
112
|
+
[same, extra1, extra2]
|
113
|
+
end
|
114
|
+
|
115
|
+
def debug(title, data, node)
|
116
|
+
return unless ENV["DEBUG"] == "true"
|
117
|
+
puts "---"
|
118
|
+
puts title
|
119
|
+
data.each { |k, v| puts "#{k}: #{v}" }
|
120
|
+
node.root.print if node
|
121
|
+
puts ""
|
122
|
+
end
|
123
|
+
|
124
|
+
def build_node(node, pairs)
|
125
|
+
complete = node.common_complete
|
126
|
+
same, complete_extra, pairs_extra = diff(complete, pairs)
|
127
|
+
|
128
|
+
if complete == same
|
129
|
+
if pairs_extra.empty?
|
130
|
+
# we've hit a rare case of a line which is a duplicate of its immediate
|
131
|
+
# successor: do nothing
|
132
|
+
else
|
133
|
+
# all common is shared but we have extra: add a new child node
|
134
|
+
new = Node.new(node, pairs_extra)
|
135
|
+
node.slots << new
|
136
|
+
node = new
|
137
|
+
debug("simple leaf addition", { pairs_extra: pairs_extra }, node)
|
138
|
+
end
|
139
|
+
else
|
140
|
+
# First of all, determine whether our node has any data in common with
|
141
|
+
# the current node.
|
142
|
+
local_same, _, _ = diff(node.common, pairs)
|
143
|
+
|
144
|
+
# Then figure out that given a split, what the old node's new set would
|
145
|
+
# look like in the context of a shared common parent.
|
146
|
+
_, other_extra, _ = diff(node.common, local_same)
|
147
|
+
|
148
|
+
# And finally, determine whether the parent hierarchy contains any data
|
149
|
+
# that is incompatible with our new node because it's not shared.
|
150
|
+
_, illegal_extra, _ = diff(complete_extra, other_extra)
|
151
|
+
|
152
|
+
if !local_same.empty? && illegal_extra.empty?
|
153
|
+
# create a replacement node and swap it into place
|
154
|
+
new_parent = Node.new(node.parent, local_same)
|
155
|
+
node.common = other_extra
|
156
|
+
node.parent = new_parent
|
157
|
+
new_parent.slots << node
|
158
|
+
new_parent.parent.replace_slot(node, new_parent)
|
159
|
+
|
160
|
+
new = Node.new(new_parent, pairs_extra)
|
161
|
+
new_parent.slots << new
|
162
|
+
node = new
|
163
|
+
debug("split", { pairs_extra: pairs_extra }, node)
|
164
|
+
else
|
165
|
+
debug("before tree ascention", { pairs: pairs }, nil)
|
166
|
+
# Nothing is shared, ascend the tree until something is. Eventually
|
167
|
+
# something will be because the root node contains an empty hash of common
|
168
|
+
# attributes.
|
169
|
+
node = build_node(node.parent, pairs)
|
170
|
+
debug("after tree ascention", { pairs: pairs }, nil)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
node
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
metadata
ADDED
@@ -0,0 +1,120 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hutils
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brandur
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-08-22 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: excon
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.39'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 0.39.5
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0.39'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 0.39.5
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: inifile
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '3.0'
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 3.0.0
|
43
|
+
type: :runtime
|
44
|
+
prerelease: false
|
45
|
+
version_requirements: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '3.0'
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: 3.0.0
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: term-ansicolor
|
55
|
+
requirement: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '1.3'
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: 1.3.0
|
63
|
+
type: :runtime
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '1.3'
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
version: 1.3.0
|
73
|
+
description:
|
74
|
+
email: brandur@mutelight.org
|
75
|
+
executables:
|
76
|
+
- lcut
|
77
|
+
- ltap
|
78
|
+
- lviz
|
79
|
+
extensions: []
|
80
|
+
extra_rdoc_files: []
|
81
|
+
files:
|
82
|
+
- "./lib/hutils.rb"
|
83
|
+
- "./lib/hutils/curses_visualizer.rb"
|
84
|
+
- "./lib/hutils/ltap.rb"
|
85
|
+
- "./lib/hutils/ltap/conf.rb"
|
86
|
+
- "./lib/hutils/ltap/paper_trail_drainer.rb"
|
87
|
+
- "./lib/hutils/ltap/splunk_drainer.rb"
|
88
|
+
- "./lib/hutils/node_navigator.rb"
|
89
|
+
- "./lib/hutils/stripper.rb"
|
90
|
+
- "./lib/hutils/text_visualizer.rb"
|
91
|
+
- README.md
|
92
|
+
- bin/lcut
|
93
|
+
- bin/ltap
|
94
|
+
- bin/lviz
|
95
|
+
homepage: https://github.com/brandur/hutils
|
96
|
+
licenses:
|
97
|
+
- MIT
|
98
|
+
metadata: {}
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - ">="
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
requirements: []
|
114
|
+
rubyforge_project:
|
115
|
+
rubygems_version: 2.2.2
|
116
|
+
signing_key:
|
117
|
+
specification_version: 4
|
118
|
+
summary: A collection of command line utilities for working with logfmt.
|
119
|
+
test_files: []
|
120
|
+
has_rdoc:
|