Skip to content

Instantly share code, notes, and snippets.

@sunjon
Last active February 14, 2023 19:13
Show Gist options
  • Save sunjon/b375f0bdc27824ce5ef010982f9a6cda to your computer and use it in GitHub Desktop.
Save sunjon/b375f0bdc27824ce5ef010982f9a6cda to your computer and use it in GitHub Desktop.
Elasticsearch viewer based on FZF
#!/usr/bin/env bash
# Senghan Bright
# Delta Projects
# TODO: read predefined searches from a .INI file to make management easier for people?
# or sync directly with what already exists in Kibana
declare -A el_predefined_searches
el_predefined_searches=(
# [bidrequests]=''
['delta-java-app']=''
# [delta-metrics]=''
['filebeat']=''
['filebeat-*:airflow']='beat.hostname: *airflow* AND source: *airflow*'
['filebeat-*:airflow_slimer']='beat.hostname: *airflow* AND source:*slimer_batch_*_dag*'
['flink']=''
['fluentd-*:dogfight']='host:dogfight* AND source:dogfight'
['fluentd-*:haproxy']='host:dflb-ssl* AND ident:haproxy'
['fluentd-*:mailgw_postfix']='host:mailgw* AND ident:postfix*'
['fluentd-*:trackbox']='host:trackbox* AND source:trackbox-logs'
# [googlebidrequests]=''
# [logstash]=''
['metricbeat']=''
['rancher-default']=''
['rancher-local']=''
)
declare -a fzf_options
# fzf_options=('--layout=default' '--inline-info' '--no-sort' '--no-clear' '--ansi' '--no-border' '--preview-window down:1')
fzf_options=('--layout=default' '--inline-info' '--no-sort' '--no-clear' '--ansi' '--no-border')
# these bindings are passive and do not alter the menu state
script_dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
declare -A fzf_navigation_keys
fzf_navigation_keys=(
[page-down]='ctrl-f'
[page-up]='ctrl-b'
[top]='ctrl-t'
[kill-line]='ctrl-k'
[toggle-preview]='?'
)
# ["execute-silent(source $script_dir/toggle_elktail.sh; toggle_elktail)"]='ctrl-p'
# these bindings will exit the menu and perform an action
# user can set environment variables to override default bindings
# (or values read from a .INI file will override bindings)
declare -A fzf_postmenuaction_keys
fzf_postmenuaction_keys=(
['change_source']="${TBANA_SOURCE_KEY:-tab}"
['navigate_back']="${TBANA_BACK_KEY:-esc}"
['select_date']="${TBANA_DATE_KEY:-f1}"
)
declare -a HOSTS_CACHE
declare -a MENU_SOURCES=('HOSTS_CACHE' 'EL_SEARCHES')
ACTIVE_MENU=0
elktail_bin=/usr/local/bin/elktail
elktail_initial_entries=5000
el_host='elasticsearch.service.consul:9200'
el_log_format='%@timestamp %host %level %container_name%ident %message%short_message%log'
COLOR_RED='\x1b[38;5;1m'
COLOR_GREEN='\x1b[38;5;6m'
COLOR_YELLOW='\x1b[38;5;2m'
COLOR_GREY='\x1b[38;5;8m'
COLOR_RESET='\x1b[0m'
# Usage: cut "string" "delimiter" "field"
cut() {
local string=$1
local delimiter=$2
local field=$3
local result
IFS=$delimiter read -r -a result <<< "$string"
printf '%s' "${result[field]}"
}
# Usage: regex "string" "regex"
regex() {
[[ $1 =~ $2 ]] && printf '%s\n' "${BASH_REMATCH[1]}"
}
cycle_menu() {
local source_total
source_total=$(( ${#MENU_SOURCES[@]} - 1 ))
((ACTIVE_MENU++))
if [[ $ACTIVE_MENU -gt $source_total ]]; then
ACTIVE_MENU=0
fi
}
generate_help_text() {
local help_text=''
IFS=$','
for binding in $fzf_binds; do
key=$(cut "$binding" ':' 0)
action=$(cut "$binding" ':' 1)
help_text+="\o033[32m$key\o033[39m$action "
done
unset IFS
echo "$help_text"
}
generate_fzf_binds() {
local -n array_reference=$1
local key
local fzf_bindings=''
for action in "${!array_reference[@]}"; do
key=${array_reference[$action]}
if [[ $1 =~ ^fzf_navigation* ]]; then
fzf_bindings+="$key:$action," # navigation
else
fzf_bindings+="$key:execute(echo $key)+accept," # post-menu action
fi
done
# remove trailing comma
echo "${fzf_bindings::-1}"
}
#
get_predefined_searches() {
local menu_entries=''
for array_key in "${!el_predefined_searches[@]}"; do
menu_entries+=$(format_el_query "$array_key")
done
echo -en "$menu_entries" | sort
}
# generates a list of dates from today -> ((today - 365 days))
generate_dates() {
local start_date=$1
local generated_date
local date_list
local date_suffix
local count
count=0
while [ $count -lt 363 ]; do
generated_date=$(date +"%d-%b-%Y" -d "$start_date -$count days" )
date_list+="$generated_date\n"
# optimisation: Generate dates we know each month has without using `date`
if [[ $generated_date == '28-'* ]]; then
date_suffix=${generated_date:2:9}
for d in {27..1}; do
date_list+="${d}${date_suffix}\n"
((count++))
[ $count -gt 363 ] && break
done
fi
((count++))
done
echo "$date_list"
}
# cache the output of `knife node list`
get_hosts() {
local cache=$1
local cache_file
local nodes
cache_file="$script_dir/.hosts_cache"
# is the $cache var empty?
if [[ -z $cache ]]; then
# check if there's a cache file on disk
if [[ $(find "$cache_file" -mmin -$(( 24 * 60 )) 2>/dev/null) ]]; then
cache=$(<"$cache_file")
else
nodes=$(knife node list)
# strip the domain
cache=${nodes//.se-ix.delta.prod/}
# store cache on disk
echo -n "$cache" > "$cache_file"
fi
fi
echo -n "${cache[*]}"
}
# Show menu of predefined queries or hosts
show_fzf_menu() {
local content=$1
# add any options sent as additional parameters
declare -a options
options=("${fzf_options[@]}")
if [ -n "${*:2}" ]; then
options+=("${@:2}")
fi
local bindings
bindings=$(generate_fzf_binds fzf_navigation_keys)
bindings+=",$(generate_fzf_binds fzf_postmenuaction_keys)"
# bindings+=',ctrl-o:execute(/usr/local/bin/dialog --print-text-only "hello world" 8 52; sleep 2)'
bindings+=',ctrl-o:execute(tput smcup; vim "~/.skhdrc")'
echo -en "$content" | fzf "${options[@]}" --bind "$bindings"
}
show_fzf_logview() {
local log=1
}
#
select_date() {
local date_list
local remaining_dates
local date_search_start
local date_search_end
local preview_title
preview_title="\\${COLOR_RED}search-range:\\${COLOR_RESET}"
date_list=$(generate_dates "$(date +%F)")
date_search_start=$(show_fzf_menu "$date_list")
# remove all dates before $date_search_start
remaining_dates=$(regex "$date_list" "(.*$date_search_start).*")
# display the list in reverse so dates close to $date_search_start are easy to select
date_search_end=$(show_fzf_menu "$remaining_dates" --tac)
}
format_el_query() {
local input=$1
local index_name
local query_pattern
if [[ $input == *":"* ]]; then
index_name=$(cut "$input" ':' 0)
query_pattern=$(cut "$input" ':' 1)
else
index_name=$input
query_pattern=''
fi
# index_doc_count=$(curl -s "http://elasticsearch.service.consul:9200/$index_name/_count" | jq '.count')
# apply formatting and color
echo "${COLOR_RED}[${COLOR_RESET}${index_name}${COLOR_RED}]${COLOR_RESET} ${COLOR_GREEN}${query_pattern}${COLOR_RESET}\n"
}
offset_date() {
local epoch_time=$1
local offset=$2
local offset_epoch
offset_epoch=$(("$epoch_time" + "$offset"))
# return date in human readable format
date -d @$offset_epoch '+%Y-%m-%dT%H:%M+02:00'
}
init() {
echo "Generating hosts cache..."
HOSTS_CACHE=$(get_hosts "$HOSTS_CACHE")
# TODO: also cache our highlighted predefined searches (just as a var)
EL_SEARCHES=$(get_predefined_searches)
clear
}
main() {
local menu_content
local selected_line
local key_pressed
menu_content=${MENU_SOURCES[$ACTIVE_MENU]}
fzf_output=$(show_fzf_menu "${!menu_content}")
# split output
key_pressed=${fzf_output%%$'\n'*}
selected_line=${fzf_output#*$'\n'}
# Handle action keys
case $key_pressed in
${TBANA_BACK_KEY:-esc})
echo "OHMAN"
exit
;;
${TBANA_SOURCE_KEY:-tab})
cycle_menu
main
;;
${TBANA_DATE_KEY:-f1})
select_date
echo "OOT"
exit
;;
esac
# do we really have a choice?
if [ -n "$selected_line" ]; then
local el_index_pattern
local el_query
local el_query_pattern
case ${MENU_SOURCES[ACTIVE_MENU]} in
HOSTS_CACHE)
el_index_pattern='fluentd-[0-9]'
el_query="host:$selected_line"
;;
EL_SEARCHES)
el_index_pattern=$(cut "$selected_line" ' ' 0)
el_index_pattern=$(regex "$el_index_pattern" '\[(.*)\]') # remove braces
el_query_pattern=$(cut "$selected_line" ' ' 1)
if [[ -n $el_query_pattern ]]; then
el_query=${el_predefined_searches[${el_index_pattern}:${el_query_pattern}]}
else
el_query='*'
fi
;;
esac
edit_key=${FZF_CTRL_R_EDIT_KEY:-ctrl-space}
exec_key=${FZF_CTRL_R_EXEC_KEY:-ctrl-x}
back_key=${FZF_CTRL_R_BACK_KEY:-esc}
logentry_selection=$(
# TODO: improve this.
# highlight date-time column, application and message(message can be colored for warn/error)
$elktail_bin -n $elktail_initial_entries -url $el_host -f "$el_log_format" -i "$el_index_pattern" \
"$el_query" | sed \
-e "s/^\([0-9\-]*T[0-9:\.\+Z]*\)/$COLOR_GREY\1$COLOR_RESET/" \
-e "s/\b\(ERROR\)\b/$COLOR_RED\1$COLOR_RESET/" \
-e "s/\b\(WARN\)\b/$COLOR_YELLOW\1$COLOR_RESET/" |
$fzf_command --header "$el_index_pattern::$el_query" --ansi \
--preview 'echo :: {3..}' --tac --preview-window down:1:hidden \
--bind "$(generate_fzf_binds fzf_navigation_keys),$back_key:execute(kill -INT $(pgrep elktail) 2>/dev/null; echo $back_key)+accept" \
--expect "$edit_key","$exec_key")
key_pressed=${logentry_selection%%$'\n'*}
selection=${logentry_selection#*$'\n'}
select_source_key=${FZF_CTRL_R_BACK_KEY:-tab}
case $key_pressed in
$edit_key) result=$(echo "edit");;
$exec_key) result=$(echo "exec");;
$select_source_key) echo "BOO" && exit 0;;
$back_key) main;;
esac
exit 0
local entry_time
local entry_time_epoch
entry_time=$(cut "$log_selection" ' ' 0)
# convert to epoch time
entry_time_epoch=$(date -d "$entry_time" +%s)
# NOTE: elktail ignores "+02:00" time specifier?
# offset the time manually
local log_begin
local log_end
log_begin=$(offset_date "$entry_time_epoch" -300) # 5mins before
log_end=$(offset_date "$entry_time_epoch" 300) # 5mins after
# TODO: make the context log default to showing the selected hoost
$elktail_bin -url $el_host -a "$log_begin" -b "$log_end" -f "$el_log_format" -n 10000 -i "$el_index_pattern" "$el_query" | less --tabs=4 --no-init --LONG-PROMPT --ignore-case --quit-if-one-screen --RAW-CONTROL-CHARS
fi
}
# MAIN
init
main
exit 0
#!/bin/sh
# Toggle the running state of the elktail process
toggle_elktail() {
elktail_pid=$(pgrep elktail)
elktail_status=$(ps -o state -p "$elktail_pid")
case "$elktail_status" in
*T+* ) process_signal='-CONT' ;;
*S+* ) process_signal='-STOP' ;;
* ) exit ;;
esac
kill $process_signal "$elktail_pid"
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment