hutils 0.0.1
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.
- 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:
|