-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathChecks.pm
718 lines (494 loc) · 20.7 KB
/
Checks.pm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
package PostgreSQL::SecureMonitoring::Checks;
=head1 NAME
PostgreSQL::SecureMonitoring::Checks -- base class for all Posemo checks
=head1 SYNOPSIS
The following example doesn't use the sugar from L<PostgreSQL::SecureMonitoring::ChecksHelper|PostgreSQL::SecureMonitoring::ChecksHelper>.
package PostgreSQL::SecureMonitoring::Checks::SimpleAlive; # by Default, the name of the check is build from this package name
use Moose; # This is a Moose class ...
extends "PostgreSQL::SecureMonitoring::Checks"; # ... which extends our base check class
sub _build_sql { return "SELECT true;"; } # this sub simply returns the SQL for the check
1; # every Perl module must return (end with) a true value
=head1 DESCRIPTION
TODO: more Documentation!
TODO: Separate install methods into their own module?
This is the base class for all Posemo checks. It declares all base methods for
creating SQL in initialisation, calling the check at runtime etc.
The above minimalistic example SimpleAlive creates the following SQL function:
CREATE OR REPLACE FUNCTION simple_alive()
RETURNS boolean
AS
$code$
SELECT true;
$code$
LANGUAGE sql
STABLE
SECURITY DEFINER
SET search_path = monitoring, pg_temp;
ALTER FUNCTION simple_alive OWNER TO posemo_admin;
REVOKE ALL ON FUNCTION simple_alive() FROM PUBLIC;
GRANT EXECUTE ON FUNCTION simple_alive() TO posemo;
At runtime it is called with this SQL:
SELECT * FROM my_check();
=head2 results
There may be a lot of different ways for check modules to deliver their results.
There may be one result, e.g. true/false or a single number (e.g. number of backends),
or multiple values for e.g. multiple databases
If there are more then one database which delivers inforations in one check, it
should report the database in the first column:
database | active | idle | idle in transaction | idle in transaction (aborted) | fastpath function call | disabled
---------------+--------+------+---------------------+-------------------------------+------------------------+----------
!TOTAL | 1 | 0 | 0 | 0 | 0 | 0
_posemo_tests | 1 | 0 | 0 | 0 | 0 | 0
postgres | 0 | 0 | 0 | 0 | 0 | 0
(3 rows)
Types of results:
* Single value: scalar
* Multiple single values: Array of scalars
* Multiple rows with multiple values: array of arrayrefs
Structure for output/result:
[
{
host => "hostname",
results =>
[
{
check => "check_name",
result => [ {}, {}, ... ],
result_type => "", # multiline, single, list
columns => [qw(database total active idle), "idle in transaction", "other", ],
critical => 0, # or 1
warning => 0, # or 1
message => "", # warn/crit message or empty
error => "", # error message, e.g. when can't run the check
},
],
},
]
Simple results: one value.
More complex:
This check would give 5 results; ouput modules should use the databse in the check name and
each value in the performance data
=head2 TODO
???
TODO: SQL schema handling;
should be: default empty and user should have an search_path? (to "monitorin")???
=cut
use Moose;
use namespace::autoclean;
use Scalar::Util qw(looks_like_number);
use List::Util qw(any);
use English qw( -no_match_vars );
use Data::Dumper;
use Carp;
use Config::FindFile qw(search_conf);
use Log::Log4perl::EasyCatch ( log_config => search_conf( "posemo-logging.properties", "Posemo" ) );
=head3 Constants: STATUS_OK, STATUS_WARNING, STATUS_CRITICAL, STATUS_UNKNOWN
Constants for the result status infos. See status sub.
=cut
use constant {
STATUS_OK => 0,
STATUS_WARNING => 1,
STATUS_CRITICAL => 2,
STATUS_UNKNOWN => 3,
};
use base qw(Exporter);
our @EXPORT_OK = qw( STATUS_OK STATUS_WARNING STATUS_CRITICAL STATUS_UNKNOWN );
our %EXPORT_TAGS = ( all => \@EXPORT_OK, status => \@EXPORT_OK ); # at the moment: status and all are the same
#<<< no pertidy formatting
# lazy / build functions
foreach my $attr (qw(class name description code install_sql sql_function sql_function_name result_type order))
{
my $builder = "_build_$attr";
has $attr => ( is => "rw", isa => "Str", lazy => 1, builder => $builder, );
}
has return_type => ( is => "ro", isa => "Str", default => "boolean", );
has result_unit => ( is => "ro", isa => "Str", default => "", );
has language => ( is => "ro", isa => "Str", default => "sql", );
has volatility => ( is => "ro", isa => "Str", default => "STABLE", );
has has_multiline_result => ( is => "ro", isa => "Bool", default => 0, );
has has_writes => ( is => "ro", isa => "Bool", default => 0, );
has arguments => ( is => "ro", isa => "ArrayRef[Any]", default => sub { [] }, traits => ['Array'],
handles =>
{
has_arguments => 'count',
all_arguments => 'elements',
}, );
# options for graphs, display, ...
# Graph type: line, area, stacked_area, ...
# TODO: POD Documentation!
has result_is_counter => ( is => "ro", isa => "Bool", predicate => "has_result_is_counter", );
has graph_type => ( is => "ro", isa => "Str", predicate => "has_graph_type", );
has graph_mirrored => ( is => "ro", isa => "Bool", predicate => "has_graph_mirrored", );
has graph_colors => ( is => "ro", isa => "ArrayRef[Str]", predicate => "has_graph_colors", );
# The following values can be set via config file etc as parameter
has enabled => ( is => "ro", isa => "Bool", default => 1,);
has warning_level => ( is => "ro", isa => "Num", predicate => "has_warning_level", );
has critical_level => ( is => "ro", isa => "Num", predicate => "has_critical_level", );
has min_value => ( is => "ro", isa => "Num", predicate => "has_min_value", );
has max_value => ( is => "ro", isa => "Num", predicate => "has_max_value", );
# Flag for critical/warning check:
# when true, then check if result is lower else higher then critical/warning_level
has lower_is_worse => ( is => "ro", isa => "Bool", predicate => "has_lower_is_worse",);
# Internal states
has app => ( is => "ro", isa => "Object", required => 1, handles => [qw(dbh do_sql has_dbh schema user superuser host port host_desc has_host has_port commit rollback)], );
# has result => ( is => "ro", isa => "ArrayRef[Any]", default => sub { [] }, );
# attributes for attrs with builder method
# the builder looks first here and when nothing found then uses his default
has _code_attr => ( is => "ro", isa => "Str", predicate => "has_code_attr", );
has _name_attr => ( is => "ro", isa => "Str", predicate => "has_name_attr", );
has _description_attr => ( is => "ro", isa => "Str", predicate => "has_description_attr", );
has _result_type_attr => ( is => "ro", isa => "Str", predicate => "has_result_type_attr", );
has _install_sql_attr => ( is => "ro", isa => "Str", predicate => "has_install_sql_attr", );
# arguments, which may be set from check, or should be set here.
#has result_is_warning => ( is => "rw", isa => "Bool", default => 0, );
#has result_is_critical => ( is => "rw", isa => "Bool", default => 0, );
#>>>
#
# internal default builder methods
# for the default values of the attributes with builder
#
sub _build_class
{
my $self = shift;
return $self unless ref $self;
return blessed($self);
}
sub _build_name
{
my $self = shift;
return $self->_name_attr if $self->has_name_attr;
my $package = __PACKAGE__;
( my $name = $self->class ) =~ s{ $package :: }{}ox;
return _camel_case_to_words($name);
}
sub _build_description
{
my $self = shift;
return $self->_description_attr if $self->has_description_attr;
return "The ${ \$self->name } check has no description";
}
sub _camel_case_to_words
{
my $name = shift;
die "Non-word characters in check name $name\n" if $name =~ m{[\W_]}x;
die "Check package name must start with uppercase letter ($name)\n" if $name =~ m{ ^ [^[:upper:]] }x;
$name =~ s{ ( [[:lower:][:digit:]]+ ) ( [[:upper:]]+ ) }
{$1 $2}gx;
$name =~ s{ ( [[:alpha:]]+ ) ( [[:digit:]]+ ) }
{$1 $2}gx;
return $name;
}
sub _build_sql_function_name
{
my $self = shift;
( my $function_name = $self->name ) =~ s{\W}{_}gx;
return lc("${ \$self->schema }.$function_name");
}
sub _build_code
{
my $self = shift;
return $self->_code_attr if $self->has_code_attr;
die "The check (${ \$self->class }) must set his Code (or SQL-Function)\n";
}
sub _build_install_sql
{
my $self = shift;
return $self->_install_sql_attr if $self->has_install_sql_attr;
return "";
}
sub _build_sql_function
{
my $self = shift;
my ( @arguments, @arguments_with_default );
foreach my $par_ref ( $self->all_arguments )
{
my $param = "$par_ref->[0] $par_ref->[1]";
my $param_with_default = $param;
if ( defined $par_ref->[2] )
{
my $default = $par_ref->[2];
# $default = qq{'$default'} unless looks_like_number($default);
$default = $self->dbh->quote($default) unless looks_like_number($default);
$param_with_default .= " DEFAULT $default";
}
push @arguments, $param;
push @arguments_with_default, $param_with_default;
}
my $arguments = join( ", ", @arguments );
my $arguments_with_default = join( ", ", @arguments_with_default );
my $setof = "";
$setof = "SETOF" if $self->has_multiline_result;
# When return type contains a comma, then we need a new type!
# because then the return type contains a list of elements
my $return_type = $self->return_type;
my $new_type = "";
if ( $return_type =~ m{,} )
{
#<<<
$new_type = "CREATE TYPE ${ \$self->sql_function_name }_type AS ($return_type);"
. "ALTER TYPE ${ \$self->sql_function_name }_type OWNER TO ${ \$self->superuser };";
$return_type = "${ \$self->sql_function_name }_type";
#>>>
}
return qq{$new_type
CREATE OR REPLACE FUNCTION ${ \$self->sql_function_name }($arguments_with_default)
RETURNS $setof $return_type
AS
\$code\$
${ \$self->code }
\$code\$
LANGUAGE ${ \$self->language }
${ \$self->volatility }
SECURITY DEFINER
SET search_path = ${ \$self->schema }, pg_temp;
ALTER FUNCTION ${ \$self->sql_function_name }($arguments) OWNER TO ${ \$self->superuser };
REVOKE ALL ON FUNCTION ${ \$self->sql_function_name }($arguments) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION ${ \$self->sql_function_name }($arguments) TO ${ \$self->user };
};
} ## end sub _build_sql_function
sub _build_result_type
{
my $self = shift;
return $self->_result_type_attr if $self->has_result_type_attr;
return $self->return_type; # result type is by default the same as the return type of the SQL function
}
sub _build_order
{
return shift->name;
}
=head1 METHODS
=head2 install
This method installs the check on the server.
Executes the SQL from sql_function on the server.
This method does not commit!
No local error handling, don't disable RaiseError!
Only for installation use, needs an DB connection with
superuser privileges
=cut
sub install
{
my $self = shift;
if ( $self->install_sql )
{
TRACE "${ \$self->sql_function_name }: call extra SQL for installation " . $self->install_sql;
$self->do_sql( $self->install_sql );
}
TRACE "SQL-Function to install: " . $self->sql_function;
$self->do_sql( $self->sql_function );
return $self;
}
=head2 ->run_check()
Executes the check, takes the result, checks for critical/warning and returns the result...
Disabled checks are NOT skipped, this is the job of the caller!
=cut
sub run_check
{
my $self = shift;
INFO " Run check ${ \$self->name } for host ${ \$self->host_desc }";
# my $result = $self->enabled ? $self->execute : {};
my $result = eval {
my $return = $self->execute;
$self->commit if $self->has_writes;
return $return;
};
unless ($result)
{
$result->{error} = "Error executing SQL function ${ \$self->sql_function_name } from ${ \$self->class }: $EVAL_ERROR\n";
ERROR $result->{error};
eval { $self->rollback if $self->has_dbh; return 1; } or ERROR "Error in rollback: $EVAL_ERROR";
}
$result->{row_type} //= "none";
$result->{check_name} = $self->name;
$result->{description} = $self->description;
$result->{result_unit} = $self->result_unit;
$result->{result_type} = $self->result_type;
$result->{return_type} = $self->return_type;
$result->{sql_function_name} = $self->sql_function_name;
foreach my $attr (
qw(warning_level critical_level
min_value max_value
lower_is_worse
result_is_counter
graph_type graph_mirrored graph_colors
)
)
{
my $method = "has_$attr";
next unless $self->$method;
$result->{$attr} = $self->$attr;
}
# skip critical/warning test, when no real result!
if ( not $result->{error} )
{
$self->test_critical_warning($result);
}
# according to set status according to critical/warning/error
$result->{status} = $self->status($result);
TRACE "Finished check ${ \$self->name } for host ${ \$self->host_desc }";
TRACE "Result: " . Dumper($result);
return $result;
} ## end sub run_check
=head2 execute
Executes the check inside the PostgreSQL server and returns the result.
Throws exception on error!
=cut
sub execute
{
my $self = shift;
my ( @values, @placeholders );
foreach my $par_ref ( $self->all_arguments )
{
my ( $name, $type, $default ) = @$par_ref;
push @values, $self->$name // $default;
push @placeholders, q{?};
}
my %result;
my $placeholders = join( ", ", @placeholders );
# SELECT with FROM, because function with multiple OUT arguments will result in multiple columns
TRACE "Prepare: SELECT * FROM ${ \$self->sql_function_name }($placeholders);";
my $sth = $self->dbh->prepare("SELECT * FROM ${ \$self->sql_function_name }($placeholders);");
DEBUG "All values for execute: " . join( ", ", map { "'$_'" } @values );
$sth->execute(@values);
$result{columns} = $sth->{NAME};
if ( $self->has_multiline_result )
{
$result{result} = $sth->fetchall_arrayref;
$result{row_type} = "multiline";
}
else
{
my @row = $sth->fetchrow_array;
if ( scalar @row <= 1 )
{
$result{result} = $row[0];
$result{row_type} = "single";
}
else
{
$result{result} = \@row;
$result{row_type} = "list";
}
}
$sth->finish;
return \%result;
} ## end sub execute
=head2 ->test_critical_warning($result)
This method checks, if the result is critical or warning.
It may be overriden in the check to do more detailed checks, see below.
Default check depends on C<result_type> value of the result:
=over 4
=item single
Checks the single value against C<warning_level> / C<critical_level> attribute.
=item list
Checks every value against C<warning_level> / C<critical_level> attribute.
=item multiline
Checks checks every value except the first element of each row against C<warning_level> / C<critical_level> attribute
=back
=head3 Overriding
You may want to override this method. It gets one parameter (beside C<$self>): the complete C<$result>.
You can use this to write your own critical/warning test in a check module.
You can set the following attributes (hash keys!) in C<$result>:
=over 4
=item *
C<message>: a message, usually used when there is a critical / warning level reached with a description what failed.
=item *
C<critical>: a flag; set it to 1, when the critical level is reached.
=item *
C<warning>: a flag; set it to 1, when the warning level is reached.
=back
It is possible to change everything in C<$result>, but usually you should not do this (here).
=cut
sub test_critical_warning
{
my $self = shift;
my $result = shift;
if ( $result->{row_type} eq "single" and $self->return_type eq "boolean" )
{
return if $result->{result};
$result->{message} = "Failed ${ \$self->name } for host ${ \$self->host_desc }";
$result->{critical} = 1;
return;
}
return unless $self->has_critical_level or $self->has_warning_level;
my @values;
if ( $result->{row_type} eq "single" )
{
@values = ( $result->{result} );
}
elsif ( $result->{row_type} eq "list" )
{
@values = @{ $result->{result} };
}
elsif ( $result->{row_type} eq "multiline" )
{
@values = map { @$_[ 1 .. $#$_ ] } @{ $result->{result} };
}
else { $result->{error} = "FATAL: Wrong row_type '${ \$self->result_type }' in critical/warning-check\n"; }
TRACE "All Values to test for crit/warn: @values";
my $message = "";
my ( @crit, @warn );
if ( $self->lower_is_worse )
{
@crit = grep { $_ <= $self->critical_level } @values if $self->has_critical_level;
@warn = grep { $_ <= $self->warning_level } @values if $self->has_warning_level;
}
else
{
@crit = grep { $_ >= $self->critical_level } @values if $self->has_critical_level;
@warn = grep { $_ >= $self->warning_level } @values if $self->has_warning_level;
}
if (@crit)
{
$message = "Critical values: @crit! ";
$result->{critical} = 1;
INFO "$message in check ${ \$self->name } for host ${ \$self->host_desc }";
}
if (@warn)
{
$message .= "; " if $message;
$message .= "Warning values: @warn! ";
$result->{warning} = 1;
INFO "Warning values: @warn in check ${ \$self->name } for host ${ \$self->host_desc }";
}
$result->{message} = $message if $message;
return;
} ## end sub test_critical_warning
=head2 status
This method returns a C<status> according to the warning/critical flags in the given result.
0: OK
1: warning
2: critical
3: unknown (e.g. SQL error)
This may be used by some frontend or output modules to interpret the result instead
of looking into critical/warning result.
This method may be overriden in the check to do something very special and something else then
looking in warning/critical, but usually this here should be fine and you should
override C<test_critical_warning> instead (or too).
=cut
sub status
{
my $self = shift;
my $result = shift // croak "status needs a result hash for checking!";
return STATUS_CRITICAL if $result->{critical};
return STATUS_WARNING if $result->{warning};
return STATUS_UNKNOWN if $result->{error};
return STATUS_OK;
}
=head2 enabled_on_this_platform
B<Usually you should not use this flag.>
Flag, if the check is globally enabled on the current platform.
May be overridden in check, to disable some checks on some
platforms or based on other (non config!) state. When the decision
is about the configuration or something similar, the attribute
"enabled" should used/overridden.
When a check module sets C<enabled_on_this_platform> to false, then
the check will not run, because C<get_all_checks_ordered> removes it.
This should only be dependent on the platform the application is
running (local), not the PostgreSQL server (remote).
=cut
sub enabled_on_this_platform
{
return 1;
}
__PACKAGE__->meta->make_immutable;
1;