File Coverage

blib/lib/App/ElasticSearch/Utilities.pm
Criterion Covered Total %
statement 142 503 28.2
branch 65 364 17.8
condition 3 127 2.3
subroutine 28 61 45.9
pod 32 32 100.0
total 270 1087 24.8


line stmt bran cond sub pod time code
1             # ABSTRACT: Utilities for Monitoring ElasticSearch
2             package App::ElasticSearch::Utilities;
3              
4 4     4   1448 use v5.16;
  4         29  
5 4     4   30 use warnings;
  4         10  
  4         182  
6              
7             our $VERSION = '8.6'; # VERSION
8              
9 4     4   1780 use App::ElasticSearch::Utilities::HTTPRequest;
  4         13  
  4         223  
10 4     4   2424 use CLI::Helpers qw(:all);
  4         380433  
  4         32  
11 4     4   974 use Getopt::Long qw(GetOptionsFromArray :config pass_through no_auto_abbrev);
  4         13  
  4         42  
12 4     4   2812 use Hash::Flatten qw(flatten);
  4         10011  
  4         261  
13 4     4   1831 use Hash::Merge::Simple qw(clone_merge);
  4         2204  
  4         240  
14 4     4   2197 use IPC::Run3;
  4         13794  
  4         223  
15 4     4   43 use JSON::MaybeXS;
  4         9  
  4         245  
16 4     4   2996 use LWP::UserAgent;
  4         121852  
  4         146  
17 4     4   2081 use Net::Netrc;
  4         18116  
  4         151  
18 4     4   53 use Ref::Util qw(is_ref is_arrayref is_hashref);
  4         10  
  4         275  
19 4     4   29 use Time::Local;
  4         10  
  4         220  
20 4     4   32 use URI;
  4         34  
  4         108  
21 4     4   1901 use URI::QueryParam;
  4         3324  
  4         153  
22 4     4   1824 use YAML::XS ();
  4         12097  
  4         511  
23              
24             # Control loading ARGV
25             my $ARGV_AT_INIT = 1;
26             my $COPY_ARGV = 0;
27             our $_init_complete = 0;
28              
29 4         61 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   2779 };
  4         46540  
75 4     4   6636 use App::ElasticSearch::Utilities::Connection;
  4         3717  
  4         185  
76 4     4   1839 use App::ElasticSearch::Utilities::VersionHacks qw(_fix_version_request);
  4         26  
  4         70  
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             );
91             my $PATTERN;
92              
93             {
94             ## no critic (ProhibitNoWarnings)
95 4     4   1516 no warnings;
  4         11  
  4         27986  
96             INIT {
97 4 50   4   59 return if $_init_complete++;
98 4 50       23 es_utils_initialize() if $ARGV_AT_INIT;
99             }
100             ## use critic
101             }
102              
103              
104             {
105             # Argument Parsing Block
106             my @argv_original = ();
107             my $parsed_argv = 0;
108             sub _parse_options {
109 4     4   12 my ($opt_ref) = @_;
110 4         40 my @opt_spec = qw(
111             local
112             host=s
113             port=i
114             timeout=i
115             keep-proxy
116             index=s
117             pattern=s
118             base|index-basename=s
119             days=i
120             noop!
121             datesep|date-separator=s
122             proto=s
123             http-username=s
124             password-exec=s
125             master-only|M
126             insecure
127             capath=s
128             cacert=s
129             cert=s
130             key=s
131             );
132              
133 4         11 my $argv;
134             my %opt;
135 4 50 33     26 if( defined $opt_ref && is_arrayref($opt_ref) ) {
136             # If passed an argv array, use that
137 0 0       0 $argv = $COPY_ARGV ? [ @{ $opt_ref } ] : $opt_ref;
  0         0  
138             }
139             else {
140             # Ensure multiple calls to cli_helpers_initialize() yield the same results
141 4 50       15 if ( $parsed_argv ) {
142             ## no critic
143 0         0 @ARGV = @argv_original;
144             ## use critic
145             }
146             else {
147 4         12 @argv_original = @ARGV;
148 4         9 $parsed_argv++;
149             }
150             # Operate on @ARGV
151 4 50       17 $argv = $COPY_ARGV ? [ @ARGV ] : \@ARGV;
152             }
153 4         36 GetOptionsFromArray($argv, \%opt, @opt_spec );
154 4         5356 return \%opt;
155             }
156             }
157              
158              
159             sub es_utils_initialize {
160 4     4 1 16 my ($argv) = @_;
161              
162             # Parse Options
163 4         38 my $opts = _parse_options($argv);
164              
165             # Config file locations
166 4         21 my @configs = (
167             '/etc/es-utils.yaml',
168             '/etc/es-utils.yml',
169             );
170 4 50       27 if( $ENV{HOME} ) {
171 4         14 push @configs, map { "$ENV{HOME}/.es-utils.$_" } qw( yaml yml );
  8         38  
172 4   33     37 my $xdg_config_home = $ENV{XDG_CONFIG_HOME} || "$ENV{HOME}/.config";
173 4         15 push @configs, map { "${xdg_config_home}/es-utils/config.$_" } qw( yaml yml );
  8         28  
174             }
175              
176 4         19 my @ConfigData=();
177 4         14 foreach my $config_file (@configs) {
178 24 50       397 next unless -f $config_file;
179 0         0 debug("Loading options from $config_file");
180             eval {
181 0         0 my $ref = YAML::XS::LoadFile($config_file);
182 0         0 push @ConfigData, $ref;
183 0         0 debug_var($ref);
184 0         0 1;
185 0 0       0 } or do {
186 0         0 debug({color=>"red"}, "[$config_file] $@");
187             };
188             }
189 4 50       29 %_GLOBALS = @ConfigData ? %{ clone_merge(@ConfigData) } : ();
  0         0  
190              
191             # Set defaults
192             %DEF = (
193             # Connection Options
194             HOST => exists $opts->{host} ? $opts->{host}
195             : exists $opts->{local} ? 'localhost'
196             : exists $_GLOBALS{host} ? $_GLOBALS{host}
197             : 'localhost',
198             PORT => exists $opts->{port} ? $opts->{port}
199             : exists $_GLOBALS{port} ? $_GLOBALS{port}
200             : 9200,
201             PROTO => exists $opts->{proto} ? $opts->{proto}
202             : exists $_GLOBALS{proto} ? $_GLOBALS{proto}
203             : 'http',
204             TIMEOUT => exists $opts->{timeout} ? $opts->{timeout}
205             : exists $_GLOBALS{timeout} ? $_GLOBALS{timeout}
206             : 10,
207             NOOP => exists $opts->{noop} ? $opts->{noop}
208             : exists $_GLOBALS{noop} ? $_GLOBALS{noop}
209             : undef,
210             NOPROXY => exists $opts->{'keep-proxy'} ? 0
211             : exists $_GLOBALS{'keep-proxy'} ? $_GLOBALS{'keep-proxy'}
212             : 1,
213             MASTERONLY => exists $opts->{'master-only'} ? $opts->{'master-only'} : 0,
214             # Index selection opts->ions
215             INDEX => exists $opts->{index} ? $opts->{index} : undef,
216             BASE => exists $opts->{base} ? lc $opts->{base}
217             : exists $_GLOBALS{base} ? $_GLOBALS{base}
218             : undef,
219             PATTERN => exists $opts->{pattern} ? $opts->{pattern} : '*',
220             DAYS => exists $opts->{days} ? $opts->{days}
221             : exists $_GLOBALS{days} ? $_GLOBALS{days} : 1,
222             DATESEP => exists $opts->{datesep} ? $opts->{datesep}
223             : exists $_GLOBALS{datesep} ? $_GLOBALS{datesep}
224             : exists $_GLOBALS{"date-separator"} ? $_GLOBALS{"date-separator"}
225             : '.',
226             # HTTP Basic Authentication
227             USERNAME => exists $opts->{'http-username'} ? $opts->{'http-username'}
228             : exists $_GLOBALS{'http-username'} ? $_GLOBALS{'http-username'}
229             : $ENV{USER},
230             PASSEXEC => exists $opts->{'password-exec'} ? $opts->{'password-exec'}
231             : exists $_GLOBALS{'password-exec'} ? $_GLOBALS{'password-exec'}
232             : undef,
233             # TLS Options
234             INSECURE => exists $opts->{insecure} ? 1
235             : exists $_GLOBALS{insecure} ? $_GLOBALS{insecure}
236             : 0,
237             CACERT => exists $opts->{cacert} ? 1
238             : exists $_GLOBALS{cacert} ? $_GLOBALS{cacert}
239             : undef,
240             CAPATH => exists $opts->{capath} ? 1
241             : exists $_GLOBALS{capath} ? $_GLOBALS{capath}
242             : undef,
243             CERT => exists $opts->{cert} ? 1
244             : exists $_GLOBALS{cert} ? $_GLOBALS{cert}
245             : undef,
246             KEY => exists $opts->{key} ? 1
247             : exists $_GLOBALS{key} ? $_GLOBALS{key}
248             : undef,
249 4 50       275 );
    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          
    50          
    50          
    50          
250 4 50       24 CLI::Helpers::override(verbose => 1) if $DEF{NOOP};
251              
252 4 50       18 if( $DEF{NOPROXY} ) {
253 4         23 debug("Removing any active HTTP Proxies from ENV.");
254 4         69476 delete $ENV{$_} for qw(http_proxy HTTP_PROXY);
255             }
256              
257             # Setup Variables based on the config
258             %PATTERN_REGEX = (
259 4         159 '*' => qr/.*/,
260             DATE => qr/\d{4}(?:\Q$DEF{DATESEP}\E)?\d{2}(?:\Q$DEF{DATESEP}\E)?\d{2}/,
261             ANY => qr/.*/,
262             );
263 4         22 my @ordered = qw(* DATE ANY);
264              
265 4 50       43 if( index($DEF{DATESEP},'-') >= 0 ) {
266 0         0 output({stderr=>1,color=>'yellow'}, "=== Using a '-' as your date separator may cause problems with other utilities. ===");
267             }
268              
269             # Build the Index Pattern
270 4         13 $PATTERN = $DEF{PATTERN};
271 4         13 foreach my $literal ( @ordered ) {
272 12         152 $PATTERN =~ s/\Q$literal\E/$PATTERN_REGEX{$literal}/g;
273             }
274              
275             }
276              
277             # Regexes for Pattern Expansion
278             our $CURRENT_VERSION;
279             my $CLUSTER_MASTER;
280              
281              
282             sub es_globals {
283 1     1 1 4 my ($key) = @_;
284              
285 1 50       7 es_utils_initialize() unless keys %DEF;
286              
287 1 50       6 return unless exists $_GLOBALS{$key};
288 0         0 return $_GLOBALS{$key};
289             }
290              
291              
292             my %_auth_cache = ();
293              
294             sub es_basic_auth {
295 0     0 1 0 my ($host) = @_;
296              
297 0 0       0 es_utils_initialize() unless keys %DEF;
298              
299 0   0     0 $host ||= $DEF{HOST};
300              
301             # Return the results if we've done this already
302 0         0 return @{ $_auth_cache{$host} }{qw(username password)}
303 0 0       0 if exists $_auth_cache{$host};
304              
305             # Set the cached element
306 0         0 my %auth = ();
307              
308             # Lookup the details netrc
309 0         0 my $netrc = Net::Netrc->lookup($host);
310 0 0       0 if( $DEF{HOST} eq $host ) {
311 0         0 %auth = map { lc($_) => $DEF{$_} } qw(USERNAME);
  0         0  
312             }
313 0         0 my %meta = ();
314 0         0 foreach my $k (qw( http-username password-exec )) {
315 0         0 foreach my $name ( $DEF{INDEX}, $DEF{BASE} ) {
316 0 0       0 next unless $name;
317 0 0       0 if( my $v = es_local_index_meta($k, $name) ) {
318 0         0 $meta{$k} = $v;
319 0         0 last;
320             }
321             }
322             }
323              
324             # Get the Username
325             $auth{username} ||= $meta{'http-username'} ? $meta{'http-username'}
326             : defined $DEF{USERNAME} ? $DEF{USERNAME}
327             : defined $netrc ? $netrc->login
328 0 0 0     0 : $ENV{USER};
    0          
    0          
329              
330             # Prompt for the password
331             $auth{password} ||= defined $netrc ? $netrc->password
332             : (es_pass_exec($host,$auth{username},$meta{'password-exec'})
333 0 0 0     0 || prompt(sprintf "Password for '%s' at '%s': ", $auth{username}, $host)
      0        
334             );
335              
336             # Store
337 0         0 $_auth_cache{$host} = \%auth;
338 0         0 return @auth{qw(username password)};
339             }
340              
341              
342             sub es_pass_exec {
343 0     0 1 0 my ($host,$username,$exec) = @_;
344              
345 0 0       0 es_utils_initialize() unless keys %DEF;
346              
347             # Simplest case we can't run
348 0   0     0 $exec ||= $DEF{PASSEXEC};
349 0 0 0     0 return unless length $exec && -x $exec;
350              
351 0         0 my(@out,@err);
352             # Run the password command captue out, error and RC
353 0         0 run3 [ $exec, $host, $username ], \undef, \@out, \@err;
354 0         0 my $rc = $?;
355              
356             # Record the error
357 0 0 0     0 if( @err or $rc != 0 ) {
358 0         0 output({color=>'red',stderr=>1},
359             sprintf("es_pass_exec() called '%s' and met with an error code '%d'", $exec, $rc),
360             @err
361             );
362 0         0 return;
363             }
364              
365             # Format and return the result
366 0         0 my $passwd = $out[-1];
367 0         0 chomp($passwd);
368 0         0 return $passwd;
369             }
370              
371              
372              
373             sub es_pattern {
374 0 0   0 1 0 es_utils_initialize() unless keys %DEF;
375             return {
376             re => $PATTERN,
377             string => $DEF{PATTERN},
378 0         0 };
379             }
380              
381             sub _get_ssl_opts {
382 0 0   0   0 es_utils_initialize() unless keys %DEF;
383 0         0 my %opts = ();
384 0 0       0 $opts{verify_hostname} = 0 if $DEF{INSECURE};
385 0 0       0 $opts{SSL_ca_file} = $DEF{CACERT} if $DEF{CACERT};
386 0 0       0 $opts{SSL_ca_path} = $DEF{CAPATH} if $DEF{CAPATH};
387 0 0       0 $opts{SSL_cert_file} = $DEF{CERT} if $DEF{CERT};
388 0 0       0 $opts{SSL_key_file} = $DEF{KEY} if $DEF{KEY};
389 0         0 return \%opts;
390             }
391              
392             sub _get_es_version {
393 0 0   0   0 return $CURRENT_VERSION if defined $CURRENT_VERSION;
394 0         0 my $conn = es_connect();
395             # Build the request
396 0         0 my $req = App::ElasticSearch::Utilities::HTTPRequest->new(
397             GET => sprintf "%s://%s:%d",
398             $conn->proto, $conn->host, $conn->port
399             );
400             # Check if we're doing auth
401 0 0       0 my @auth = $DEF{PASSEXEC} ? es_basic_auth($conn->host) : ();
402             # Add authentication if we get a password
403 0 0       0 $req->authorization_basic( @auth ) if @auth;
404              
405             # Retry with TLS and/or Auth
406 0         0 my %try = map { $_ => 1 } qw( tls auth );
  0         0  
407 0         0 my $resp;
408 0         0 while( not defined $CURRENT_VERSION ) {
409 0         0 $resp = $conn->ua->request($req);
410 0 0 0     0 if( $resp->is_success ) {
    0          
    0          
411 0         0 my $ver;
412 0         0 eval {
413 0         0 $ver = $resp->content->{version};
414             };
415 0 0       0 if( $ver ) {
416 0 0 0     0 if( $ver->{distribution} and $ver->{distribution} eq 'opensearch' ) {
417 0         0 $CURRENT_VERSION = '7.10';
418             }
419             else {
420 0         0 $CURRENT_VERSION = join('.', (split /\./,$ver->{number})[0,1]);
421             }
422             }
423             }
424             elsif( $resp->code == 500 && $resp->message eq "Server closed connection without sending any data back" ) {
425             # Try TLS
426 0 0       0 last unless $try{tls};
427 0         0 delete $try{tls};
428 0         0 $conn->proto('https');
429 0         0 warn "Attempting promotion to HTTPS, try setting 'proto: https' in ~/.es-utils.yaml";
430             }
431             elsif( $resp->code == 401 ) {
432             # Retry with credentials
433 0 0       0 last unless $try{auth};
434 0         0 delete $try{auth};
435             warn "Authorization required, try setting 'password-exec: /home/user/bin/get-password.sh` in ~/.es-utils.yaml'"
436 0 0       0 unless $DEF{PASSEXEC};
437 0         0 $req->authorization_basic( es_basic_auth($conn->host) );
438             }
439             else {
440 0         0 warn "Failed getting version";
441 0         0 last;
442             }
443             }
444 0 0 0     0 if( !defined $CURRENT_VERSION || $CURRENT_VERSION <= 2 ) {
445 0         0 output({color=>'red',stderr=>1}, sprintf "[%d] Unable to determine Elasticsearch version, something has gone terribly wrong: aborting.", $resp->code);
446 0 0       0 output({color=>'red',stderr=>1}, ref $resp->content ? YAML::XS::Dump($resp->content) : $resp->content) if $resp->content;
    0          
447 0         0 exit 1;
448             }
449 0         0 debug({color=>'magenta'}, "FOUND VERISON '$CURRENT_VERSION'");
450 0         0 return $CURRENT_VERSION;
451             }
452              
453              
454             my $ES = undef;
455              
456             sub es_connect {
457 0     0 1 0 my ($override_servers) = @_;
458              
459 0 0       0 es_utils_initialize() unless keys %DEF;
460              
461             my %conn = (
462             host => $DEF{HOST},
463             port => $DEF{PORT},
464             proto => $DEF{PROTO},
465             timeout => $DEF{TIMEOUT},
466 0         0 ssl_opts => _get_ssl_opts,
467             );
468             # Only authenticate over TLS
469 0 0       0 if( $DEF{PROTO} eq 'https' ) {
470 0         0 $conn{username} = $DEF{USERNAME};
471 0 0       0 $conn{password} = es_pass_exec(@DEF{qw(HOST USERNAME)}) if $DEF{PASSEXEC};
472             }
473              
474             # If we're overriding, return a unique handle
475 0 0       0 if(defined $override_servers) {
476 0 0       0 my @overrides = is_arrayref($override_servers) ? @$override_servers : $override_servers;
477 0         0 my @servers;
478 0         0 foreach my $entry ( @overrides ) {
479 0         0 my ($s,$p) = split /\:/, $entry;
480 0   0     0 $p ||= $conn{port};
481 0         0 push @servers, { %conn, host => $s, port => $p };
482             }
483              
484 0 0       0 if( @servers > 0 ) {
485 0 0       0 my $pick = @servers > 1 ? $servers[int(rand(@servers))] : $servers[0];
486 0         0 return App::ElasticSearch::Utilities::Connection->new(%{$pick});
  0         0  
487             }
488             }
489             else {
490             # Check for index metadata
491 0         0 foreach my $k ( keys %conn ) {
492 0         0 foreach my $name ( $DEF{INDEX}, $DEF{BASE} ) {
493 0 0       0 next unless $name;
494 0 0       0 if( my $v = es_local_index_meta($k => $name) ) {
495 0         0 $conn{$k} = $v;
496 0         0 last;
497             }
498             }
499             }
500             }
501              
502             # Otherwise, cache our handle
503 0   0     0 $ES ||= App::ElasticSearch::Utilities::Connection->new(%conn);
504              
505 0         0 return $ES;
506             }
507              
508              
509             sub es_master {
510 0     0 1 0 my ($instance) = @_;
511 0 0 0     0 if(!defined $instance && defined $CLUSTER_MASTER) {
512 0         0 return $CLUSTER_MASTER;
513             }
514 0         0 my $is_master = 0;
515 0         0 my @request = ('/_cluster/state/master_node');
516 0 0       0 unshift @request, $instance if defined $instance;
517              
518 0         0 my $cluster = es_request(@request);
519 0 0 0     0 if( defined $cluster && $cluster->{master_node} ) {
520 0         0 my $local = es_request('/_nodes/_local');
521 0 0 0     0 if ($local->{nodes} && $local->{nodes}{$cluster->{master_node}}) {
522 0         0 $is_master = 1;
523             }
524             }
525 0 0       0 $CLUSTER_MASTER = $is_master unless defined $instance;
526 0         0 return $is_master;
527             }
528              
529              
530             sub es_request {
531 0 0   0 1 0 my $instance = ref $_[0] eq 'App::ElasticSearch::Utilities::Connection' ? shift @_ : es_connect();
532              
533 0 0       0 $CURRENT_VERSION = _get_es_version() if !defined $CURRENT_VERSION;
534              
535 0         0 my($url,$options,$body) = _fix_version_request(@_);
536              
537             # Normalize the options
538 0   0     0 $options->{method} ||= 'GET';
539 0         0 $options->{command} = $url;
540 0         0 my $index;
541              
542 0 0       0 if( exists $options->{index} ) {
543 0 0       0 if( my $index_in = delete $options->{index} ) {
544             # No need to validate _all
545 0 0       0 if( $index_in eq '_all') {
546 0         0 $index = $index_in;
547             }
548             else {
549             # Validate each included index
550 0 0       0 my @indexes = is_arrayref($index_in) ? @{ $index_in } : split /\,/, $index_in;
  0         0  
551 0         0 $index = join(',', @indexes);
552             }
553             }
554             }
555              
556             # For the cat api, index goes *after* the command
557 0 0 0     0 if( $url =~ /^_(cat|stats)/ && $index ) {
    0          
558 0         0 $url =~ s/\/$//;
559 0         0 $url = join('/', $url, $index);
560 0         0 delete $options->{command};
561             }
562             elsif( $index ) {
563 0         0 $options->{index} = $index;
564             }
565             else {
566 0         0 $index = '';
567             }
568              
569             # Figure out if we're modifying things
570             my $modification = $url eq '_search' && $options->{method} eq 'POST' ? 0
571 0 0 0     0 : $options->{method} ne 'GET';
572              
573 0 0       0 if($modification) {
574             # Set NOOP if necessary
575 0 0 0     0 if(!$DEF{NOOP} && $DEF{MASTERONLY}) {
576 0 0       0 if( !es_master() ) {
577 0         0 $DEF{NOOP} = 1;
578             }
579             }
580              
581             # Check for noop
582 0 0       0 if( $DEF{NOOP} ) {
583 0 0 0     0 my $flag = $DEF{MASTERONLY} && !es_master() ? '--master-only' : '--noop';
584 0         0 output({color=>'cyan'}, "Called es_request($index/$options->{command}), but $flag set and method is $options->{method}");
585 0         0 return;
586             }
587             }
588              
589             # Make the request
590 0         0 my $resp = $instance->request($url,$options,$body);
591              
592             # Check the response is defined, bail if it's not
593 0 0       0 die "Unsupported request method: $options->{method}" unless defined $resp;
594              
595             # Logging
596             verbose({color=>'yellow'}, sprintf "es_request(%s/%s) returned HTTP Status %s",
597 0 0       0 $index, $options->{command}, $resp->message,
598             ) if $resp->code != 200;
599              
600             # Error handling
601 0 0 0     0 if( !$resp->is_success ) {
    0          
602 0         0 my $msg;
603             eval {
604 0         0 my @causes = ();
605 0         0 foreach my $cause ( @{ $resp->content->{error}{root_cause} } ) {
  0         0  
606 0 0       0 push @causes, $cause->{index} ? "$cause->{index}: $cause->{reason}" : $cause->{reason};
607             }
608 0         0 $msg = join("\n", map { "\t$_" } @causes);
  0         0  
609 0         0 1;
610 0 0       0 } or do {
611             # Default to the message, though it's usually unhelpful
612 0         0 $msg = $resp->{message};
613             };
614             die sprintf "es_request(%s/%s) failed[%d]:\n%s",
615 0   0     0 $index, $options->{command}, $resp->code, $msg || 'missing error message';
616              
617             } elsif( !defined $resp->content || ( !is_ref($resp->content) && !length $resp->content )) {
618             output({color=>'yellow',stderr=>1},
619             sprintf "es_request(%s/%s) empty response[%d]: %s",
620 0         0 $index, $options->{command}, $resp->code, $resp->message
621             );
622             }
623              
624 0         0 return $resp->content;
625             }
626              
627              
628              
629             my %_nodes;
630             sub es_nodes {
631 0 0   0 1 0 if(!keys %_nodes) {
632 0         0 my $res = es_request('_cluster/state/nodes', {});
633 0 0       0 if( !defined $res ) {
634 0         0 output({color=>"red"}, "es_nodes(): Unable to locate nodes in status!");
635 0         0 exit 1;
636             }
637 0         0 foreach my $id ( keys %{ $res->{nodes} } ) {
  0         0  
638 0         0 $_nodes{$id} = $res->{nodes}{$id}{name};
639             }
640             }
641              
642 0 0       0 return wantarray ? %_nodes : { %_nodes };
643             }
644              
645              
646             my $_indices_meta;
647             sub es_indices_meta {
648 0 0   0 1 0 if(!defined $_indices_meta) {
649 0         0 my $result = es_request('_cluster/state/metadata');
650 0 0       0 if ( !defined $result ) {
651 0         0 output({stderr=>1,color=>"red"}, "es_indices_meta(): Unable to locate indices in status!");
652 0         0 exit 1;
653             }
654 0         0 $_indices_meta = $result->{metadata}{indices};
655             }
656              
657 0         0 my %copy = %{ $_indices_meta };
  0         0  
658 0 0       0 return wantarray ? %copy : \%copy;
659             }
660              
661              
662             my %_valid_index = ();
663             sub es_indices {
664 0     0 1 0 my %args = (
665             state => 'open',
666             check_state => 1,
667             check_dates => 1,
668             @_
669             );
670              
671 0 0       0 es_utils_initialize() unless keys %DEF;
672              
673             # Seriously, English? Do you speak it motherfucker?
674 0 0       0 $args{state} = 'close' if $args{state} eq 'closed';
675              
676 0         0 my @indices = ();
677 0         0 my %idx = ();
678 0 0 0     0 my $wildcard = !exists $args{_all} && defined $DEF{BASE} ? sprintf "/*%s*", $DEF{BASE} : '';
679              
680             # Simplest case, single index
681 0 0 0     0 if( defined $DEF{INDEX} ) {
    0 0        
682 0         0 push @indices, $DEF{INDEX};
683             }
684             # Next simplest case, open indexes
685             elsif( !exists $args{_all} && $args{check_state} && $args{state} eq 'open' ) {
686             # Use _stats because it's break neck fast
687 0 0       0 if( my $res = es_request($wildcard . '/_stats/docs') ) {
688 0         0 foreach my $idx ( keys %{ $res->{indices} } ) {
  0         0  
689 0         0 $idx{$idx} = 'open';
690             }
691             }
692             }
693             else {
694 0         0 my $res = es_request('_cat/indices' . $wildcard, { uri_param => { h => 'index,status' } });
695 0         0 foreach my $entry (@{ $res }) {
  0         0  
696 0 0       0 my ($index,$status) = is_hashref($entry) ? @{ $entry }{qw(index status)} : split /\s+/, $entry;
  0         0  
697 0         0 $idx{$index} = $status;
698             }
699             }
700              
701 0         0 foreach my $index (sort keys %idx) {
702 0 0       0 if(!exists $args{_all}) {
703 0         0 my $status = $idx{$index};
704             # State Check Disqualification
705 0 0 0     0 if($args{state} ne 'all' && $args{check_state}) {
706 0         0 my $result = $status eq $args{state};
707 0 0       0 next unless $result;
708             }
709              
710 0         0 my $p = es_pattern();
711 0 0       0 next unless $index =~ /$p->{re}/;
712 0         0 debug({indent=>2},"= name checks succeeded");
713              
714 0 0 0     0 if ($args{older} && defined $DEF{DAYS}) {
    0 0        
715 0         0 my $days_old = es_index_days_old( $index );
716 0 0 0     0 if (!defined $days_old || $days_old < $DEF{DAYS}) {
717 0         0 next;
718             }
719             }
720             elsif( $args{check_dates} && defined $DEF{DAYS} ) {
721              
722 0         0 my $days_old = es_index_days_old( $index );
723 0 0 0     0 if( !defined $days_old ) {
    0          
724 0         0 debug({indent=>2,color=>'red'}, "! error locating date in string, skipping !");
725 0         0 next;
726             }
727             elsif( $DEF{DAYS} >= 0 && $days_old >= $DEF{DAYS} ) {
728 0         0 next;
729             }
730             }
731             }
732             else {
733 0         0 debug({indent=>1}, "Called with _all, all checks skipped.");
734             }
735 0         0 debug({indent=>1,color=>"green"}, "+ match!");
736 0         0 push @indices, $index;
737             }
738              
739             # We retrieved these from the cluster, so preserve them here.
740 0         0 $_valid_index{$_} = 1 for @indices;
741              
742 0 0       0 return wantarray ? @indices : \@indices;
743             }
744              
745              
746             sub es_index_strip_date {
747 48     48 1 101 my ($index) = @_;
748              
749 48 50       103 return -1 unless defined $index;
750              
751 48 50       102 es_utils_initialize() unless keys %DEF;
752              
753             # Try the Date Pattern
754 48 50       278 if( $index =~ s/[-_]$PATTERN_REGEX{DATE}.*//o ) {
    0          
755 48         148 return $index;
756             }
757             # Fallback to matching thing-YYYY-MM-DD or thing-YYYY.MM.DD
758             elsif( $index =~ s/[-_]\d{4}([.-])\d{2}\g{1}\d{2}(?:[-_.]\d+)?$// ) {
759 0         0 return $index;
760             }
761 0         0 return;
762             }
763              
764              
765             my %_stripped=();
766              
767             sub es_index_bases {
768 24     24 1 26243 my ($index) = @_;
769              
770 24 50       69 return unless defined $index;
771              
772             # Strip to the base
773 24         47 my $stripped = es_index_strip_date($index);
774 24 50 33     106 return unless defined $stripped and length $stripped;
775              
776             # Compute if we haven't already memoized
777 24 100       54 if( !exists $_stripped{$stripped} ) {
778 3         7 my %bases=();
779 3 50       13 my @parts = grep { defined && length } split /[-_]/, $stripped;
  5         23  
780 3         27 debug(sprintf "es_index_bases(%s) dissected to %s", $index, join(',', @parts));
781 3 100       56 my $sep = index( $stripped, '_' ) >= 0 ? '_' : '-';
782              
783 3         9 my %collected = ();
784 3         12 foreach my $end ( 0..$#parts ) {
785 5         16 my $name = join($sep, @parts[0..$end]);
786 5         16 $collected{$name} = 1;
787             }
788 3         18 $_stripped{$stripped} = [ sort keys %collected ]
789             }
790              
791 24         34 return @{ $_stripped{$stripped} };
  24         98  
792             }
793              
794              
795             my $NOW = timegm(0,0,0,(gmtime)[3,4,5]);
796             sub es_index_days_old {
797 24     24 1 60 my ($index) = @_;
798              
799 24 50       52 return unless defined $index;
800              
801 24 50       56 es_utils_initialize() unless keys %DEF;
802              
803 24 50       188 if( my ($dateStr) = ($index =~ /($PATTERN_REGEX{DATE})/) ) {
804 24         52 my @date=();
805 24 50       54 if(length $DEF{DATESEP}) {
806 24         105 @date = reverse map { int } split /\Q$DEF{DATESEP}\E/, $dateStr;
  72         178  
807             }
808             else {
809 0         0 for my $len (qw(4 2 2)) {
810 0         0 unshift @date, substr($dateStr,0,$len,'');
811             }
812             }
813 24         63 $date[1]--; # move 1-12 -> 0-11
814 24         38 my $idx_time = eval { timegm( 0,0,0, @date ) };
  24         89  
815 24 50       807 return unless $idx_time;
816 24         40 my $diff = $NOW - $idx_time;
817 24         37 $diff++; # Add one second
818 24         287 debug({color=>"yellow"}, sprintf "es_index_days_old(%s) - Time difference is %0.3f", $index, $diff/86400);
819 24         326 return int($diff / 86400);
820             }
821 0         0 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 1476 my $hash = shift;
1046 2         14 my $_flat = flatten($hash, { HashDelimiter=>':', ArrayDelimiter=>':' });
1047 2         728 my %compat = map { s/:/./gr => $_flat->{$_} } keys %{ $_flat };
  6         21  
  2         8  
1048 2         10 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.6
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             --datesep Date separator, default '.' also (--date-separator)
1412             --pattern Use a pattern to operate on the indexes
1413             --days If using a pattern or base, how many days back to go, default: 1
1414              
1415             See also the "CONNECTION ARGUMENTS" and "INDEX SELECTION ARGUMENTS" sections from App::ElasticSearch::Utilities.
1416              
1417             =head1 ARGUMENT GLOBALS
1418              
1419             Some options may be specified in the B</etc/es-utils.yaml>, B<$HOME/.es-utils.yaml>
1420             or B<$HOME/.config/es-utils/config.yaml> file:
1421              
1422             ---
1423             base: logstash
1424             days: 7
1425             host: esproxy.example.com
1426             port: 80
1427             timeout: 10
1428             proto: https
1429             http-username: bob
1430             password-exec: /home/bob/bin/get-es-passwd.sh
1431              
1432             =head1 CONNECTION ARGUMENTS
1433              
1434             Arguments for establishing a connection with the cluster. Unless specified otherwise, these options
1435             can all be set in the globals file.
1436              
1437             =over
1438              
1439             =item B<local>
1440              
1441             Assume ElasticSearch is running locally, connect to localhost.
1442              
1443             =item B<host>
1444              
1445             Use a different hostname or IP address to connect.
1446              
1447             =item B<port>
1448              
1449             Defaults to 9200.
1450              
1451             =item B<proto>
1452              
1453             Defaults to 'http', can also be 'https'.
1454              
1455             =item B<http-username>
1456              
1457             If HTTP Basic Authentication is required, use this username.
1458              
1459             See also the L<HTTP Basic Authentication> section for more details
1460              
1461             =item B<password-exec>
1462              
1463             If HTTP Basic Authentication is required, run this command, passing the arguments:
1464              
1465             <command_to_run> <es_host> <es_username>
1466              
1467             The script expects the last line to contain the password in plaintext.
1468              
1469             =item B<noop>
1470              
1471             Prevents any communication to the cluster from making changes to the settings or data contained therein.
1472             In short, it prevents anything but HEAD and GET requests, B<except> POST requests to the _search endpoint.
1473              
1474             =item B<timeout>
1475              
1476             Timeout for connections and requests, defaults to 10.
1477              
1478             =item B<keep-proxy>
1479              
1480             By default, HTTP proxy environment variables are stripped. Use this option to keep your proxy environment variables
1481             in tact.
1482              
1483             =item B<insecure>
1484              
1485             Don't verify TLS certificates
1486              
1487             =item B<cacert>
1488              
1489             Specify a file with the TLS CA certificates.
1490              
1491             =item B<capath>
1492              
1493             Specify a directory containing the TLS CA certificates.
1494              
1495             =item B<cert>
1496              
1497             Specify the path to the TLS client certificate file..
1498              
1499             =item B<key>
1500              
1501             Specify the path to the TLS client private key file.
1502              
1503             =back
1504              
1505             =head1 INDEX SELECTION ARGUMENTS
1506              
1507             =over
1508              
1509             =item B<base>
1510              
1511             In an environment using monthly, weekly, daily, or hourly indexes. The base index name is everything without the date.
1512             Parsing for bases, also provides splitting and matching on segments of the index name delineated by the '-' character.
1513             If we have the following indexes:
1514              
1515             web-dc1-YYYY.MM.DD
1516             web-dc2-YYYY.MM.DD
1517             logstash-dc1-YYYY.MM.DD
1518             logstash-dc2-YYYY.MM.DD
1519              
1520             Valid bases would be:
1521              
1522             web
1523             web-dc1
1524             web-dc2
1525             logstash
1526             logstash-dc1
1527             logstash-dc2
1528             dc1
1529             dc2
1530              
1531             Combining that with the days option can provide a way to select many indexes at once.
1532              
1533             =item B<days>
1534              
1535             How many days backwards you want your operation to be relevant.
1536              
1537             =item B<datesep>
1538              
1539             Default is '.' Can be set to an empty string for no separator.
1540              
1541             =item B<pattern>
1542              
1543             A pattern to match the indexes. Can expand the following key words and characters:
1544              
1545             '*' expanded to '.*'
1546             'ANY' expanded to '.*'
1547             'DATE' expanded to a pattern to match a date,
1548              
1549             The indexes are compared against this pattern.
1550              
1551             =back
1552              
1553             =head1 HTTP Basic Authentication
1554              
1555             HTTP Basic Authorization is only supported when the C<proto> is set to B<https>
1556             as not to leak credentials all over.
1557              
1558             The username is selected by going through these mechanisms until one is found:
1559              
1560             --http-username
1561             'http-username' in /etc/es-utils.yml or ~/.es-utils.yml
1562             Netrc element matching the hostname of the request
1563             CLI::Helpers prompt()
1564              
1565             Once the username has been resolved, the following mechanisms are tried in order:
1566              
1567             Netrc element matching the hostname of the request
1568             Password executable defined by --password-exec
1569             'password-exec' in /etc/es-utils.yml, ~/.es-utils.yml
1570             CLI::Helpers prompt()
1571              
1572             =head2 Password Exec
1573              
1574             It is B<BAD> practice to specify passwords as a command line argument, or store it in a plaintext
1575             file. There are cases where this may be necessary, but it is not recommended. The best method for securing your
1576             password is to use the B<password-exec> option.
1577              
1578             This option must point to an executable script. That script will be passed two arguments, the hostname and the username
1579             for the request. It expects the password printed to STDOUT as the last line of output. Here's an example password-exec setup
1580             using Apple Keychain:
1581              
1582             #!/bin/sh
1583              
1584             HOSTNAME=$1;
1585             USERNAME=$2;
1586              
1587             /usr/bin/security find-generic-password -w -a "$USERNAME" -s "$HOSTNAME"
1588              
1589             If we save this to "$HOME/bin/get-passwd.sh" we can execute a script
1590             like this:
1591              
1592             $ es-search.pl --http-username bob --password-exec $HOME/bin/get-passwd.sh \
1593             --base secure-data --fields
1594              
1595             Though it's probably best to set this in your ~/.es-utils.yml file:
1596              
1597             ---
1598             host: secured-cluster.example.org
1599             port: 443
1600             proto: https
1601             http-username: bob
1602             password-exec: /home/bob/bin/get-passwd.sh
1603              
1604             =head3 CLI::Helpers and Password Prompting
1605              
1606             If all the fails to yield a password, the last resort is to use CLI::Helpers::prompt() to ask the user for their
1607             password. If the user is using version 1.1 or higher of CLI::Helpers, this call will turn off echo and readline magic
1608             for the password prompt.
1609              
1610             =head1 INSTALL
1611              
1612             B<This library attempts to provide scripts compatible with version 0.19 through 1.1 of ElasticSearch>.
1613              
1614             Recommended install with L<CPAN Minus|http://cpanmin.us>:
1615              
1616             cpanm App::ElasticSearch::Utilities
1617              
1618             You can also use CPAN:
1619              
1620             cpan App::ElasticSearch::Utilities
1621              
1622             Or if you'd prefer to manually install:
1623              
1624             export RELEASE=<CurrentRelease>
1625              
1626             wget "https://github.com/reyjrar/es-utils/blob/master/releases/App-ElasticSearch-Utilities-$RELEASE.tar.gz?raw=true" -O es-utils.tgz
1627              
1628             tar -zxvf es-utils.tgz
1629              
1630             cd App-ElasticSearch-Utilities-$RELEASE
1631              
1632             perl Makefile.PL
1633              
1634             make
1635              
1636             make install
1637              
1638             This will take care of ensuring all the dependencies are satisfied and will install the scripts into the same
1639             directory as your Perl executable.
1640              
1641             =head2 USAGE
1642              
1643             The tools are all wrapped in their own documentation, please see:
1644              
1645             $UTILITY --help
1646             $UTILITY --manual
1647              
1648             For individual options and capabilities
1649              
1650             =head2 PATTERNS
1651              
1652             Patterns are used to match an index to the aliases it should have. A few symbols are expanded into
1653             regular expressions. Those patterns are:
1654              
1655             * expands to match any number of any characters.
1656             DATE expands to match YYYY.MM.DD, YYYY-MM-DD, or YYYYMMDD
1657             ANY expands to match any number of any characters.
1658              
1659             =head1 AUTHOR
1660              
1661             Brad Lhotsky <brad@divisionbyzero.net>
1662              
1663             =head1 CONTRIBUTORS
1664              
1665             =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
1666              
1667             =over 4
1668              
1669             =item *
1670              
1671             Alexey Shatlovsky <alexey.shatlovsky@booking.com>
1672              
1673             =item *
1674              
1675             Samit Badle <Samit.Badle@gmail.com>
1676              
1677             =item *
1678              
1679             Takumi Sakamoto <takumi.saka@gmail.com>
1680              
1681             =item *
1682              
1683             Vitaly Shupak <vitaly.shupak@deshaw.com>
1684              
1685             =item *
1686              
1687             Alexey Surikov <ksurent@gmail.com>
1688              
1689             =item *
1690              
1691             Andrei Grechkin <andrei.grechkin@booking.com>
1692              
1693             =item *
1694              
1695             Daniel Ostermeier <daniel.ostermeier@gmail.com>
1696              
1697             =item *
1698              
1699             Jason Rojas <jason.rojas@mgo.com>
1700              
1701             =item *
1702              
1703             Kang-min Liu <gugod@gugod.org>
1704              
1705             =item *
1706              
1707             Lisa Hare <lhare@inview.co.uk>
1708              
1709             =item *
1710              
1711             Markus Linnala <Markus.Linnala@cybercom.com>
1712              
1713             =item *
1714              
1715             Matthew Feinberg <mattf@intex.com>
1716              
1717             =item *
1718              
1719             Mohammad S Anwar <mohammad.anwar@yahoo.com>
1720              
1721             =back
1722              
1723             =for :stopwords cpan testmatrix url bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
1724              
1725             =head1 SUPPORT
1726              
1727             =head2 Websites
1728              
1729             The following websites have more information about this module, and may be of help to you. As always,
1730             in addition to those websites please use your favorite search engine to discover more resources.
1731              
1732             =over 4
1733              
1734             =item *
1735              
1736             MetaCPAN
1737              
1738             A modern, open-source CPAN search engine, useful to view POD in HTML format.
1739              
1740             L<https://metacpan.org/release/App-ElasticSearch-Utilities>
1741              
1742             =item *
1743              
1744             CPAN Testers
1745              
1746             The CPAN Testers is a network of smoke testers who run automated tests on uploaded CPAN distributions.
1747              
1748             L<http://www.cpantesters.org/distro/A/App-ElasticSearch-Utilities>
1749              
1750             =item *
1751              
1752             CPAN Testers Matrix
1753              
1754             The CPAN Testers Matrix is a website that provides a visual overview of the test results for a distribution on various Perls/platforms.
1755              
1756             L<http://matrix.cpantesters.org/?dist=App-ElasticSearch-Utilities>
1757              
1758             =back
1759              
1760             =head2 Bugs / Feature Requests
1761              
1762             This module uses the GitHub Issue Tracker: L<https://github.com/reyjrar/es-utils/issues>
1763              
1764             =head2 Source Code
1765              
1766             This module's source code is available by visiting:
1767             L<https://github.com/reyjrar/es-utils>
1768              
1769             =head1 COPYRIGHT AND LICENSE
1770              
1771             This software is Copyright (c) 2023 by Brad Lhotsky.
1772              
1773             This is free software, licensed under:
1774              
1775             The (three-clause) BSD License
1776              
1777             =cut