Skip to content

Instantly share code, notes, and snippets.

@karmi
Last active August 24, 2020 09:25
Show Gist options
  • Star 22 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save karmi/5670573 to your computer and use it in GitHub Desktop.
Save karmi/5670573 to your computer and use it in GitHub Desktop.
Elasticat makes Elasticsearch JSON responses pretty • http://git.io/elasticat
.DS_Store
tmp/

Elasticat will turn a regular Elasticsearch JSON response into something like this:

Elasticat Screenshot

Requirements

Elasticat requires Ruby 1.9 or higher and the ansi Rubygem:

ruby -v
gem install ansi

Installation

curl -o elasticat https://gist.github.com/karmi/5670573/raw/elasticat
chmod +x elasticat

You might want to put the command into a directory on your PATH, such as ~/bin or /usr/local/bin.

Nothing stops you from naming the command whatever you want:

mv elasticat cherryblossom

Usage

Simply pipe a curl command to elasticat:

curl localhost:9200/_search | ./elasticat

or use it instead of curl:

./elasticat localhost:9200/_cluster/state

MIT Licensed • http://git.io/elasticat

#!/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)
@OpakAlex
Copy link

OpakAlex commented Jun 2, 2013

Great tool!

@felixbuenemann
Copy link

@karmi I've added support for range and histogram facets: https://gist.github.com/felixbuenemann/6422175/revisions

@costin
Copy link

costin commented Sep 5, 2013

For those using Windows cmd:

  1. use a proper ANSI font like Lucida Console (not Raster or Consolas)
  2. change the code page to unicode: chcp 65001 (run first chcp w/o any arguments to see the current code page). Note this setting applies only to the current window

This should improve the output significantly (tested on Win 7 x64 SP1)

@VBourdine
Copy link

Awesome tool! Thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment