-
Notifications
You must be signed in to change notification settings - Fork 533
Expand file tree
/
Copy pathmk_postgres.py
More file actions
executable file
·1350 lines (1166 loc) · 55.6 KB
/
mk_postgres.py
File metadata and controls
executable file
·1350 lines (1166 loc) · 55.6 KB
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
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
# Copyright (C) 2019 Checkmk GmbH - License: GNU General Public License v2
# This file is part of Checkmk (https://checkmk.com). It is subject to the terms and
# conditions defined in the file COPYING, which is part of this source code package.
# mypy: disable-error-code="no-any-return"
# mypy: disable-error-code="no-untyped-call"
# mypy: disable-error-code="no-untyped-def"
# mypy: disable-error-code="type-arg"
r"""Check_MK Agent Plugin: mk_postgres
This is a Check_MK Agent plugin. If configured, it will be called by the
agent without any arguments.
Can be configured with $MK_CONFDIR/postgres.cfg
Example for postgres.cfg file:
-----postgres.cfg-----------------------------------------
DBUSER=postgres
PG_BINARY_PATH=/usr/bin/psql
INSTANCE=/home/postgres/db1.env:USER_NAME:/PATH/TO/.pgpass:
INSTANCE=/home/postgres/db2.env:USER_NAME:/PATH/TO/.pgpass:
----------------------------------------------------------
Example of an environment file:
-----/home/postgres/db1.env-----------------------------------------
export PGDATABASE="data"
export PGPORT="5432"
export PGVERSION="14"
----------------------------------------------------------
Inside of the environment file, only `PGPORT` is mandatory.
In case there is no `INSTANCE` specified by the postgres.cfg, then the plugin assumes defaults.
For example, the configuration
-----postgres.cfg-----------------------------------------
DBUSER=postgres
PG_BINARY_PATH=/usr/bin/psql
----------------------------------------------------------
is equivalent to
-----postgres.cfg-----------------------------------------
DBUSER=postgres
PG_BINARY_PATH=/usr/bin/psql
INSTANCE=/home/postgres/does-not-exist.env:postgres::postgres
----------------------------------------------------------
-----/home/postgres/does-not-exist.env--------------------
export PGDATABASE="main"
export PGPORT="5432"
----------------------------------------------------------
The only difference being `/home/postgres/does-not-exist.env` does not exist in the first setup.
Different defaults are chosen for Windows.
"""
__version__ = "2.5.0b1"
import abc
import io
import logging
# optparse exist in python2.6 up to python 3.8. Do not use argparse, because it will not run with python2.6
import optparse
import os
import platform
import re
import stat
import subprocess
import sys
import tempfile
try:
from collections.abc import Callable, Iterable, Sequence
from typing import Any
_ = Callable, Iterable, Sequence, Any # make ruff happy
except ImportError:
# We need typing only for testing
pass
# For Python 3 sys.stdout creates \r\n as newline for Windows.
# Checkmk can't handle this therefore we rewrite sys.stdout to a new_stdout function.
# If you want to use the old behaviour just use old_stdout.
new_stdout = io.TextIOWrapper(
sys.stdout.buffer, newline="\n", encoding=sys.stdout.encoding, errors=sys.stdout.errors
)
old_stdout, sys.stdout = sys.stdout, new_stdout
OS = platform.system()
IS_LINUX = OS == "Linux"
IS_WINDOWS = OS == "Windows"
LOGGER = logging.getLogger(__name__)
LINUX_PROCESS_MATCH_PATTERNS = [
re.compile(pattern)
for pattern in [
"(.*)bin/postgres(.*)",
"(.*)bin/postmaster(.*)",
"(.*)bin/edb-postgres(.*)",
"(.*)bin/edb-postmaster(.*)",
"^[0-9]+ postgres(.*)",
"^[0-9]+ postmaster(.*)",
"^[0-9]+ edb-postgres(.*)",
"^[0-9]+ edb-postmaster(.*)",
]
]
WINDOWS_PROCESS_MATCH_PATTERNS = [
re.compile(pattern)
for pattern in [
r"(.*)bin\\postgres(.*)",
r"(.*)bin\\postmaster(.*)",
r"(.*)bin\\edb-postgres(.*)",
]
]
UTF_8_NEWLINE_CHARS = re.compile(r"[\n\r\u2028\u000B\u0085\u2028\u2029]+") # fmt: skip
class OSNotImplementedError(NotImplementedError):
def __str__(self):
# type: () -> str
return "The OS type ({}) is not yet implemented.".format(platform.system())
if IS_LINUX:
import resource
elif IS_WINDOWS:
import time
else:
raise OSNotImplementedError
# for compatibility with python 2.6
def subprocess_check_output(args):
return subprocess.Popen(args, stdout=subprocess.PIPE).communicate()[0]
# Borrowed from six
def ensure_str(s):
if isinstance(s, bytes):
return s.decode("utf-8")
return s
class PostgresPsqlError(RuntimeError):
pass
class PostgresBase:
"""
Base class for x-plattform postgres queries
:param db_user: The postgres db user
:param instance: Pass an instance, in case of monitoring a server with multiple instances
All abstract methods must have individual implementation depending on the OS type
which runs postgres.
All non-abstract methods are meant to work on all OS types which were subclassed.
"""
__metaclass__ = abc.ABCMeta
_supported_pg_versions = ["12", "15"]
def __init__(self, db_user, pg_binary_path, instance, process_match_patterns):
# type: (str, str | None, dict, Sequence[re.Pattern]) -> None
self.db_user = db_user
self.name = instance["name"]
self.pg_user = instance["pg_user"]
self.pg_port = instance["pg_port"]
self.pg_database = instance["pg_database"]
self.pg_passfile = instance.get("pg_passfile", "")
self.pg_version = instance.get("pg_version")
self.my_env = os.environ.copy()
pg_passfile = instance.get("pg_passfile")
if pg_passfile:
self.my_env["PGPASSFILE"] = pg_passfile
self.sep = os.sep
self.psql_binary_name = "psql"
if pg_binary_path is None:
self.psql_binary_path = self.get_psql_binary_path()
else:
self.psql_binary_path = pg_binary_path
self.psql_binary_dirname = self.get_psql_binary_dirname()
self.conn_time = "" # For caching as conn_time and version are in one query
self.process_match_patterns = process_match_patterns
@abc.abstractmethod
def run_sql_as_db_user(
self, sql_cmd, extra_args="", field_sep=";", quiet=True, rows_only=True, mixed_cmd=False
):
# type: (str, str, str, bool, bool, bool) -> str
"""This method implements the system specific way to call the psql interface"""
@abc.abstractmethod
def get_psql_binary_path(self):
"""This method returns the system specific psql binary and its path"""
@abc.abstractmethod
def get_psql_binary_dirname(self):
"""This method returns the system specific psql binary and its path"""
@abc.abstractmethod
def get_instances(self):
"""Gets all instances"""
@abc.abstractmethod
def get_stats(self, databases):
"""Get the stats"""
@abc.abstractmethod
def get_version_and_connection_time(self):
"""Get the pg version and the time for the query connection"""
@abc.abstractmethod
def get_bloat(self, databases, numeric_version):
"""Get the db bloats"""
def get_databases(self):
"""Gets all non template databases"""
sql_cmd = "SELECT datname FROM pg_database WHERE datistemplate = false;"
out = self.run_sql_as_db_user(sql_cmd)
return out.replace("\r", "").split("\n")
def get_server_version(self):
"""Gets the server version"""
out = self.run_sql_as_db_user("SHOW server_version;")
if out == "":
raise PostgresPsqlError("psql connection returned with no data")
version_as_string = out.split()[0]
# Use Major and Minor version for float casting: "12.6.4" -> 12.6
return float(".".join(version_as_string.split(".")[0:2]))
def get_condition_vars(self, numeric_version):
"""Gets condition variables for other queries"""
if numeric_version > 9.2:
return "state", "'idle'"
return "current_query", "'<IDLE>'"
def get_connections(self):
"""Gets the the idle and active connections"""
connection_sql_cmd = (
"SELECT datname, "
"(SELECT setting AS mc FROM pg_settings "
"WHERE name = 'max_connections') AS mc, "
"COUNT(state) FILTER (WHERE state='idle') AS idle, "
"COUNT(state) FILTER (WHERE state='active') AS active "
"FROM pg_stat_activity group by 1;"
)
return self.run_sql_as_db_user(
connection_sql_cmd, rows_only=False, extra_args="-P footer=off"
)
def get_sessions(self, row, idle):
"""Gets idle and open sessions"""
condition = "%s = %s" % (row, idle)
sql_cmd = (
"SELECT %s, count(*) FROM pg_stat_activity WHERE %s IS NOT NULL GROUP BY (%s);"
) % (condition, row, condition) # nosec B608 # BNS:fa3c6c
out = self.run_sql_as_db_user(
sql_cmd, quiet=False, extra_args="--variable ON_ERROR_STOP=1", field_sep=" "
)
# line with number of idle sessions is sometimes missing on Postgres 8.x. This can lead
# to an altogether empty section and thus the check disappearing.
if not out.startswith("t"):
out += "\nt 0"
return out
def get_query_duration(self, numeric_version):
"""Gets the query duration"""
# Previously part of simple_queries
if numeric_version > 9.2:
querytime_sql_cmd = (
"SELECT datname, datid, usename, client_addr, state AS state, "
"COALESCE(ROUND(EXTRACT(epoch FROM now()-query_start)),0) "
"AS seconds, pid, "
"query "
"AS current_query FROM pg_stat_activity "
"WHERE (query_start IS NOT NULL AND "
"(state NOT LIKE 'idle%' OR state IS NULL)) "
"ORDER BY query_start, pid DESC;"
)
else:
querytime_sql_cmd = (
"SELECT datname, datid, usename, client_addr, '' AS state,"
" COALESCE(ROUND(EXTRACT(epoch FROM now()-query_start)),0) "
"AS seconds, procpid as pid, query AS current_query "
"FROM pg_stat_activity WHERE "
"(query_start IS NOT NULL AND current_query NOT LIKE '<IDLE>%') "
"ORDER BY query_start, procpid DESC;"
)
return self.run_sql_as_db_user(
querytime_sql_cmd, rows_only=False, extra_args="-P footer=off"
)
def get_stat_database(self):
"""Gets the database stats"""
# Previously part of simple_queries
sql_cmd = (
"SELECT datid, datname, numbackends, xact_commit, xact_rollback, blks_read, "
"blks_hit, tup_returned, tup_fetched, tup_inserted, tup_updated, tup_deleted, "
"pg_database_size(datname) AS datsize FROM pg_stat_database;"
)
return self.run_sql_as_db_user(sql_cmd, rows_only=False, extra_args="-P footer=off")
def get_locks(self):
"""Get the locks"""
# Previously part of simple_queries
sql_cmd = (
"SELECT datname, granted, mode FROM pg_locks l RIGHT "
"JOIN pg_database d ON (d.oid=l.database) WHERE d.datallowconn;"
)
return self.run_sql_as_db_user(sql_cmd, rows_only=False, extra_args="-P footer=off")
def get_version(self):
"""Wrapper around get_version_conn_time"""
version, self.conn_time = self.get_version_and_connection_time()
return version
def get_connection_time(self):
"""
Wrapper around get_version_conn time.
Execute query only if conn_time wasn't already set
"""
if self.conn_time == "":
_, self.conn_time = self.get_version_and_connection_time()
return self.conn_time
def is_pg_ready(self):
"""Executes pg_isready.
pg_isready is a utility for checking the connection status of a PostgreSQL database server.
"""
out = subprocess_check_output(
["%s%spg_isready" % (self.psql_binary_dirname, os.sep), "-p", self.pg_port],
)
sys.stdout.write("%s\n" % ensure_str(out))
def is_postgres_process(self, process):
# type: (str) -> bool
"""Determine whether a process is a PostgreSQL process.
Note that the relevant binaries are contained in PATH under Linux, so they
may or may not be called using the full path. Starting from PostgreSQL
verion >= 13, they are not called using the full path.
Examples:
1252 /usr/bin/postmaster -D /var/lib/pgsql/data
3148 postmaster -D /var/lib/pgsql/data
"""
return any(re.search(p, process) for p in self.process_match_patterns)
def execute_all_queries(self):
"""Executes all queries and writes the output formatted to stdout"""
instance = "\n[[[%s]]]" % self.name
try:
databases = self.get_databases()
database_text = "\n[databases_start]\n%s\n[databases_end]" % "\n".join(databases)
version = self.get_server_version()
row, idle = self.get_condition_vars(version)
except PostgresPsqlError:
# if tcp connection to db instance failed variables are empty
databases = ""
database_text = ""
version = None
row, idle = "", ""
out = "<<<postgres_instances>>>"
out += instance
out += "\n%s" % self.get_instances()
sys.stdout.write("%s\n" % out)
out = "<<<postgres_sessions>>>"
if row and idle:
out += instance
out += "\n%s" % self.get_sessions(row, idle)
sys.stdout.write("%s\n" % out)
out = "<<<postgres_stat_database:sep(59)>>>"
out += instance
out += "\n%s" % self.get_stat_database()
sys.stdout.write("%s\n" % out)
out = "<<<postgres_locks:sep(59)>>>"
if database_text:
out += instance
out += database_text
out += "\n%s" % self.get_locks()
sys.stdout.write("%s\n" % out)
out = "<<<postgres_query_duration:sep(59)>>>"
if version:
out += instance
out += database_text
out += "\n%s" % self.get_query_duration(version)
sys.stdout.write("%s\n" % out)
out = "<<<postgres_connections:sep(59)>>>"
if database_text:
out += instance
out += database_text
out += "\n%s" % self.get_connections()
sys.stdout.write("%s\n" % out)
out = "<<<postgres_stats:sep(59)>>>"
if databases:
out += instance
out += database_text
out += "\n%s" % self.get_stats(databases)
sys.stdout.write("%s\n" % out)
out = "<<<postgres_version:sep(1)>>>"
out += instance
out += "\n%s" % self.get_version()
sys.stdout.write("%s\n" % out)
out = "<<<postgres_conn_time>>>"
out += instance
out += "\n%s" % self.get_connection_time()
sys.stdout.write("%s\n" % out)
out = "<<<postgres_bloat:sep(59)>>>"
if databases and version:
out += instance
out += database_text
out += "\n%s" % self.get_bloat(databases, version)
sys.stdout.write("%s\n" % out)
def _sanitize_sql_query(out):
# type: (bytes) -> str
utf_8_out = ensure_str(out)
# The sql queries may contain any char in `UTF_8_NEWLINE_CHARS`. However,
# Checkmk only knows how to handle `\n`. Furthermore, `\n` is always
# interpreted as a record break by Checkmk (see `parse_dbs`). This means
# that we have to remove all newline chars, before printing the section. We
# solve the issue in three steps.
# - Make Postgres return the NULL byte (instead of newlines). This achieved
# by using the flag `-0`.
# - Remove all newlines from whatever Postgres returns. This is safe,
# because of the first step.
# - Finally, turn the NULL bytes into linebreaks, so Checkmk interprets
# them as record breaks.
utf_8_out_no_new_lines = UTF_8_NEWLINE_CHARS.sub(" ", utf_8_out)
return utf_8_out_no_new_lines.replace("\x00", "\n").rstrip()
class PostgresWin(PostgresBase):
def run_sql_as_db_user(
self, sql_cmd, extra_args="", field_sep=";", quiet=True, rows_only=True, mixed_cmd=False
):
# type: (str, str, str, bool | None, bool | None,bool | None) -> str
"""This method implements the system specific way to call the psql interface"""
extra_args += " -U %s" % self.pg_user
extra_args += " -d %s" % self.pg_database
extra_args += " -p %s" % self.pg_port
if quiet:
extra_args += " -q"
if rows_only:
extra_args += " -t"
if mixed_cmd:
cmd_str = 'cmd /c echo %s | cmd /c ""%s" -X %s -A -0 -F"%s" -U %s"' % (
sql_cmd,
self.psql_binary_path,
extra_args,
field_sep,
self.db_user,
)
else:
cmd_str = 'cmd /c ""%s" -X %s -A -0 -F"%s" -U %s -c "%s"" ' % (
self.psql_binary_path,
extra_args,
field_sep,
self.db_user,
sql_cmd,
)
proc = subprocess.Popen(
cmd_str,
env=self.my_env,
stdout=subprocess.PIPE,
)
out = proc.communicate()[0]
return _sanitize_sql_query(out)
@staticmethod
def _call_wmic_logicaldisk():
# type: () -> str
return ensure_str(
subprocess_check_output(
[
"wmic",
"logicaldisk",
"get",
"deviceid",
]
)
)
@staticmethod
def _parse_wmic_logicaldisk(wmic_output):
# type: (str) -> Iterable[str]
for drive in wmic_output.replace("DeviceID", "").split(":")[:-1]:
yield drive.strip()
@classmethod
def _logical_drives(cls):
# type: () -> Iterable[str]
yield from cls._parse_wmic_logicaldisk(cls._call_wmic_logicaldisk())
def get_psql_binary_path(self):
# type: () -> str
"""This method returns the system specific psql interface binary as callable string"""
if self.pg_version is None:
# This is a fallback in case the user does not have any instances
# configured.
return self._default_psql_binary_path()
return self._psql_path(self.pg_version)
def _default_psql_binary_path(self):
# type: () -> str
for pg_version in self._supported_pg_versions:
try:
return self._psql_path(pg_version)
except OSError as e:
ioerr = e
continue
raise ioerr
def _psql_path(self, pg_version):
# type: (str) -> str
# TODO: Make this more clever...
for drive in self._logical_drives():
for program_path in [
"Program Files\\PostgreSQL",
"Program Files (x86)\\PostgreSQL",
"PostgreSQL",
]:
psql_path = (
"{drive}:\\{program_path}\\{pg_version}\\bin\\{psql_binary_name}.exe".format(
drive=drive,
program_path=program_path,
pg_version=pg_version.split(".", 1)[
0
], # Only the major version is relevant
psql_binary_name=self.psql_binary_name,
)
)
if os.path.isfile(psql_path):
return psql_path
raise OSError("Could not determine %s bin and its path." % self.psql_binary_name)
def get_psql_binary_dirname(self):
# type: () -> str
return self.psql_binary_path.rsplit("\\", 1)[0]
def get_instances(self):
# type: () -> str
"""Gets all instances"""
taskslist = ensure_str(
subprocess_check_output(
["wmic", "process", "get", "processid,commandline", "/format:list"]
)
).split("\r\r\n\r\r\n\r\r\n")
out = ""
for task in taskslist:
task = task.lstrip().rstrip()
if len(task) == 0:
continue
cmd_line, PID = task.split("\r\r\n")
cmd_line = cmd_line.split("CommandLine=")[1]
PID = PID.split("ProcessId=")[1]
if self.is_postgres_process(cmd_line):
if task.find(self.name) != -1:
out += "%s %s\n" % (PID, cmd_line)
return out.rstrip()
def get_stats(self, databases):
# type: (list[str]) -> str
"""Get the stats"""
# The next query had to be slightly modified:
# As cmd.exe interprets > as redirect and we need <> as "not equal", this was changed to
# != as it has the same SQL implementation
sql_cmd_lastvacuum = (
"SELECT "
"current_database() AS datname, nspname AS sname, "
"relname AS tname, CASE WHEN v IS NULL THEN -1 "
"ELSE round(extract(epoch FROM v)) END AS vtime, "
"CASE WHEN g IS NULL THEN -1 ELSE round(extract(epoch FROM g)) "
"END AS atime FROM (SELECT nspname, relname, "
"GREATEST(pg_stat_get_last_vacuum_time(c.oid), "
"pg_stat_get_last_autovacuum_time(c.oid)) AS v, "
"GREATEST(pg_stat_get_last_analyze_time(c.oid), "
"pg_stat_get_last_autoanalyze_time(c.oid)) AS g "
"FROM pg_class c, pg_namespace n WHERE relkind = 'r' "
"AND n.oid = c.relnamespace AND n.nspname != 'information_schema' "
"ORDER BY 3) AS foo;"
)
query = "\\pset footer off \\\\ BEGIN;SET statement_timeout=30000;COMMIT;"
cur_rows_only = False
for cnt, database in enumerate(databases):
query = "%s \\c %s \\\\ %s" % (query, database, sql_cmd_lastvacuum)
if cnt == 0:
query = "%s \\pset tuples_only on" % query
return self.run_sql_as_db_user(query, mixed_cmd=True, rows_only=cur_rows_only)
def get_version_and_connection_time(self):
# type: () -> tuple[str, str]
"""Get the pg version and the time for the query connection"""
cmd = "SELECT version() AS v"
# TODO: Verify this time measurement
start_time = time.time()
out = self.run_sql_as_db_user(cmd)
diff = time.time() - start_time
return out, "%.3f" % diff
def get_bloat(self, databases, numeric_version):
# type: (list[Any], float) -> str
"""Get the db bloats"""
# Bloat index and tables
# Supports versions <9.0, >=9.0
# This huge query has been gratefully taken from Greg Sabino Mullane's check_postgres.pl
if numeric_version > 9.0:
# TODO: Reformat query in a more readable way
# Here as well: "<" and ">" must be escaped. As we're using meta-command + SQL in one
# query, we need to use pipe. Due to Window's cmd behaviour, we need to escape those
# symbols with 3 (!) carets. See https://ss64.com/nt/syntax-redirection.html
bloat_query = (
"SELECT current_database() AS db, "
"schemaname, tablename, reltuples::bigint "
"AS tups, relpages::bigint AS pages, otta, "
"ROUND(CASE WHEN sml.relpages=0 "
"OR sml.relpages=otta THEN 0.0 "
"ELSE (sml.relpages-otta::numeric)/sml.relpages END,3) AS tbloat, "
"CASE WHEN relpages ^^^< otta THEN 0 "
"ELSE relpages::bigint - otta END AS wastedpages, "
"CASE WHEN relpages ^^^< otta THEN 0 ELSE bs*(sml.relpages-otta)::bigint END "
"AS wastedbytes, CASE WHEN relpages ^^^< otta THEN 0 "
"ELSE (bs*(relpages-otta))::bigint END "
"AS wastedsize, iname, ituples::bigint AS itups, ipages::bigint "
"AS ipages, iotta, ROUND(CASE WHEN ipages=0 OR ipages^^^<=iotta THEN 0.0 "
"ELSE (ipages-iotta::numeric)/ipages END,3) AS ibloat, "
"CASE WHEN ipages ^^^< iotta THEN 0 ELSE ipages::bigint - iotta END "
"AS wastedipages, CASE WHEN ipages ^^^< iotta THEN 0 ELSE bs*(ipages-iotta) "
"END AS wastedibytes, CASE WHEN ipages ^^^< iotta THEN 0 "
"ELSE (bs*(ipages-iotta))::bigint END AS wastedisize, "
"CASE WHEN relpages ^^^< otta THEN CASE WHEN ipages ^^^< iotta THEN 0 "
"ELSE bs*(ipages-iotta::bigint) END ELSE CASE WHEN ipages ^^^< iotta "
"THEN bs*(relpages-otta::bigint) "
"ELSE bs*(relpages-otta::bigint + ipages-iotta::bigint) "
"END END AS totalwastedbytes "
"FROM ( SELECT nn.nspname AS schemaname, cc.relname AS tablename, "
"COALESCE(cc.reltuples,0) AS reltuples, COALESCE(cc.relpages,0) "
"AS relpages, COALESCE(bs,0) AS bs, "
"COALESCE(CEIL((cc.reltuples*((datahdr+ma- (CASE WHEN datahdr%ma=0 "
"THEN ma ELSE datahdr%ma END))+nullhdr2+4))/(bs-20::float)),0) "
"AS otta, COALESCE(c2.relname,'?') AS iname, COALESCE(c2.reltuples,0) "
"AS ituples, COALESCE(c2.relpages,0) "
"AS ipages, COALESCE(CEIL((c2.reltuples*(datahdr-12))/(bs-20::float)),0) "
"AS iotta FROM pg_class cc "
"JOIN pg_namespace nn ON cc.relnamespace = nn.oid "
"AND nn.nspname != 'information_schema' LEFT JOIN "
"( SELECT ma,bs,foo.nspname,foo.relname, "
"(datawidth+(hdr+ma-(case when hdr%ma=0 "
"THEN ma ELSE hdr%ma END)))::numeric AS datahdr, "
"(maxfracsum*(nullhdr+ma-(case when nullhdr%ma=0 THEN ma "
"ELSE nullhdr%ma END))) AS nullhdr2 "
"FROM ( SELECT ns.nspname, tbl.relname, hdr, ma, bs, "
"SUM((1-coalesce(null_frac,0))*coalesce(avg_width, 2048)) AS datawidth, "
"MAX(coalesce(null_frac,0)) AS maxfracsum, hdr+( SELECT 1+count(*)/8 "
"FROM pg_stats s2 WHERE null_frac != 0 AND s2.schemaname = ns.nspname "
"AND s2.tablename = tbl.relname ) AS nullhdr FROM pg_attribute att "
"JOIN pg_class tbl ON att.attrelid = tbl.oid JOIN pg_namespace ns "
"ON ns.oid = tbl.relnamespace LEFT JOIN pg_stats s "
"ON s.schemaname=ns.nspname AND s.tablename = tbl.relname AND "
"s.inherited=false AND s.attname=att.attname, "
"( SELECT (SELECT current_setting('block_size')::numeric) AS bs, CASE WHEN "
"SUBSTRING(SPLIT_PART(v, ' ', 2) FROM '#\\[0-9]+.[0-9]+#\\%' for '#') "
"IN ('8.0','8.1','8.2') THEN 27 ELSE 23 END AS hdr, CASE "
"WHEN v ~ 'mingw32' OR v ~ '64-bit' THEN 8 ELSE 4 END AS ma "
"FROM (SELECT version() AS v) AS foo ) AS constants WHERE att.attnum ^^^> 0 "
"AND tbl.relkind='r' GROUP BY 1,2,3,4,5 ) AS foo ) AS rs "
"ON cc.relname = rs.relname AND nn.nspname = rs.nspname LEFT "
"JOIN pg_index i ON indrelid = cc.oid LEFT JOIN pg_class c2 "
"ON c2.oid = i.indexrelid ) AS sml WHERE sml.relpages - otta ^^^> 0 "
"OR ipages - iotta ^^^> 10 ORDER BY totalwastedbytes DESC LIMIT 10;"
)
else:
bloat_query = (
"SELECT "
"current_database() AS db, schemaname, tablename, "
"reltuples::bigint AS tups, relpages::bigint AS pages, otta, "
"ROUND(CASE WHEN sml.relpages=0 OR sml.relpages=otta THEN 0.0 "
"ELSE (sml.relpages-otta::numeric)/sml.relpages END,3) AS tbloat, "
"CASE WHEN relpages ^^^< otta THEN 0 ELSE relpages::bigint - otta END "
"AS wastedpages, CASE WHEN relpages ^^^< otta THEN 0 "
"ELSE bs*(sml.relpages-otta)::bigint END AS wastedbytes, "
"CASE WHEN relpages ^^^< otta THEN '0 bytes'::text "
"ELSE (bs*(relpages-otta))::bigint || ' bytes' END AS wastedsize, "
"iname, ituples::bigint AS itups, ipages::bigint AS ipages, iotta, "
"ROUND(CASE WHEN ipages=0 OR ipages^^^<=iotta THEN 0.0 ELSE "
"(ipages-iotta::numeric)/ipages END,3) AS ibloat, "
"CASE WHEN ipages ^^^< iotta THEN 0 ELSE ipages::bigint - iotta END "
"AS wastedipages, CASE WHEN ipages ^^^< iotta THEN 0 "
"ELSE bs*(ipages-iotta) END AS wastedibytes, "
"CASE WHEN ipages ^^^< iotta THEN '0 bytes' ELSE "
"(bs*(ipages-iotta))::bigint || ' bytes' END AS wastedisize, CASE "
"WHEN relpages ^^^< otta THEN CASE WHEN ipages ^^^< iotta THEN 0 "
"ELSE bs*(ipages-iotta::bigint) END ELSE CASE WHEN ipages ^^^< iotta "
"THEN bs*(relpages-otta::bigint) "
"ELSE bs*(relpages-otta::bigint + ipages-iotta::bigint) END "
"END AS totalwastedbytes FROM (SELECT nn.nspname AS schemaname, "
"cc.relname AS tablename, COALESCE(cc.reltuples,0) AS reltuples, "
"COALESCE(cc.relpages,0) AS relpages, COALESCE(bs,0) AS bs, "
"COALESCE(CEIL((cc.reltuples*((datahdr+ma-(CASE WHEN datahdr%ma=0 "
"THEN ma ELSE datahdr%ma END))+nullhdr2+4))/(bs-20::float)),0) AS otta, "
"COALESCE(c2.relname,'?') AS iname, COALESCE(c2.reltuples,0) AS ituples, "
"COALESCE(c2.relpages,0) AS ipages, "
"COALESCE(CEIL((c2.reltuples*(datahdr-12))/(bs-20::float)),0) AS iotta "
"FROM pg_class cc JOIN pg_namespace nn ON cc.relnamespace = nn.oid "
"AND nn.nspname ^^^<^^^> 'information_schema' LEFT "
"JOIN(SELECT ma,bs,foo.nspname,foo.relname, "
"(datawidth+(hdr+ma-(case when hdr%ma=0 THEN ma "
"ELSE hdr%ma END)))::numeric AS datahdr, "
"(maxfracsum*(nullhdr+ma-(case when nullhdr%ma=0 THEN ma "
"ELSE nullhdr%ma END))) AS nullhdr2 "
"FROM (SELECT ns.nspname, tbl.relname, hdr, ma, bs, "
"SUM((1-coalesce(null_frac,0))*coalesce(avg_width, 2048)) "
"AS datawidth, MAX(coalesce(null_frac,0)) AS maxfracsum, hdr+("
"SELECT 1+count(*)/8 FROM pg_stats s2 WHERE null_frac^^^<^^^>0 "
"AND s2.schemaname = ns.nspname AND s2.tablename = tbl.relname) "
"AS nullhdr FROM pg_attribute att JOIN pg_class tbl "
"ON att.attrelid = tbl.oid JOIN pg_namespace ns ON "
"ns.oid = tbl.relnamespace LEFT JOIN pg_stats s "
"ON s.schemaname=ns.nspname AND s.tablename = tbl.relname "
"AND s.attname=att.attname, (SELECT ("
"SELECT current_setting('block_size')::numeric) AS bs, CASE WHEN "
"SUBSTRING(SPLIT_PART(v, ' ', 2) FROM '#\"[0-9]+.[0-9]+#\"%' for '#') "
"IN ('8.0','8.1','8.2') THEN 27 ELSE 23 END AS hdr, CASE "
"WHEN v ~ 'mingw32' OR v ~ '64-bit' THEN 8 ELSE 4 END AS ma "
"FROM (SELECT version() AS v) AS foo) AS constants WHERE att.attnum ^^^> 0 "
"AND tbl.relkind='r' GROUP BY 1,2,3,4,5) AS foo) AS rs ON "
"cc.relname = rs.relname AND nn.nspname = rs.nspname LEFT JOIN pg_index i "
"ON indrelid = cc.oid LEFT JOIN pg_class c2 ON c2.oid = i.indexrelid) "
"AS sml WHERE sml.relpages - otta ^^^> 0 OR ipages - iotta ^^^> 10 ORDER "
"BY totalwastedbytes DESC LIMIT 10;"
)
cur_rows_only = False
output = ""
for idx, database in enumerate(databases):
query = "\\pset footer off \\\\ \\c %s \\\\ %s" % (database, bloat_query)
if idx == 0:
query = "%s \\pset tuples_only on" % query
output += self.run_sql_as_db_user(query, mixed_cmd=True, rows_only=cur_rows_only)
cur_rows_only = True
return output
class PostgresLinux(PostgresBase):
def _run_sql_as_db_user(
self, sql_file_path, extra_args="", field_sep=";", quiet=True, rows_only=True
):
# type: (str, str, str, bool, bool) -> str
base_cmd_list = [
"su",
"-",
self.db_user,
"-c",
r"""PGPASSFILE=%s %s -X %s -A0 -F'%s' -f %s""",
]
extra_args += " -U %s" % self.pg_user
extra_args += " -d %s" % self.pg_database
extra_args += " -p %s" % self.pg_port
if quiet:
extra_args += " -q"
if rows_only:
extra_args += " -t"
base_cmd_list[-1] = base_cmd_list[-1] % (
self.pg_passfile,
self.psql_binary_path,
extra_args,
field_sep,
sql_file_path,
)
proc = subprocess.Popen(base_cmd_list, env=self.my_env, stdout=subprocess.PIPE)
return _sanitize_sql_query(proc.communicate()[0])
def run_sql_as_db_user(
self, sql_cmd, extra_args="", field_sep=";", quiet=True, rows_only=True, mixed_cmd=False
):
# type: (str, str, str, bool, bool, bool) -> str
with tempfile.NamedTemporaryFile(delete=True) as tmp:
tmp.write(sql_cmd.encode("utf-8"))
# set cursor to the beginning of the file
tmp.seek(0)
# We use 'psql ... -f <FILE_PATH>', the tmp file has to be readable to all users,
# ie. stat.S_IROTH
os.chmod(tmp.name, stat.S_IROTH)
return self._run_sql_as_db_user(
tmp.name,
extra_args=extra_args,
field_sep=field_sep,
quiet=quiet,
rows_only=rows_only,
)
def get_psql_binary_path(self):
# type: () -> str
"""If possible, do not use the binary from PATH directly. This could lead to a generic
binary that is not able to find the correct UNIX socket. See SUP-11729.
In case the user does not have any instances configured or if the assembled path does not
exist, fallback to the PATH location. See SUP-12878"""
if self.pg_version is None:
return self._default_psql_binary_path()
binary_path = "/{pg_database}/{pg_version}/bin/{psql_binary_name}".format(
pg_database=self.pg_database,
pg_version=self.pg_version,
psql_binary_name=self.psql_binary_name,
)
if not os.path.isfile(binary_path):
return self._default_psql_binary_path()
return binary_path
def _default_psql_binary_path(self):
# type: () -> str
proc = subprocess.Popen(["which", self.psql_binary_name], stdout=subprocess.PIPE)
out = ensure_str(proc.communicate()[0])
if proc.returncode != 0:
raise RuntimeError("Could not determine %s executable." % self.psql_binary_name)
return out.strip()
def get_psql_binary_dirname(self):
# type: () -> str
return self.psql_binary_path.rsplit("/", 1)[0]
def _matches_main(self, proc):
# type: (str) -> bool
# the data directory for the instance "main" is not called "main" but "data" on some
# platforms
return self.name == "main" and "data" in proc
def _filter_instances(self, procs_list, proc_sensitive_filter):
# type: (list[str], Callable[[str], bool]) -> list[str]
return [
proc
for proc in procs_list
if self.is_postgres_process(proc)
and (proc_sensitive_filter(proc) or self._matches_main(proc))
]
def get_instances(self):
# type: () -> str
procs_list = ensure_str(
subprocess_check_output(["ps", "h", "-eo", "pid:1,command:1"])
).split("\n")
# trying to address setups in SUP-12878 (instance "A01" -> process "A01") and SUP-12539
# (instance "epcomt" -> process "EPCOMT") as well as possible future setups (containing
# instances where the names only differ in case (e.g. "instance" and "INSTANCE"))
procs = self._filter_instances(procs_list, proc_sensitive_filter=lambda p: self.name in p)
if not procs:
procs = self._filter_instances(
procs_list, proc_sensitive_filter=lambda p: self.name.lower() in p.lower()
)
out = "\n".join(procs)
return out.rstrip()
def get_query_duration(self, numeric_version):
# type: (float) -> str
# Previously part of simple_queries
if numeric_version > 9.2:
querytime_sql_cmd = (
"SELECT datname, datid, usename, client_addr, state AS state, "
"COALESCE(ROUND(EXTRACT(epoch FROM now()-query_start)),0) "
"AS seconds, pid, "
"query AS current_query FROM pg_stat_activity "
"WHERE (query_start IS NOT NULL AND "
"(state NOT LIKE 'idle%' OR state IS NULL)) "
"ORDER BY query_start, pid DESC;"
)
else:
querytime_sql_cmd = (
"SELECT datname, datid, usename, client_addr, '' AS state, "
"COALESCE(ROUND(EXTRACT(epoch FROM now()-query_start)),0) "
"AS seconds, procpid as pid, "
"query "
"AS current_query FROM pg_stat_activity WHERE "
"(query_start IS NOT NULL AND current_query NOT LIKE '<IDLE>%') "
"ORDER BY query_start, procpid DESC;"
)
return self.run_sql_as_db_user(
querytime_sql_cmd, rows_only=False, extra_args="-P footer=off"
)
def get_stats(self, databases):
# type: (list[str]) -> str
sql_cmd_lastvacuum = (
"SELECT "
"current_database() AS datname, nspname AS sname, "
"relname AS tname, CASE WHEN v IS NULL THEN -1 "
"ELSE round(extract(epoch FROM v)) END AS vtime, "
"CASE WHEN g IS NULL THEN -1 ELSE round(extract(epoch FROM g)) "
"END AS atime FROM (SELECT nspname, relname, "
"GREATEST(pg_stat_get_last_vacuum_time(c.oid), "
"pg_stat_get_last_autovacuum_time(c.oid)) AS v, "
"GREATEST(pg_stat_get_last_analyze_time(c.oid), "
"pg_stat_get_last_autoanalyze_time(c.oid)) AS g "
"FROM pg_class c, pg_namespace n WHERE relkind = 'r' "
"AND n.oid = c.relnamespace AND n.nspname <> 'information_schema' "
"ORDER BY 3) AS foo;"
)
query = "\\pset footer off\nBEGIN;\nSET statement_timeout=30000;\nCOMMIT;"
cur_rows_only = False
for cnt, database in enumerate(databases):
query = "%s\n\\c %s\n%s" % (query, database, sql_cmd_lastvacuum)
if cnt == 0:
query = "%s\n\\pset tuples_only on" % query
return self.run_sql_as_db_user(query, mixed_cmd=True, rows_only=cur_rows_only)
def get_version_and_connection_time(self):
# type: () -> tuple[str, str]
cmd = "SELECT version() AS v"
usage_start = resource.getrusage(resource.RUSAGE_CHILDREN)
out = self.run_sql_as_db_user(cmd)
usage_end = resource.getrusage(resource.RUSAGE_CHILDREN)
sys_time = usage_end.ru_stime - usage_start.ru_stime
usr_time = usage_end.ru_utime - usage_start.ru_utime
real = sys_time + usr_time
return out, "%.3f" % real
def get_bloat(self, databases, numeric_version):
# type: (list[Any], float) -> str
# Bloat index and tables
# Supports versions <9.0, >=9.0
# This huge query has been gratefully taken from Greg Sabino Mullane's check_postgres.pl
if numeric_version > 9.0:
# TODO: Reformat query in a more readable way
bloat_query = (
"SELECT current_database() AS db, schemaname, tablename, reltuples::bigint "
"AS tups, relpages::bigint AS pages, otta, ROUND(CASE WHEN sml.relpages=0 "
"OR sml.relpages=otta THEN 0.0 "
"ELSE (sml.relpages-otta::numeric)/sml.relpages END,3) AS tbloat, "
"CASE WHEN relpages < otta THEN 0 "
"ELSE relpages::bigint - otta END AS wastedpages, "
"CASE WHEN relpages < otta THEN 0 ELSE bs*(sml.relpages-otta)::bigint END "
"AS wastedbytes, CASE WHEN relpages < otta THEN 0 "
"ELSE (bs*(relpages-otta))::bigint END "
"AS wastedsize, iname, ituples::bigint AS itups, ipages::bigint "
"AS ipages, iotta, ROUND(CASE WHEN ipages=0 OR ipages<=iotta THEN 0.0 "
"ELSE (ipages-iotta::numeric)/ipages END,3) AS ibloat, "
"CASE WHEN ipages < iotta THEN 0 ELSE ipages::bigint - iotta END "
"AS wastedipages, CASE WHEN ipages < iotta THEN 0 ELSE bs*(ipages-iotta) "
"END AS wastedibytes, CASE WHEN ipages < iotta THEN 0 "
"ELSE (bs*(ipages-iotta))::bigint END AS wastedisize, "
"CASE WHEN relpages < otta THEN CASE WHEN ipages < iotta THEN 0 "
"ELSE bs*(ipages-iotta::bigint) END ELSE CASE WHEN ipages < iotta "
"THEN bs*(relpages-otta::bigint) "
"ELSE bs*(relpages-otta::bigint + ipages-iotta::bigint) "
"END END AS totalwastedbytes "
"FROM ( SELECT nn.nspname AS schemaname, cc.relname AS tablename, "
"COALESCE(cc.reltuples,0) AS reltuples, COALESCE(cc.relpages,0) "