|
#!/usr/bin/env ruby |
|
# encoding: utf-8 |
|
|
|
STDOUT.sync = true |
|
|
|
require 'time' |
|
require 'optparse' |
|
require 'ansi/mixin' |
|
require 'ansi/table' |
|
require 'json' |
|
|
|
class String |
|
include ANSI::Mixin |
|
end |
|
|
|
class Integer |
|
include ANSI::Mixin |
|
def ansi(*args) |
|
self.to_s.ansi(*args) |
|
end |
|
end |
|
|
|
$options = {} |
|
# OptionParser.new do |opts| |
|
# opts.banner = "Usage: elasticat [options]" |
|
|
|
# opts.on("-i", "--interval INTERVAL", "Set interval for date histogram facets") do |i| |
|
# $options[:interval] = i |
|
# end |
|
# end.parse! |
|
|
|
module Elasticat |
|
def decide(name, json, &block) |
|
puts "[!] ERROR: No handler for '#{name}' found".ansi(:red) unless respond_to?(name) |
|
self.send name, json if self.instance_eval(&block) |
|
end |
|
|
|
module Helpers |
|
def table(data, options={}, &format) |
|
ANSI::Table.new(data, options, &format) |
|
end |
|
def width |
|
Integer(ENV['COLUMNS'] || 80) |
|
end |
|
def humanize(string) |
|
string.to_s.gsub(/\_/, ' ').split.map { |s| s.capitalize}.join(' ') |
|
end |
|
def date(date, interval='day') |
|
case interval |
|
when 'minute' |
|
date.strftime('%a %m/%d %H:%M') + ' – ' + (date+60).strftime('%H:%M') |
|
when 'hour' |
|
date.strftime('%a %m/%d %H:%M') + ' – ' + (date+60*60).strftime('%H:%M') |
|
when 'day' |
|
date.strftime('%a %m/%d') |
|
when 'week' |
|
days_to_monday = date.wday!=0 ? date.wday-1 : 6 |
|
days_to_sunday = date.wday!=0 ? 7-date.wday : 0 |
|
start = (date - days_to_monday*24*60*60).strftime('%a %m/%d') |
|
stop = (date+(days_to_sunday*24*60*60)).strftime('%a %m/%d') |
|
"#{start} – #{stop}" |
|
when 'month' |
|
date.strftime('%B %Y') |
|
when 'year' |
|
date.strftime('%Y') |
|
else |
|
date.strftime('%Y-%m-%d %H:%M') |
|
end |
|
end |
|
def ___ |
|
('─'*Elasticat::Helpers.width).ansi(:faint) |
|
end |
|
extend self |
|
end |
|
|
|
module Actions |
|
include Helpers |
|
|
|
def display_allocation_on_nodes(json) |
|
json['routing_nodes']['nodes'].each do |id, shards| |
|
puts (json['nodes'][id]['name'] || id).to_s.ansi(:bold) + " [#{id}]".ansi(:faint) |
|
if shards.empty? |
|
puts "No shards".ansi(:cyan) |
|
else |
|
puts table(shards.map do |shard| |
|
[ |
|
shard['index'], |
|
shard['shard'].ansi( shard['primary'] ? :bold : :clear ), |
|
shard['primary'] ? '◼'.ansi(:green) : '◻'.ansi(:yellow) |
|
] |
|
end) |
|
end |
|
end |
|
|
|
unless json['routing_nodes']['unassigned'].empty? |
|
puts 'Unassigned: '.ansi(:faint, :yellow) + "#{json['routing_nodes']['unassigned'].size} shards" |
|
puts table( json['routing_nodes']['unassigned'].map do |shard| |
|
primary = shard['primary'] |
|
|
|
[ |
|
shard['index'], |
|
shard['shard'].ansi( primary ? :bold : :clear ), |
|
primary ? '◼'.ansi(:red) : '◻'.ansi(:yellow) |
|
] |
|
end, border: false) |
|
end |
|
end |
|
|
|
def display_hits(json) |
|
hits = json['hits']['hits'] |
|
source = json['hits']['hits'].any? { |h| h['fields'] } ? 'fields' : '_source' |
|
properties = hits.map { |h| h[source] ? h[source].keys : nil }.compact.flatten.uniq |
|
max_property_length = properties.map { |d| d.to_s.size }.compact.max.to_i + 1 |
|
|
|
hits.each_with_index do |hit, i| |
|
title = hit[source] && hit[source].select { |k, v| ['title', 'name'].include?(k) }.to_a.first |
|
size_length = hits.size.to_s.size+2 |
|
padding = size_length |
|
|
|
puts "#{i+1}. ".rjust(size_length).ansi(:faint) + |
|
" <#{hit['_id']}> " + |
|
(title ? title.last.to_s.ansi(:bold) : ''), |
|
___ |
|
|
|
['_score', '_index', '_type'].each do |property| |
|
puts ' '*padding + "#{property}: ".rjust(max_property_length+1).ansi(:faint) + hit[property].to_s if hit[property] |
|
end |
|
|
|
hit[source].each do |property, value| |
|
puts ' '*padding + "#{property}: ".rjust(max_property_length+1).ansi(:faint) + value.to_s |
|
end if hit[source] |
|
|
|
# Highlight |
|
if hit['highlight'] |
|
puts "", ' '*(padding+max_property_length+1) + "Highlights".ansi(:faint), |
|
' '*(padding+max_property_length+1) + ('─'*10).ansi(:faint) |
|
|
|
hit['highlight'].each do |property, matches| |
|
print ' '*padding + "#{property}: ".rjust(max_property_length+1).ansi(:faint) |
|
matches.each_with_index do |match, i| |
|
print ' '*padding + ''.rjust(max_property_length+1) if i > 0 |
|
puts match.ansi(:faint).gsub(/\n/, ' ') |
|
.gsub(/<em>(.+)<\/em>/, '\1'.ansi(:clear, :bold)) |
|
.ansi(:faint) |
|
end |
|
end |
|
end |
|
|
|
puts "" |
|
end |
|
puts ___, "#{hits.size.to_s.ansi(:bold)} of #{json['hits']['total'].to_s.ansi(:bold)} results".ansi(:faint) |
|
end |
|
|
|
def display_terms_facets(json) |
|
facets = json['facets'].select { |name, values| values['_type'] == 'terms' } |
|
|
|
facets.each do |name, values| |
|
longest = values['terms'].map { |t| t['term'].size }.max |
|
max = values['terms'].map { |t| t['count'] }.max |
|
padding = longest.to_i + max.to_s.size + 5 |
|
ratio = ((width)-padding)/max.to_f |
|
|
|
puts "", "#{'Facet: '.ansi(:faint)}#{humanize(name)}", ___ |
|
values['terms'].each_with_index do |value, i| |
|
puts value['term'].ljust(longest).ansi(:bold) + |
|
" [" + value['count'].to_s.rjust(max.to_s.size) + "] " + |
|
" " + '█' * (value['count']*ratio).ceil |
|
end |
|
end |
|
end |
|
|
|
def display_date_histogram_facets(json) |
|
facets = json['facets'].select { |name, values| values['_type'] == 'date_histogram' } |
|
facets.each do |name, values| |
|
max = values['entries'].map { |t| t['count'] }.max |
|
padding = 27 |
|
ratio = ((width)-padding)/max.to_f |
|
|
|
interval = $options[:interval] |
|
puts "", "#{'Facet: '.ansi(:faint)}#{humanize(name)} #{interval ? ('(by ' + interval + ')').ansi(:faint) : ''}", ___ |
|
values['entries'].each_with_index do |value, i| |
|
puts date(Time.at(value['time'].to_i/1000).utc, interval).rjust(21).ansi(:bold) + |
|
" [" + value['count'].to_s.rjust(max.to_s.size) + "] " + |
|
" " + '█' * (value['count']*ratio).ceil |
|
end |
|
end |
|
end |
|
|
|
def display_analyze_output(json) |
|
max_length = json['tokens'].map { |d| d['token'].to_s.size }.max |
|
|
|
puts table(json['tokens'].map do |t| |
|
[ |
|
t['position'], |
|
t['token'].ljust(max_length+5).ansi(:bold), |
|
"#{t['start_offset']}–#{t['end_offset']}", |
|
t['type'] |
|
] |
|
end) |
|
end |
|
|
|
end |
|
end |
|
|
|
unless ARGV.empty? |
|
# Running as `elasticurl` |
|
args = ARGV.map { |d| d =~ /(^http)|(^{)/ ? d = "'#{d}'" : d } |
|
command = "curl #{args.join(' ')}" |
|
input = `#{command}` |
|
else |
|
# Running as piped `elasticat` |
|
input = STDIN.read |
|
end |
|
|
|
begin |
|
json = JSON.load(input) |
|
rescue JSON::ParserError |
|
puts "[!] ERROR: Invalid json received".ansi(:red), |
|
input.ansi(:faint) |
|
exit(1) |
|
end |
|
|
|
___ = ('─'*Elasticat::Helpers.width).ansi(:faint) |
|
|
|
puts input.to_s.ansi(:faint), "" |
|
|
|
include Elasticat, Elasticat::Actions |
|
|
|
decide :display_allocation_on_nodes, json do |
|
json['routing_nodes'] && !json['routing_nodes'].empty? |
|
end |
|
|
|
decide :display_hits, json do |
|
json['hits'] && json['hits']['hits'] && !json['hits']['hits'].empty? |
|
end |
|
|
|
decide :display_terms_facets, json do |
|
json['facets'] && json['facets'].any? { |name, values| values['_type'] == 'terms' } |
|
end |
|
|
|
decide :display_date_histogram_facets, json do |
|
json['facets'] && json['facets'].any? { |name, values| values['_type'] == 'date_histogram' } |
|
end |
|
|
|
decide :display_analyze_output, json do |
|
json['tokens'].is_a?(Array) |
|
end |
|
|
|
puts ('▂'*Elasticat::Helpers.width).ansi(:faint) |