File Coverage

blib/lib/App/ElasticSearch/Utilities.pm
Criterion Covered Total %
statement 148 507 29.1
branch 62 354 17.5
condition 3 127 2.3
subroutine 29 62 46.7
pod 32 32 100.0
total 274 1082 25.3


line stmt bran cond sub pod time code
1             # ABSTRACT: Utilities for Monitoring ElasticSearch
2             package App::ElasticSearch::Utilities;
3              
4 4     4   1347 use v5.16;
  4         27  
5 4     4   24 use warnings;
  4         8  
  4         178  
6              
7             our $VERSION = '8.7'; # VERSION
8              
9 4     4   1731 use App::ElasticSearch::Utilities::HTTPRequest;
  4         15  
  4         141  
10 4     4   2255 use CLI::Helpers qw(:all);
  4         376425  
  4         29  
11 4     4   874 use Getopt::Long qw(GetOptionsFromArray :config pass_through no_auto_abbrev);
  4         11  
  4         37  
12 4     4   2776 use Hash::Flatten qw(flatten);
  4         9852  
  4         250  
13 4     4   1831 use Hash::Merge::Simple qw(clone_merge);
  4         1968  
  4         233  
14 4     4   2196 use IPC::Run3;
  4         13317  
  4         229  
15 4     4   36 use JSON::MaybeXS;
  4         9  
  4         244  
16 4     4   2950 use LWP::UserAgent;
  4         121470  
  4         158  
17 4     4   2068 use Net::Netrc;
  4         17990  
  4         162  
18 4     4   59 use Ref::Util qw(is_ref is_arrayref is_hashref);
  4         10  
  4         280  
19 4     4   34 use Time::Local;
  4         9  
  4         233  
20 4     4   31 use URI;
  4         31  
  4         101  
21 4     4   1839 use URI::QueryParam;
  4         3271  
  4         139  
22 4     4   1794 use YAML::XS ();
  4         11759  
  4         445  
23              
24             # Control loading ARGV
25             my $ARGV_AT_INIT = 1;
26             my $COPY_ARGV = 0;
27             our $_init_complete = 0;
28              
29 4         74 use Sub::Exporter -setup => {
30             collectors => [
31             copy_argv => \'_copy_argv',
32             preprocess_argv => \'_preprocess_argv',
33             delay_argv => \'_delay_argv',
34             ],
35             exports => [ qw(
36             es_utils_initialize
37             es_globals
38             es_basic_auth
39             es_pattern
40             es_connect
41             es_master
42             es_request
43             es_nodes
44             es_indices
45             es_indices_meta
46             es_index_valid
47             es_index_bases
48             es_index_strip_date
49             es_index_days_old
50             es_index_shards
51             es_index_segments
52             es_index_stats
53             es_index_fields
54             es_settings
55             es_node_stats
56             es_segment_stats
57             es_close_index
58             es_open_index
59             es_delete_index
60             es_optimize_index
61             es_apply_index_settings
62             es_local_index_meta
63             es_flatten_hash
64             es_human_count
65             es_human_size
66             )],
67             groups => {
68             config => [qw(es_utils_initialize es_globals)],
69             default => [qw(es_utils_initialize es_connect es_indices es_request)],
70             human => [qw(es_human_count es_human_size)],
71             indices => [qw(:default es_indices_meta)],
72             index => [qw(:default es_index_valid es_index_fields es_index_days_old es_index_bases)],
73             },
74 4     4   2828 };
  4         45637  
75 4     4   6486 use App::ElasticSearch::Utilities::Connection;
  4         3716  
  4         234  
76 4     4   1876 use App::ElasticSearch::Utilities::VersionHacks qw(_fix_version_request);
  4         13  
  4         39  
77              
78             # Collectors
79 0     0   0 sub _copy_argv { $COPY_ARGV = 1 }
80 0     0   0 sub _preprocess_argv { $ARGV_AT_INIT = 1 }
81 0     0   0 sub _delay_argv { $ARGV_AT_INIT = 0 }
82              
83              
84             # Global Variables
85             our %_GLOBALS = ();
86             my %DEF = ();
87             my %PATTERN_REGEX = (
88             '*' => qr/.*/,
89             ANY => qr/.*/,
90             DATE => qr/
91             (?<datestr>
92             (?<year>\d{4}) # Extract 4 digits for the year
93             (?:(?<datesep>[\-.]))? # Optionally, look for . - as a separator
94             (?<month>\d{2}) # Two digits for the month
95             \g{datesep} # Whatever the date separator was in the previous match
96             (?<day>\d{2}) # Two digits for the day
97             (?![a-zA-Z0-9]) # Zero width negative look ahead, not alphanumeric
98             )
99             /x,
100             );
101             my $PATTERN;
102              
103             {
104             ## no critic (ProhibitNoWarnings)
105 4     4   1712 no warnings;
  4         11  
  4         19049  
106             INIT {
107 4 50   4   75 return if $_init_complete++;
108 4 50       26 es_utils_initialize() if $ARGV_AT_INIT;
109             }
110             ## use critic
111             }
112              
113              
114             {
115             # Argument Parsing Block
116             my @argv_original = ();
117             my $parsed_argv = 0;
118             sub _parse_options {
119 4     4   15 my ($opt_ref) = @_;
120 4         37 my @opt_spec = qw(
121             local
122             host=s
123             port=i
124             timeout=i
125             keep-proxy
126             index=s
127             pattern=s
128             base|index-basename=s
129             days=i
130             noop!
131             proto=s
132             http-username=s
133             password-exec=s
134             master-only|M
135             insecure
136             capath=s
137             cacert=s
138             cert=s
139             key=s
140             );
141              
142 4         10 my $argv;
143             my %opt;
144 4 50 33     28 if( defined $opt_ref && is_arrayref($opt_ref) ) {
145             # If passed an argv array, use that
146 0 0       0 $argv = $COPY_ARGV ? [ @{ $opt_ref } ] : $opt_ref;
  0         0  
147             }
148             else {
149             # Ensure multiple calls to cli_helpers_initialize() yield the same results
150 4 50       16 if ( $parsed_argv ) {
151             ## no critic
152 0         0 @ARGV = @argv_original;
153             ## use critic
154             }
155             else {
156 4         15 @argv_original = @ARGV;
157 4         8 $parsed_argv++;
158             }
159             # Operate on @ARGV
160 4 50       17 $argv = $COPY_ARGV ? [ @ARGV ] : \@ARGV;
161             }
162 4         71 GetOptionsFromArray($argv, \%opt, @opt_spec );
163 4         5053 return \%opt;
164             }
165             }
166              
167              
168             sub es_utils_initialize {
169 4     4 1 15 my ($argv) = @_;
170              
171             # Parse Options
172 4         12 my $opts = _parse_options($argv);
173              
174             # Config file locations
175 4         17 my @configs = (
176             '/etc/es-utils.yaml',
177             '/etc/es-utils.yml',
178             );
179 4 50       23 if( $ENV{HOME} ) {
180 4         15 push @configs, map { "$ENV{HOME}/.es-utils.$_" } qw( yaml yml );
  8         35  
181 4   33     45 my $xdg_config_home = $ENV{XDG_CONFIG_HOME} || "$ENV{HOME}/.config";
182 4         13 push @configs, map { "${xdg_config_home}/es-utils/config.$_" } qw( yaml yml );
  8         30  
183             }
184              
185 4         13 my @ConfigData=();
186 4         15 foreach my $config_file (@configs) {
187 24 50       354 next unless -f $config_file;
188 0         0 debug("Loading options from $config_file");
189             eval {
190 0         0 my $ref = YAML::XS::LoadFile($config_file);
191 0         0 push @ConfigData, $ref;
192 0         0 debug_var($ref);
193 0         0 1;
194 0 0       0 } or do {
195 0         0 debug({color=>"red"}, "[$config_file] $@");
196             };
197             }
198 4 50       40 %_GLOBALS = @ConfigData ? %{ clone_merge(@ConfigData) } : ();
  0         0  
199              
200             # Set defaults
201             %DEF = (
202             # Connection Options
203             HOST => exists $opts->{host} ? $opts->{host}
204             : exists $opts->{local} ? 'localhost'
205             : exists $_GLOBALS{host} ? $_GLOBALS{host}
206             : 'localhost',
207             PORT => exists $opts->{port} ? $opts->{port}
208             : exists $_GLOBALS{port} ? $_GLOBALS{port}
209             : 9200,
210             PROTO => exists $opts->{proto} ? $opts->{proto}
211             : exists $_GLOBALS{proto} ? $_GLOBALS{proto}
212             : 'http',
213             TIMEOUT => exists $opts->{timeout} ? $opts->{timeout}
214             : exists $_GLOBALS{timeout} ? $_GLOBALS{timeout}
215             : 10,
216             NOOP => exists $opts->{noop} ? $opts->{noop}
217             : exists $_GLOBALS{noop} ? $_GLOBALS{noop}
218             : undef,
219             NOPROXY => exists $opts->{'keep-proxy'} ? 0
220             : exists $_GLOBALS{'keep-proxy'} ? $_GLOBALS{'keep-proxy'}
221             : 1,
222             MASTERONLY => exists $opts->{'master-only'} ? $opts->{'master-only'} : 0,
223             # Index selection opts->ions
224             INDEX => exists $opts->{index} ? $opts->{index} : undef,
225             BASE => exists $opts->{base} ? lc $opts->{base}
226             : exists $_GLOBALS{base} ? $_GLOBALS{base}
227             : undef,
228             PATTERN => exists $opts->{pattern} ? $opts->{pattern} : '*',
229             DAYS => exists $opts->{days} ? $opts->{days}
230             : exists $_GLOBALS{days} ? $_GLOBALS{days} : 1,
231             # HTTP Basic Authentication
232             USERNAME => exists $opts->{'http-username'} ? $opts->{'http-username'}
233             : exists $_GLOBALS{'http-username'} ? $_GLOBALS{'http-username'}
234             : $ENV{USER},
235             PASSEXEC => exists $opts->{'password-exec'} ? $opts->{'password-exec'}
236             : exists $_GLOBALS{'password-exec'} ? $_GLOBALS{'password-exec'}
237             : undef,
238             # TLS Options
239             INSECURE => exists $opts->{insecure} ? 1
240             : exists $_GLOBALS{insecure} ? $_GLOBALS{insecure}
241             : 0,
242             CACERT => exists $opts->{cacert} ? 1
243             : exists $_GLOBALS{cacert} ? $_GLOBALS{cacert}
244             : undef,
245             CAPATH => exists $opts->{capath} ? 1
246             : exists $_GLOBALS{capath} ? $_GLOBALS{capath}
247             : undef,
248             CERT => exists $opts->{cert} ? 1
249             : exists $_GLOBALS{cert} ? $_GLOBALS{cert}
250             : undef,
251             KEY => exists $opts->{key} ? 1
252             : exists $_GLOBALS{key} ? $_GLOBALS{key}
253             : undef,
254 4 50       287 );
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
255 4 50       25 CLI::Helpers::override(verbose => 1) if $DEF{NOOP};
256              
257 4 50       18 if( $DEF{NOPROXY} ) {
258 4         26 debug("Removing any active HTTP Proxies from ENV.");
259 4         71362 delete $ENV{$_} for qw(http_proxy HTTP_PROXY);
260             }
261              
262              
263             # Build the Index Pattern
264 4         20 $PATTERN = $DEF{PATTERN};
265              
266 4         20 my @ordered = qw(* DATE ANY);
267 4         13 foreach my $literal ( @ordered ) {
268 12         190 $PATTERN =~ s/\Q$literal\E/$PATTERN_REGEX{$literal}/g;
269             }
270              
271             }
272              
273             # Regexes for Pattern Expansion
274             our $CURRENT_VERSION;
275             my $CLUSTER_MASTER;
276              
277              
278             sub es_globals {
279 1     1 1 5 my ($key) = @_;
280              
281 1 50       6 es_utils_initialize() unless keys %DEF;
282              
283 1 50       7 return unless exists $_GLOBALS{$key};
284 0         0 return $_GLOBALS{$key};
285             }
286              
287              
288             my %_auth_cache = ();
289              
290             sub es_basic_auth {
291 0     0 1 0 my ($host) = @_;
292              
293 0 0       0 es_utils_initialize() unless keys %DEF;
294              
295 0   0     0 $host ||= $DEF{HOST};
296              
297             # Return the results if we've done this already
298 0         0 return @{ $_auth_cache{$host} }{qw(username password)}
299 0 0       0 if exists $_auth_cache{$host};
300              
301             # Set the cached element
302 0         0 my %auth = ();
303              
304             # Lookup the details netrc
305 0         0 my $netrc = Net::Netrc->lookup($host);
306 0 0       0 if( $DEF{HOST} eq $host ) {
307 0         0 %auth = map { lc($_) => $DEF{$_} } qw(USERNAME);
  0         0  
308             }
309 0         0 my %meta = ();
310 0         0 foreach my $k (qw( http-username password-exec )) {
311 0         0 foreach my $name ( $DEF{INDEX}, $DEF{BASE} ) {
312 0 0       0 next unless $name;
313 0 0       0 if( my $v = es_local_index_meta($k, $name) ) {
314 0         0 $meta{$k} = $v;
315 0         0 last;
316             }
317             }
318             }
319              
320             # Get the Username
321             $auth{username} ||= $meta{'http-username'} ? $meta{'http-username'}
322             : defined $DEF{USERNAME} ? $DEF{USERNAME}
323             : defined $netrc ? $netrc->login
324 0 0 0     0 : $ENV{USER};
    0          
    0          
325              
326             # Prompt for the password
327             $auth{password} ||= defined $netrc ? $netrc->password
328             : (es_pass_exec($host,$auth{username},$meta{'password-exec'})
329 0 0 0     0 || prompt(sprintf "Password for '%s' at '%s': ", $auth{username}, $host)
      0        
330             );
331              
332             # Store
333 0         0 $_auth_cache{$host} = \%auth;
334 0         0 return @auth{qw(username password)};
335             }
336              
337              
338             sub es_pass_exec {
339 0     0 1 0 my ($host,$username,$exec) = @_;
340              
341 0 0       0 es_utils_initialize() unless keys %DEF;
342              
343             # Simplest case we can't run
344 0   0     0 $exec ||= $DEF{PASSEXEC};
345 0 0 0     0 return unless length $exec && -x $exec;
346              
347 0         0 my(@out,@err);
348             # Run the password command captue out, error and RC
349 0         0 run3 [ $exec, $host, $username ], \undef, \@out, \@err;
350 0         0 my $rc = $?;
351              
352             # Record the error
353 0 0 0     0 if( @err or $rc != 0 ) {
354 0         0 output({color=>'red',stderr=>1},
355             sprintf("es_pass_exec() called '%s' and met with an error code '%d'", $exec, $rc),
356             @err
357             );
358 0         0 return;
359             }
360              
361             # Format and return the result
362 0         0 my $passwd = $out[-1];
363 0         0 chomp($passwd);
364 0         0 return $passwd;
365             }
366              
367              
368              
369             sub es_pattern {
370 0 0   0 1 0 es_utils_initialize() unless keys %DEF;
371             return {
372             re => $PATTERN,
373             string => $DEF{PATTERN},
374 0         0 };
375             }
376              
377             sub _get_ssl_opts {
378 0 0   0   0 es_utils_initialize() unless keys %DEF;
379 0         0 my %opts = ();
380 0 0       0 $opts{verify_hostname} = 0 if $DEF{INSECURE};
381 0 0       0 $opts{SSL_ca_file} = $DEF{CACERT} if $DEF{CACERT};
382 0 0       0 $opts{SSL_ca_path} = $DEF{CAPATH} if $DEF{CAPATH};
383 0 0       0 $opts{SSL_cert_file} = $DEF{CERT} if $DEF{CERT};
384 0 0       0 $opts{SSL_key_file} = $DEF{KEY} if $DEF{KEY};
385 0         0 return \%opts;
386             }
387              
388             sub _get_es_version {
389 0 0   0   0 return $CURRENT_VERSION if defined $CURRENT_VERSION;
390 0         0 my $conn = es_connect();
391             # Build the request
392 0         0 my $req = App::ElasticSearch::Utilities::HTTPRequest->new(
393             GET => sprintf "%s://%s:%d",
394             $conn->proto, $conn->host, $conn->port
395             );
396             # Check if we're doing auth
397 0 0       0 my @auth = $DEF{PASSEXEC} ? es_basic_auth($conn->host) : ();
398             # Add authentication if we get a password
399 0 0       0 $req->authorization_basic( @auth ) if @auth;
400              
401             # Retry with TLS and/or Auth
402 0         0 my %try = map { $_ => 1 } qw( tls auth );
  0         0  
403 0         0 my $resp;
404 0         0 while( not defined $CURRENT_VERSION ) {
405 0         0 $resp = $conn->ua->request($req);
406 0 0 0     0 if( $resp->is_success ) {
    0          
    0          
407 0         0 my $ver;
408 0         0 eval {
409 0         0 $ver = $resp->content->{version};
410             };
411 0 0       0 if( $ver ) {
412 0 0 0     0 if( $ver->{distribution} and $ver->{distribution} eq 'opensearch' ) {
413 0         0 $CURRENT_VERSION = '7.10';
414             }
415             else {
416 0         0 $CURRENT_VERSION = join('.', (split /\./,$ver->{number})[0,1]);
417             }
418             }
419             }
420             elsif( $resp->code == 500 && $resp->message eq "Server closed connection without sending any data back" ) {
421             # Try TLS
422 0 0       0 last unless $try{tls};
423 0         0 delete $try{tls};
424 0         0 $conn->proto('https');
425 0         0 warn "Attempting promotion to HTTPS, try setting 'proto: https' in ~/.es-utils.yaml";
426             }
427             elsif( $resp->code == 401 ) {
428             # Retry with credentials
429 0 0       0 last unless $try{auth};
430 0         0 delete $try{auth};
431             warn "Authorization required, try setting 'password-exec: /home/user/bin/get-password.sh` in ~/.es-utils.yaml'"
432 0 0       0 unless $DEF{PASSEXEC};
433 0         0 $req->authorization_basic( es_basic_auth($conn->host) );
434             }
435             else {
436 0         0 warn "Failed getting version";
437 0         0 last;
438             }
439             }
440 0 0 0     0 if( !defined $CURRENT_VERSION || $CURRENT_VERSION <= 2 ) {
441 0         0 output({color=>'red',stderr=>1}, sprintf "[%d] Unable to determine Elasticsearch version, something has gone terribly wrong: aborting.", $resp->code);
442 0 0       0 output({color=>'red',stderr=>1}, ref $resp->content ? YAML::XS::Dump($resp->content) : $resp->content) if $resp->content;
    0          
443 0         0 exit 1;
444             }
445 0         0 debug({color=>'magenta'}, "FOUND VERISON '$CURRENT_VERSION'");
446 0         0 return $CURRENT_VERSION;
447             }
448              
449              
450             my $ES = undef;
451              
452             sub es_connect {
453 0     0 1 0 my ($override_servers) = @_;
454              
455 0 0       0 es_utils_initialize() unless keys %DEF;
456              
457             my %conn = (
458             host => $DEF{HOST},
459             port => $DEF{PORT},
460             proto => $DEF{PROTO},
461             timeout => $DEF{TIMEOUT},
462 0         0 ssl_opts => _get_ssl_opts,
463             );
464             # Only authenticate over TLS
465 0 0       0 if( $DEF{PROTO} eq 'https' ) {
466 0         0 $conn{username} = $DEF{USERNAME};
467 0 0       0 $conn{password} = es_pass_exec(@DEF{qw(HOST USERNAME)}) if $DEF{PASSEXEC};
468             }
469              
470             # If we're overriding, return a unique handle
471 0 0       0 if(defined $override_servers) {
472 0 0       0 my @overrides = is_arrayref($override_servers) ? @$override_servers : $override_servers;
473 0         0 my @servers;
474 0         0 foreach my $entry ( @overrides ) {
475 0         0 my ($s,$p) = split /\:/, $entry;
476 0   0     0 $p ||= $conn{port};
477 0         0 push @servers, { %conn, host => $s, port => $p };
478             }
479              
480 0 0       0 if( @servers > 0 ) {
481 0 0       0 my $pick = @servers > 1 ? $servers[int(rand(@servers))] : $servers[0];
482 0         0 return App::ElasticSearch::Utilities::Connection->new(%{$pick});
  0         0  
483             }
484             }
485             else {
486             # Check for index metadata
487 0         0 foreach my $k ( keys %conn ) {
488 0         0 foreach my $name ( $DEF{INDEX}, $DEF{BASE} ) {
489 0 0       0 next unless $name;
490 0 0       0 if( my $v = es_local_index_meta($k => $name) ) {
491 0         0 $conn{$k} = $v;
492 0         0 last;
493             }
494             }
495             }
496             }
497              
498             # Otherwise, cache our handle
499 0   0     0 $ES ||= App::ElasticSearch::Utilities::Connection->new(%conn);
500              
501 0         0 return $ES;
502             }
503              
504              
505             sub es_master {
506 0     0 1 0 my ($instance) = @_;
507 0 0 0     0 if(!defined $instance && defined $CLUSTER_MASTER) {
508 0         0 return $CLUSTER_MASTER;
509             }
510 0         0 my $is_master = 0;
511 0         0 my @request = ('/_cluster/state/master_node');
512 0 0       0 unshift @request, $instance if defined $instance;
513              
514 0         0 my $cluster = es_request(@request);
515 0 0 0     0 if( defined $cluster && $cluster->{master_node} ) {
516 0         0 my $local = es_request('/_nodes/_local');
517 0 0 0     0 if ($local->{nodes} && $local->{nodes}{$cluster->{master_node}}) {
518 0         0 $is_master = 1;
519             }
520             }
521 0 0       0 $CLUSTER_MASTER = $is_master unless defined $instance;
522 0         0 return $is_master;
523             }
524              
525              
526             sub es_request {
527 0 0   0 1 0 my $instance = ref $_[0] eq 'App::ElasticSearch::Utilities::Connection' ? shift @_ : es_connect();
528              
529 0 0       0 $CURRENT_VERSION = _get_es_version() if !defined $CURRENT_VERSION;
530              
531 0         0 my($url,$options,$body) = _fix_version_request(@_);
532              
533             # Normalize the options
534 0   0     0 $options->{method} ||= 'GET';
535 0         0 $options->{command} = $url;
536 0         0 my $index;
537              
538 0 0       0 if( exists $options->{index} ) {
539 0 0       0 if( my $index_in = delete $options->{index} ) {
540             # No need to validate _all
541 0 0       0 if( $index_in eq '_all') {
542 0         0 $index = $index_in;
543             }
544             else {
545             # Validate each included index
546 0 0       0 my @indexes = is_arrayref($index_in) ? @{ $index_in } : split /\,/, $index_in;
  0         0  
547 0         0 $index = join(',', @indexes);
548             }
549             }
550             }
551              
552             # For the cat api, index goes *after* the command
553 0 0 0     0 if( $url =~ /^_(cat|stats)/ && $index ) {
    0          
554 0         0 $url =~ s/\/$//;
555 0         0 $url = join('/', $url, $index);
556 0         0 delete $options->{command};
557             }
558             elsif( $index ) {
559 0         0 $options->{index} = $index;
560             }
561             else {
562 0         0 $index = '';
563             }
564              
565             # Figure out if we're modifying things
566             my $modification = $url eq '_search' && $options->{method} eq 'POST' ? 0
567 0 0 0     0 : $options->{method} ne 'GET';
568              
569 0 0       0 if($modification) {
570             # Set NOOP if necessary
571 0 0 0     0 if(!$DEF{NOOP} && $DEF{MASTERONLY}) {
572 0 0       0 if( !es_master() ) {
573 0         0 $DEF{NOOP} = 1;
574             }
575             }
576              
577             # Check for noop
578 0 0       0 if( $DEF{NOOP} ) {
579 0 0 0     0 my $flag = $DEF{MASTERONLY} && !es_master() ? '--master-only' : '--noop';
580 0         0 output({color=>'cyan'}, "Called es_request($index/$options->{command}), but $flag set and method is $options->{method}");
581 0         0 return;
582             }
583             }
584              
585             # Make the request
586 0         0 my $resp = $instance->request($url,$options,$body);
587              
588             # Check the response is defined, bail if it's not
589 0 0       0 die "Unsupported request method: $options->{method}" unless defined $resp;
590              
591             # Logging
592             verbose({color=>'yellow'}, sprintf "es_request(%s/%s) returned HTTP Status %s",
593 0 0       0 $index, $options->{command}, $resp->message,
594             ) if $resp->code != 200;
595              
596             # Error handling
597 0 0 0     0 if( !$resp->is_success ) {
    0          
598 0         0 my $msg;
599             eval {
600 0         0 my @causes = ();
601 0         0 foreach my $cause ( @{ $resp->content->{error}{root_cause} } ) {
  0         0  
602 0 0       0 push @causes, $cause->{index} ? "$cause->{index}: $cause->{reason}" : $cause->{reason};
603             }
604 0         0 $msg = join("\n", map { "\t$_" } @causes);
  0         0  
605 0         0 1;
606 0 0       0 } or do {
607             # Default to the message, though it's usually unhelpful
608 0         0 $msg = $resp->{message};
609             };
610             die sprintf "es_request(%s/%s) failed[%d]:\n%s",
611 0   0     0 $index, $options->{command}, $resp->code, $msg || 'missing error message';
612              
613             } elsif( !defined $resp->content || ( !is_ref($resp->content) && !length $resp->content )) {
614             output({color=>'yellow',stderr=>1},
615             sprintf "es_request(%s/%s) empty response[%d]: %s",
616 0         0 $index, $options->{command}, $resp->code, $resp->message
617             );
618             }
619              
620 0         0 return $resp->content;
621             }
622              
623              
624              
625             my %_nodes;
626             sub es_nodes {
627 0 0   0 1 0 if(!keys %_nodes) {
628 0         0 my $res = es_request('_cluster/state/nodes', {});
629 0 0       0 if( !defined $res ) {
630 0         0 output({color=>"red"}, "es_nodes(): Unable to locate nodes in status!");
631 0         0 exit 1;
632             }
633 0         0 foreach my $id ( keys %{ $res->{nodes} } ) {
  0         0  
634 0         0 $_nodes{$id} = $res->{nodes}{$id}{name};
635             }
636             }
637              
638 0 0       0 return wantarray ? %_nodes : { %_nodes };
639             }
640              
641              
642             my $_indices_meta;
643             sub es_indices_meta {
644 0 0   0 1 0 if(!defined $_indices_meta) {
645 0         0 my $result = es_request('_cluster/state/metadata');
646 0 0       0 if ( !defined $result ) {
647 0         0 output({stderr=>1,color=>"red"}, "es_indices_meta(): Unable to locate indices in status!");
648 0         0 exit 1;
649             }
650 0         0 $_indices_meta = $result->{metadata}{indices};
651             }
652              
653 0         0 my %copy = %{ $_indices_meta };
  0         0  
654 0 0       0 return wantarray ? %copy : \%copy;
655             }
656              
657              
658             my %_valid_index = ();
659             sub es_indices {
660 0     0 1 0 my %args = (
661             state => 'open',
662             check_state => 1,
663             check_dates => 1,
664             @_
665             );
666              
667 0 0       0 es_utils_initialize() unless keys %DEF;
668              
669             # Seriously, English? Do you speak it motherfucker?
670 0 0       0 $args{state} = 'close' if $args{state} eq 'closed';
671              
672 0         0 my @indices = ();
673 0         0 my %idx = ();
674 0 0 0     0 my $wildcard = !exists $args{_all} && defined $DEF{BASE} ? sprintf "/*%s*", $DEF{BASE} : '';
675              
676             # Simplest case, single index
677 0 0 0     0 if( defined $DEF{INDEX} ) {
    0 0        
678 0         0 push @indices, $DEF{INDEX};
679             }
680             # Next simplest case, open indexes
681             elsif( !exists $args{_all} && $args{check_state} && $args{state} eq 'open' ) {
682             # Use _stats because it's break neck fast
683 0 0       0 if( my $res = es_request($wildcard . '/_stats/docs') ) {
684 0         0 foreach my $idx ( keys %{ $res->{indices} } ) {
  0         0  
685 0         0 $idx{$idx} = 'open';
686             }
687             }
688             }
689             else {
690 0         0 my $res = es_request('_cat/indices' . $wildcard, { uri_param => { h => 'index,status' } });
691 0         0 foreach my $entry (@{ $res }) {
  0         0  
692 0 0       0 my ($index,$status) = is_hashref($entry) ? @{ $entry }{qw(index status)} : split /\s+/, $entry;
  0         0  
693 0         0 $idx{$index} = $status;
694             }
695             }
696              
697 0         0 foreach my $index (sort keys %idx) {
698 0 0       0 if(!exists $args{_all}) {
699 0         0 my $status = $idx{$index};
700             # State Check Disqualification
701 0 0 0     0 if($args{state} ne 'all' && $args{check_state}) {
702 0         0 my $result = $status eq $args{state};
703 0 0       0 next unless $result;
704             }
705              
706 0         0 my $p = es_pattern();
707 0 0       0 next unless $index =~ /$p->{re}/;
708 0         0 debug({indent=>2},"= name checks succeeded");
709              
710 0 0 0     0 if ($args{older} && defined $DEF{DAYS}) {
    0 0        
711 0         0 my $days_old = es_index_days_old( $index );
712 0 0 0     0 if (!defined $days_old || $days_old < $DEF{DAYS}) {
713 0         0 next;
714             }
715             }
716             elsif( $args{check_dates} && defined $DEF{DAYS} ) {
717 0         0 my $days_old = es_index_days_old( $index );
718 0 0 0     0 if( !defined $days_old ) {
    0          
719 0         0 debug({indent=>2,color=>'red'}, "! error locating date in string, skipping !");
720 0         0 next;
721             }
722             elsif( $DEF{DAYS} >= 0 && $days_old >= $DEF{DAYS} ) {
723 0         0 next;
724             }
725             }
726             }
727             else {
728 0         0 debug({indent=>1}, "Called with _all, all checks skipped.");
729             }
730 0         0 debug({indent=>1,color=>"green"}, "+ match!");
731 0         0 push @indices, $index;
732             }
733              
734             # We retrieved these from the cluster, so preserve them here.
735 0         0 $_valid_index{$_} = 1 for @indices;
736              
737 0 0       0 return wantarray ? @indices : \@indices;
738             }
739              
740              
741             sub es_index_strip_date {
742 68     68 1 122 my ($index) = @_;
743              
744 68 50       134 return -1 unless defined $index;
745              
746 68 50       144 es_utils_initialize() unless keys %DEF;
747              
748             # Try the Date Pattern
749 68 50       576 if( my $base = $index =~ s/[^a-z0-9]+$PATTERN_REGEX{DATE}.*$//rio ) {
750 68         221 return $base;
751             }
752 0         0 return;
753             }
754              
755              
756             my %_stripped=();
757              
758             sub es_index_bases {
759 34     34 1 9761 my ($index) = @_;
760              
761 34 50       81 return unless defined $index;
762              
763             # Strip to the base
764 34         68 my $stripped = es_index_strip_date($index);
765             # Remove the rollover portion
766 34         102 $stripped =~ s/[\-_.]\d+$//;
767 34 50 33     162 return unless defined $stripped and length $stripped;
768              
769             # Compute if we haven't already memoized
770 34 100       96 if( !exists $_stripped{$stripped} ) {
771 5         10 my %bases=();
772 5 50       20 my @parts = grep { defined && length } split /[-_]/, $stripped;
  9         40  
773 5         39 debug(sprintf "es_index_bases(%s) dissected to %s", $index, join(',', @parts));
774 5 100       62 my $sep = index( $stripped, '_' ) >= 0 ? '_' : '-';
775              
776 5         12 my %collected = ();
777 5         15 foreach my $end ( 0..$#parts ) {
778 9         26 my $name = join($sep, @parts[0..$end]);
779 9         27 $collected{$name} = 1;
780             }
781 5         42 $_stripped{$stripped} = [ sort keys %collected ]
782             }
783              
784 34         56 return @{ $_stripped{$stripped} };
  34         138  
785             }
786              
787              
788             sub es_index_days_old {
789 34     34 1 67 my ($index) = @_;
790              
791 34 50       76 return unless defined $index;
792              
793 34 50       71 es_utils_initialize() unless keys %DEF;
794              
795              
796 34 100       204 if( $index =~ /[^a-z0-9]$PATTERN_REGEX{DATE}/io ) {
797             # Build Date Array
798 99         213 my @date = map { int }
799 99         176 grep { length }
800 4     4   2036 map { $+{$_} =~ s/^0//r } qw(day month year);
  4         1544  
  4         9059  
  33         90  
  99         643  
801 33         71 $date[1]--; # move 1-12 -> 0-11
802             # Validate
803 33 50       79 if( @date != 3 ) {
804             warn sprintf "es_index_days_old(%s) matched DATE(%s), but did not receive enough parts: %s",
805             $index,
806             $+{datestr},
807 0         0 join(', ', map { "'$_'" } @date);
  0         0  
808 0         0 return;
809             }
810              
811             # Calculate Difference
812 33         224 my $now = timegm(0,0,0,(gmtime)[3,4,5]);
813 33         1093 my $idx_time = eval { timegm( 0,0,0, @date ) };
  33         73  
814 33 50       909 return unless $idx_time;
815 33         50 my $diff = $now - $idx_time;
816 33         54 $diff++; # Add one second
817 33         384 debug({color=>"yellow"}, sprintf "es_index_days_old(%s) - Time difference is %0.3f", $index, $diff/86400);
818 33         405 return int($diff / 86400);
819             }
820 1         9 verbose({color=>"red"}, "es_index_days_old($index) - date string not found");
821 1         22 return;
822             }
823              
824              
825              
826             sub es_index_shards {
827 0     0 1 0 my ($index) = @_;
828              
829 0         0 my %shards = map { $_ => 0 } qw(primaries replicas);
  0         0  
830 0         0 my $result = es_request('_settings', {index=>$index});
831 0 0 0     0 if( defined $result && is_hashref($result) ) {
832 0         0 $shards{primaries} = $result->{$index}{settings}{index}{number_of_shards};
833 0         0 $shards{replicas} = $result->{$index}{settings}{index}{number_of_replicas};
834             }
835              
836 0 0       0 return wantarray ? %shards : \%shards;
837             }
838              
839              
840             sub es_index_valid {
841 0     0 1 0 my ($index) = @_;
842              
843 0 0 0     0 return unless defined $index && length $index;
844 0 0       0 return $_valid_index{$index} if exists $_valid_index{$index};
845              
846 0         0 my $es = es_connect();
847 0         0 my $result;
848 0         0 eval {
849 0         0 debug("Running index_exists");
850 0         0 $result = $es->exists( index => $index );
851             };
852 0         0 return $_valid_index{$index} = $result;
853             }
854              
855              
856             sub es_index_fields {
857 0     0 1 0 my ($index) = @_;
858              
859 0         0 my $result = es_request('_mapping', { index => $index });
860              
861 0 0       0 return unless defined $result;
862              
863 0         0 my %fields;
864 0         0 foreach my $idx ( sort keys %{ $result } ) {
  0         0  
865             # Handle Version incompatibilities
866 0 0       0 my $ref = exists $result->{$idx}{mappings} ? $result->{$idx}{mappings} : $result->{$idx};
867              
868             # Loop through the mappings, skipping _default_, except on 7.x where we notice "properties"
869             my @mappings = exists $ref->{properties} ? ($ref)
870 0 0       0 : map { $ref->{$_} } grep { $_ ne '_default_' } keys %{ $ref };
  0         0  
  0         0  
  0         0  
871 0         0 foreach my $mapping (@mappings) {
872 0         0 _find_fields(\%fields,$mapping);
873             }
874             }
875             # Return the results
876 0         0 return \%fields;
877             }
878              
879             {
880             # Closure for field metadata
881             my $nested_path;
882              
883             sub _add_fields {
884 0     0   0 my ($f,$type,@path) = @_;
885              
886 0 0       0 return unless @path;
887              
888 0         0 my %i = (
889             type => $type,
890             );
891              
892             # Store the full path
893 0         0 my $key = join('.', @path);
894              
895 0 0       0 if( $nested_path ) {
896 0         0 $i{nested_path} = $nested_path;
897 0         0 $i{nested_key} = substr( $key, length($nested_path)+1 );
898             }
899              
900 0         0 $f->{$key} = \%i;
901             }
902              
903             sub _find_fields {
904 0     0   0 my ($f,$ref,@path) = @_;
905              
906 0 0       0 return unless is_hashref($ref);
907             # Handle things with properties
908 0 0 0     0 if( exists $ref->{properties} && is_hashref($ref->{properties}) ) {
    0          
909 0 0 0     0 $nested_path = join('.', @path) if $ref->{type} and $ref->{type} eq 'nested';
910 0         0 foreach my $k (sort keys %{ $ref->{properties} }) {
  0         0  
911 0         0 _find_fields($f,$ref->{properties}{$k},@path,$k);
912             }
913 0         0 undef($nested_path);
914             }
915             # Handle elements that contain data
916             elsif( exists $ref->{type} ) {
917 0         0 _add_fields($f,$ref->{type},@path);
918             # Handle multifields
919 0 0 0     0 if( exists $ref->{fields} && is_hashref($ref->{fields}) ) {
920 0         0 foreach my $k (sort keys %{ $ref->{fields} } ) {
  0         0  
921 0         0 _add_fields($f,$ref->{type},@path,$k);
922             }
923             }
924             }
925             # Unknown data, throw an error if we care that deeply.
926             else {
927             debug({stderr=>1,color=>'red'},
928             sprintf "_find_fields(): Invalid property at: %s ref info: %s",
929             join('.', @path),
930 0 0       0 join(',', is_hashref($ref) ? sort keys %{$ref} :
  0 0       0  
931             ref $ref ? ref $ref : 'unknown ref'
932             ),
933             );
934             }
935             }
936             }
937              
938              
939             sub es_close_index {
940 0     0 1 0 my($index) = @_;
941              
942 0         0 return es_request('_close',{ method => 'POST', index => $index });
943             }
944              
945              
946             sub es_open_index {
947 0     0 1 0 my($index) = @_;
948              
949 0         0 return es_request('_open',{ method => 'POST', index => $index });
950             }
951              
952              
953             sub es_delete_index {
954 0     0 1 0 my($index) = @_;
955              
956 0         0 return es_request('',{ method => 'DELETE', index => $index });
957             }
958              
959              
960             sub es_optimize_index {
961 0     0 1 0 my($index) = @_;
962              
963 0         0 return es_request('_forcemerge',{
964             method => 'POST',
965             index => $index,
966             uri_param => {
967             max_num_segments => 1,
968             },
969             });
970             }
971              
972              
973             sub es_apply_index_settings {
974 0     0 1 0 my($index,$settings) = @_;
975              
976 0 0       0 if(!is_hashref($settings)) {
977 0         0 output({stderr=>1,color=>'red'}, 'usage is es_apply_index_settings($index,$settings_hashref)');
978 0         0 return;
979             }
980              
981 0         0 return es_request('_settings',{ method => 'PUT', index => $index },$settings);
982             }
983              
984              
985             sub es_index_segments {
986 0     0 1 0 my ($index) = @_;
987              
988 0 0 0     0 if( !defined $index || !length $index || !es_index_valid($index) ) {
      0        
989 0         0 output({stderr=>1,color=>'red'}, "es_index_segments('$index'): invalid index");
990 0         0 return;
991             }
992              
993 0         0 return es_request('_segments', {
994             index => $index,
995             });
996              
997             }
998              
999              
1000             sub es_segment_stats {
1001 0     0 1 0 my ($index) = @_;
1002              
1003 0         0 my %segments = map { $_ => 0 } qw(shards segments);
  0         0  
1004 0         0 my $result = es_index_segments($index);
1005              
1006 0 0       0 if(defined $result) {
1007 0         0 my $shard_data = $result->{indices}{$index}{shards};
1008 0         0 foreach my $id (keys %{$shard_data}) {
  0         0  
1009 0         0 $segments{segments} += $shard_data->{$id}[0]{num_search_segments};
1010 0         0 $segments{shards}++;
1011             }
1012             }
1013 0 0       0 return wantarray ? %segments : \%segments;
1014             }
1015              
1016              
1017              
1018             sub es_index_stats {
1019 0     0 1 0 my ($index) = @_;
1020              
1021 0         0 return es_request('_stats', {
1022             index => $index
1023             });
1024             }
1025              
1026              
1027              
1028             sub es_settings {
1029 0     0 1 0 return es_request('_settings');
1030             }
1031              
1032              
1033             sub es_node_stats {
1034 0     0 1 0 my (@nodes) = @_;
1035              
1036 0         0 my @cmd = qw(_nodes);
1037 0 0       0 push @cmd, join(',', @nodes) if @nodes;
1038 0         0 push @cmd, 'stats';
1039              
1040 0         0 return es_request(join('/',@cmd));
1041             }
1042              
1043              
1044             sub es_flatten_hash {
1045 2     2 1 1731 my $hash = shift;
1046 2         15 my $_flat = flatten($hash, { HashDelimiter=>':', ArrayDelimiter=>':' });
1047 2         863 my %compat = map { s/:/./gr => $_flat->{$_} } keys %{ $_flat };
  6         28  
  2         7  
1048 2         11 return \%compat;
1049             }
1050              
1051              
1052             sub es_human_count {
1053 0     0 1   my ($size) = @_;
1054              
1055 0           my $unit = 'docs';
1056 0           my @units = qw(thousand million billion);
1057              
1058 0   0       while( $size > 1000 && @units ) {
1059 0           $size /= 1000;
1060 0           $unit = shift @units;
1061             }
1062              
1063 0           return sprintf "%0.2f %s", $size, $unit;
1064             }
1065              
1066              
1067             sub es_human_size {
1068 0     0 1   my ($size) = @_;
1069              
1070 0           my $unit = 'b';
1071 0           my @units = qw(Kb Mb Gb Tb);
1072              
1073 0   0       while( $size > 1024 && @units ) {
1074 0           $size /= 1024;
1075 0           $unit = shift @units;
1076             }
1077              
1078 0           return sprintf "%0.2f %s", $size, $unit;
1079             }
1080              
1081              
1082             sub def {
1083 0     0 1   my($key)= map { uc }@_;
  0            
1084              
1085 0 0         es_utils_initialize() unless keys %DEF;
1086              
1087 0 0         return exists $DEF{$key} ? $DEF{$key} : undef;
1088             }
1089              
1090              
1091             sub es_local_index_meta {
1092 0     0 1   my ($key,$name_or_base) = @_;
1093              
1094 0 0         es_utils_initialize() unless keys %DEF;
1095              
1096 0 0         if( exists $_GLOBALS{meta} ) {
1097 0           my $meta = $_GLOBALS{meta};
1098 0           my @search = ( $name_or_base );
1099 0           push @search, es_index_strip_date($name_or_base);
1100 0           push @search, es_index_bases($name_or_base);
1101              
1102 0           foreach my $check ( @search ) {
1103 0 0 0       if( exists $meta->{$check} && exists $meta->{$check}{$key} ) {
1104 0           return $meta->{$check}{$key};
1105             }
1106             }
1107             }
1108              
1109 0           return;
1110             }
1111              
1112              
1113              
1114             1;
1115              
1116             __END__
1117              
1118             =pod
1119              
1120             =head1 NAME
1121              
1122             App::ElasticSearch::Utilities - Utilities for Monitoring ElasticSearch
1123              
1124             =head1 VERSION
1125              
1126             version 8.7
1127              
1128             =head1 SYNOPSIS
1129              
1130             This library contains utilities for unified interfaces in the scripts.
1131              
1132             This a set of utilities to make monitoring ElasticSearch clusters much simpler.
1133              
1134             Included are:
1135              
1136             B<SEARCHING>:
1137              
1138             scripts/es-search.pl - Utility to interact with LogStash style indices from the CLI
1139              
1140             B<MONITORING>:
1141              
1142             scripts/es-graphite-dynamic.pl - Perform index maintenance on daily indexes
1143             scripts/es-status.pl - Command line utility for ES Metrics
1144             scripts/es-storage-overview.pl - View how shards/data is aligned on your cluster
1145             scripts/es-nodes.pl - View node information
1146              
1147             B<MAINTENANCE>:
1148              
1149             scripts/es-daily-index-maintenance.pl - Perform index maintenance on daily indexes
1150             scripts/es-alias-manager.pl - Manage index aliases automatically
1151             scripts/es-open.pl - Open any closed indices matching a index parameters
1152              
1153             B<MANAGEMENT>:
1154              
1155             scripts/es-apply-settings.pl - Apply settings to all indexes matching a pattern
1156             scripts/es-cluster-settings.pl - Manage cluster settings
1157             scripts/es-copy-index.pl - Copy an index from one cluster to another
1158             scripts/es-storage-overview.pl - View how shards/data is aligned on your cluster
1159              
1160             B<DEPRECATED>:
1161              
1162             scripts/es-graphite-static.pl - Send ES Metrics to Graphite or Cacti
1163              
1164             The App::ElasticSearch::Utilities module simply serves as a wrapper around the scripts for packaging and
1165             distribution.
1166              
1167             =head1 FUNCTIONS
1168              
1169             =head2 es_utils_initialize()
1170              
1171             Takes an optional reference to an C<@ARGV> like array. Performs environment and
1172             argument parsing.
1173              
1174             =head2 es_globals($key)
1175              
1176             Grab the value of the global value from the es-utils.yaml files.
1177              
1178             =head2 es_basic_auth($host)
1179              
1180             Get the user/password combination for this host. This is called from LWP::UserAgent if
1181             it recieves a 401, so the auth condition must be satisfied.
1182              
1183             Returns the username and password as a list.
1184              
1185             =head2 es_pass_exec(host, username)
1186              
1187             Called from es_basic_auth to exec a program, capture the password
1188             and return it to the caller. This allows the use of password vaults
1189             and keychains.
1190              
1191             =head2 es_pattern
1192              
1193             Returns a hashref of the pattern filter used to get the indexes
1194             {
1195             string => '*',
1196             re => '.*',
1197             }
1198              
1199             =head2 es_connect
1200              
1201             Without options, this connects to the server defined in the args. If passed
1202             an array ref, it will use that as the connection definition.
1203              
1204             =head2 es_master([$handle])
1205              
1206             Returns true (1) if the handle is to the the cluster master, or false (0) otherwise.
1207              
1208             =head2 es_request([$handle],$command,{ method => 'GET', uri_param => { a => 1 } }, {})
1209              
1210             Retrieve URL from ElasticSearch, returns a hash reference
1211              
1212             First hash ref contains options, including:
1213              
1214             uri_param Query String Parameters
1215             index Index name
1216             type Index type
1217             method Default is GET
1218              
1219             If the request is not successful, this function will throw a fatal exception.
1220             If you'd like to proceed you need to catch that error.
1221              
1222             =head2 es_nodes
1223              
1224             Returns the hash of index meta data.
1225              
1226             =head2 es_indices_meta
1227              
1228             Returns the hash of index meta data.
1229              
1230             =head2 es_indices
1231              
1232             Returns a list of active indexes matching the filter criteria specified on the command
1233             line. Can handle indices named:
1234              
1235             logstash-YYYY.MM.DD
1236             dcid-logstash-YYYY.MM.DD
1237             logstash-dcid-YYYY.MM.DD
1238             logstash-YYYY.MM.DD-dcid
1239              
1240             Makes use of --datesep to determine where the date is.
1241              
1242             Options include:
1243              
1244             =over 4
1245              
1246             =item B<state>
1247              
1248             Default is 'open', can be used to find 'closed' indexes as well.
1249              
1250             =item B<check_state>
1251              
1252             Default is 1, set to 0 to disable state checks. The combination of the default
1253             with this option and the default for B<state> means only open indices are returned.
1254              
1255             =item B<check_dates>
1256              
1257             Default is 1, set to 0 to disable checking index age.
1258              
1259             =back
1260              
1261             =head2 es_index_strip_date( 'index-name' )
1262              
1263             Returns the index name with the date removed.
1264              
1265             =head2 es_index_bases( 'index-name' )
1266              
1267             Returns an array of the possible index base names for this index
1268              
1269             =head2 es_index_days_old( 'index-name' )
1270              
1271             Return the number of days old this index is.
1272              
1273             =head2 es_index_shards( 'index-name' )
1274              
1275             Returns the number of replicas for a given index.
1276              
1277             =head2 es_index_valid( 'index-name' )
1278              
1279             Checks if the specified index is valid
1280              
1281             =head2 es_index_fields('index-name')
1282              
1283             Returns a hash reference with the following data:
1284              
1285             key_name:
1286             type: field_data_type
1287             # If the field is nested
1288             nested_path: nested_path
1289             nested_key: nested_key
1290              
1291             =head2 es_close_index('index-name')
1292              
1293             Closes an index
1294              
1295             =head2 es_open_index('index-name')
1296              
1297             Open an index
1298              
1299             =head2 es_delete_index('index-name')
1300              
1301             Deletes an index
1302              
1303             =head2 es_optimize_index('index-name')
1304              
1305             Optimize an index to a single segment per shard
1306              
1307             =head2 es_apply_index_settings('index-name', { settings })
1308              
1309             Apply a HASH of settings to an index.
1310              
1311             =head2 es_index_segments( 'index-name' )
1312              
1313             Exposes GET /$index/_segments
1314              
1315             Returns the segment data from the index in hashref:
1316              
1317             =head2 es_segment_stats($index)
1318              
1319             Return the number of shards and segments in an index as a hashref
1320              
1321             =head2 es_index_stats( 'index-name' )
1322              
1323             Exposes GET /$index/_stats
1324              
1325             Returns a hashref
1326              
1327             =head2 es_settings()
1328              
1329             Exposes GET /_settings
1330              
1331             Returns a hashref
1332              
1333             =head2 es_node_stats()
1334              
1335             Exposes GET /_nodes/stats
1336              
1337             Returns a hashref
1338              
1339             =head2 es_flatten_hash
1340              
1341             Performs flattening that's compatible with Elasticsearch's flattening.
1342              
1343             =head2 es_human_count
1344              
1345             Takes a number and returns the number as a string in docs, thousands, millions, or billions.
1346              
1347             1_000 -> "1.00 thousand",
1348             1_000_000 -> "1.00 million",
1349              
1350             =head2 es_human_size
1351              
1352             Takes a number and returns the number as a string in bytes, Kb, Mb, Gb, or Tb using base 1024.
1353              
1354             1024 -> '1.00 Kb',
1355             1048576 -> '1.00 Mb',
1356             1073741824 -> '1.00 Gb',
1357              
1358             =head2 def('key')
1359              
1360             Exposes Definitions grabbed by options parsing
1361              
1362             =head2 es_local_index_meta(key => 'base' || 'index')
1363              
1364             Fetch meta-data from the local config file, i.e. C<~/.es-utils.yaml>.
1365              
1366             Format is:
1367              
1368             ---
1369             meta:
1370             index_name:
1371             key: value
1372             index_basename:
1373             key: value
1374              
1375             The most specific version is searched first, followed by the index stripped of
1376             it's date, and then on through all the bases discovered with
1377             C<es_index_bases()>.
1378              
1379             This is used by the C<es-search.pl> utility to do lookups of the B<timestamp>
1380             field it needs to sort documents, i.e.:
1381              
1382             ---
1383             meta:
1384             logstash:
1385             timestamp: '@timestamp'
1386             host: es-cluster-01.int.example.com
1387             bro:
1388             timestamp: 'timestamp'
1389              
1390             =head1 ARGS
1391              
1392             From App::ElasticSearch::Utilities:
1393              
1394             --local Use localhost as the elasticsearch host
1395             --host ElasticSearch host to connect to
1396             --port HTTP port for your cluster
1397             --proto Defaults to 'http', can also be 'https'
1398             --http-username HTTP Basic Auth username
1399             --password-exec Script to run to get the users password
1400             --insecure Don't verify TLS certificates
1401             --cacert Specify the TLS CA file
1402             --capath Specify the directory with TLS CAs
1403             --cert Specify the path to the client certificate
1404             --key Specify the path to the client private key file
1405             --noop Any operations other than GET are disabled, can be negated with --no-noop
1406             --timeout Timeout to ElasticSearch, default 10
1407             --keep-proxy Do not remove any proxy settings from %ENV
1408             --index Index to run commands against
1409             --base For daily indexes, reference only those starting with "logstash"
1410             (same as --pattern logstash-* or logstash-DATE)
1411             --pattern Use a pattern to operate on the indexes
1412             --days If using a pattern or base, how many days back to go, default: 1
1413              
1414             See also the "CONNECTION ARGUMENTS" and "INDEX SELECTION ARGUMENTS" sections from App::ElasticSearch::Utilities.
1415              
1416             =head1 ARGUMENT GLOBALS
1417              
1418             Some options may be specified in the B</etc/es-utils.yaml>, B<$HOME/.es-utils.yaml>
1419             or B<$HOME/.config/es-utils/config.yaml> file:
1420              
1421             ---
1422             base: logstash
1423             days: 7
1424             host: esproxy.example.com
1425             port: 80
1426             timeout: 10
1427             proto: https
1428             http-username: bob
1429             password-exec: /home/bob/bin/get-es-passwd.sh
1430              
1431             =head1 CONNECTION ARGUMENTS
1432              
1433             Arguments for establishing a connection with the cluster. Unless specified otherwise, these options
1434             can all be set in the globals file.
1435              
1436             =over
1437              
1438             =item B<local>
1439              
1440             Assume ElasticSearch is running locally, connect to localhost.
1441              
1442             =item B<host>
1443              
1444             Use a different hostname or IP address to connect.
1445              
1446             =item B<port>
1447              
1448             Defaults to 9200.
1449              
1450             =item B<proto>
1451              
1452             Defaults to 'http', can also be 'https'.
1453              
1454             =item B<http-username>
1455              
1456             If HTTP Basic Authentication is required, use this username.
1457              
1458             See also the L<HTTP Basic Authentication> section for more details
1459              
1460             =item B<password-exec>
1461              
1462             If HTTP Basic Authentication is required, run this command, passing the arguments:
1463              
1464             <command_to_run> <es_host> <es_username>
1465              
1466             The script expects the last line to contain the password in plaintext.
1467              
1468             =item B<noop>
1469              
1470             Prevents any communication to the cluster from making changes to the settings or data contained therein.
1471             In short, it prevents anything but HEAD and GET requests, B<except> POST requests to the _search endpoint.
1472              
1473             =item B<timeout>
1474              
1475             Timeout for connections and requests, defaults to 10.
1476              
1477             =item B<keep-proxy>
1478              
1479             By default, HTTP proxy environment variables are stripped. Use this option to keep your proxy environment variables
1480             in tact.
1481              
1482             =item B<insecure>
1483              
1484             Don't verify TLS certificates
1485              
1486             =item B<cacert>
1487              
1488             Specify a file with the TLS CA certificates.
1489              
1490             =item B<capath>
1491              
1492             Specify a directory containing the TLS CA certificates.
1493              
1494             =item B<cert>
1495              
1496             Specify the path to the TLS client certificate file..
1497              
1498             =item B<key>
1499              
1500             Specify the path to the TLS client private key file.
1501              
1502             =back
1503              
1504             =head1 INDEX SELECTION ARGUMENTS
1505              
1506             =over
1507              
1508             =item B<base>
1509              
1510             In an environment using monthly, weekly, daily, or hourly indexes. The base index name is everything without the date.
1511             Parsing for bases, also provides splitting and matching on segments of the index name delineated by the '-' character.
1512             If we have the following indexes:
1513              
1514             web-dc1-YYYY.MM.DD
1515             web-dc2-YYYY.MM.DD
1516             logstash-dc1-YYYY.MM.DD
1517             logstash-dc2-YYYY.MM.DD
1518              
1519             Valid bases would be:
1520              
1521             web
1522             web-dc1
1523             web-dc2
1524             logstash
1525             logstash-dc1
1526             logstash-dc2
1527             dc1
1528             dc2
1529              
1530             Combining that with the days option can provide a way to select many indexes at once.
1531              
1532             =item B<days>
1533              
1534             How many days backwards you want your operation to be relevant.
1535              
1536             =item B<datesep>
1537              
1538             Default is '.' Can be set to an empty string for no separator.
1539              
1540             =item B<pattern>
1541              
1542             A pattern to match the indexes. Can expand the following key words and characters:
1543              
1544             '*' expanded to '.*'
1545             'ANY' expanded to '.*'
1546             'DATE' expanded to a pattern to match a date,
1547              
1548             The indexes are compared against this pattern.
1549              
1550             =back
1551              
1552             =head1 HTTP Basic Authentication
1553              
1554             HTTP Basic Authorization is only supported when the C<proto> is set to B<https>
1555             as not to leak credentials all over.
1556              
1557             The username is selected by going through these mechanisms until one is found:
1558              
1559             --http-username
1560             'http-username' in /etc/es-utils.yml or ~/.es-utils.yml
1561             Netrc element matching the hostname of the request
1562             CLI::Helpers prompt()
1563              
1564             Once the username has been resolved, the following mechanisms are tried in order:
1565              
1566             Netrc element matching the hostname of the request
1567             Password executable defined by --password-exec
1568             'password-exec' in /etc/es-utils.yml, ~/.es-utils.yml
1569             CLI::Helpers prompt()
1570              
1571             =head2 Password Exec
1572              
1573             It is B<BAD> practice to specify passwords as a command line argument, or store it in a plaintext
1574             file. There are cases where this may be necessary, but it is not recommended. The best method for securing your
1575             password is to use the B<password-exec> option.
1576              
1577             This option must point to an executable script. That script will be passed two arguments, the hostname and the username
1578             for the request. It expects the password printed to STDOUT as the last line of output. Here's an example password-exec setup
1579             using Apple Keychain:
1580              
1581             #!/bin/sh
1582              
1583             HOSTNAME=$1;
1584             USERNAME=$2;
1585              
1586             /usr/bin/security find-generic-password -w -a "$USERNAME" -s "$HOSTNAME"
1587              
1588             If we save this to "$HOME/bin/get-passwd.sh" we can execute a script
1589             like this:
1590              
1591             $ es-search.pl --http-username bob --password-exec $HOME/bin/get-passwd.sh \
1592             --base secure-data --fields
1593              
1594             Though it's probably best to set this in your ~/.es-utils.yml file:
1595              
1596             ---
1597             host: secured-cluster.example.org
1598             port: 443
1599             proto: https
1600             http-username: bob
1601             password-exec: /home/bob/bin/get-passwd.sh
1602              
1603             =head3 CLI::Helpers and Password Prompting
1604              
1605             If all the fails to yield a password, the last resort is to use CLI::Helpers::prompt() to ask the user for their
1606             password. If the user is using version 1.1 or higher of CLI::Helpers, this call will turn off echo and readline magic
1607             for the password prompt.
1608              
1609             =head1 INSTALL
1610              
1611             B<This library attempts to provide scripts compatible with version 0.19 through 1.1 of ElasticSearch>.
1612              
1613             Recommended install with L<CPAN Minus|http://cpanmin.us>:
1614              
1615             cpanm App::ElasticSearch::Utilities
1616              
1617             You can also use CPAN:
1618              
1619             cpan App::ElasticSearch::Utilities
1620              
1621             Or if you'd prefer to manually install:
1622              
1623             export RELEASE=<CurrentRelease>
1624              
1625             wget "https://github.com/reyjrar/es-utils/blob/master/releases/App-ElasticSearch-Utilities-$RELEASE.tar.gz?raw=true" -O es-utils.tgz
1626              
1627             tar -zxvf es-utils.tgz
1628              
1629             cd App-ElasticSearch-Utilities-$RELEASE
1630              
1631             perl Makefile.PL
1632              
1633             make
1634              
1635             make install
1636              
1637             This will take care of ensuring all the dependencies are satisfied and will install the scripts into the same
1638             directory as your Perl executable.
1639              
1640             =head2 USAGE
1641              
1642             The tools are all wrapped in their own documentation, please see:
1643              
1644             $UTILITY --help
1645             $UTILITY --manual
1646              
1647             For individual options and capabilities
1648              
1649             =head2 PATTERNS
1650              
1651             Patterns are used to match an index to the aliases it should have. A few symbols are expanded into
1652             regular expressions. Those patterns are:
1653              
1654             * expands to match any number of any characters.
1655             DATE expands to match YYYY.MM.DD, YYYY-MM-DD, or YYYYMMDD
1656             ANY expands to match any number of any characters.
1657              
1658             =head1 AUTHOR
1659              
1660             Brad Lhotsky <brad@divisionbyzero.net>
1661              
1662             =head1 CONTRIBUTORS
1663              
1664             =for stopwords Alexey Shatlovsky Samit Badle Takumi Sakamoto Vitaly Shupak Surikov Andrei Grechkin Daniel Ostermeier Jason Rojas Kang-min Liu Lisa Hare Markus Linnala Matthew Feinberg Mohammad S Anwar
1665              
1666             =over 4
1667              
1668             =item *
1669              
1670             Alexey Shatlovsky <alexey.shatlovsky@booking.com>
1671              
1672             =item *
1673              
1674             Samit Badle <Samit.Badle@gmail.com>
1675              
1676             =item *
1677              
1678             Takumi Sakamoto <takumi.saka@gmail.com>
1679              
1680             =item *
1681              
1682             Vitaly Shupak <vitaly.shupak@deshaw.com>
1683              
1684             =item *
1685              
1686             Alexey Surikov <ksurent@gmail.com>
1687              
1688             =item *
1689              
1690             Andrei Grechkin <andrei.grechkin@booking.com>
1691              
1692             =item *
1693              
1694             Daniel Ostermeier <daniel.ostermeier@gmail.com>
1695              
1696             =item *
1697              
1698             Jason Rojas <jason.rojas@mgo.com>
1699              
1700             =item *
1701              
1702             Kang-min Liu <gugod@gugod.org>
1703              
1704             =item *
1705              
1706             Lisa Hare <lhare@inview.co.uk>
1707              
1708             =item *
1709              
1710             Markus Linnala <Markus.Linnala@cybercom.com>
1711              
1712             =item *
1713              
1714             Matthew Feinberg <mattf@intex.com>
1715              
1716             =item *
1717              
1718             Mohammad S Anwar <mohammad.anwar@yahoo.com>
1719              
1720             =back
1721              
1722             =for :stopwords cpan testmatrix url bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
1723              
1724             =head1 SUPPORT
1725              
1726             =head2 Websites
1727              
1728             The following websites have more information about this module, and may be of help to you. As always,
1729             in addition to those websites please use your favorite search engine to discover more resources.
1730              
1731             =over 4
1732              
1733             =item *
1734              
1735             MetaCPAN
1736              
1737             A modern, open-source CPAN search engine, useful to view POD in HTML format.
1738              
1739             L<https://metacpan.org/release/App-ElasticSearch-Utilities>
1740              
1741             =item *
1742              
1743             CPAN Testers
1744              
1745             The CPAN Testers is a network of smoke testers who run automated tests on uploaded CPAN distributions.
1746              
1747             L<http://www.cpantesters.org/distro/A/App-ElasticSearch-Utilities>
1748              
1749             =item *
1750              
1751             CPAN Testers Matrix
1752              
1753             The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.
1754              
1755             L<http://matrix.cpantesters.org/?dist=App-ElasticSearch-Utilities>
1756              
1757             =back
1758              
1759             =head2 Bugs / Feature Requests
1760              
1761             This module uses the GitHub Issue Tracker: L<https://github.com/reyjrar/es-utils/issues>
1762              
1763             =head2 Source Code
1764              
1765             This module's source code is available by visiting:
1766             L<https://github.com/reyjrar/es-utils>
1767              
1768             =head1 COPYRIGHT AND LICENSE
1769              
1770             This software is Copyright (c) 2023 by Brad Lhotsky.
1771              
1772             This is free software, licensed under:
1773              
1774             The (three-clause) BSD License
1775              
1776             =cut