From 6b9b965b2a396892910c5957954ed458924baf85 Mon Sep 17 00:00:00 2001 From: Ben Mejia Date: Mon, 15 Jun 2026 17:03:02 -0700 Subject: [PATCH] Add support for hostname extension Collects the physical hostname, listen address, and port for each monitored server and displays them in the report header. Hostname is read from the remote's 'hostname' extension (when present) via dblink; IP/port come from the remote session with a pg_settings fallback. The feature is invisible when the extension is absent. Added tests under 'make installcheck USE_HOSTNAME=1'. --- CHANGELOG.md | 6 ++ Makefile | 14 +++ data/report_struct.sql | 24 +++++- expected/hostname_create_extension.out | 7 ++ expected/hostname_drop_extension.out | 24 ++++++ expected/hostname_feature.out | 113 +++++++++++++++++++++++++ expected/server_management.out | 6 +- management/export.sql | 48 ++++++++--- management/server.sql | 36 ++++++-- migration/migration.sql | 14 +++ report/section.sql | 35 ++++++++ sample/hostname.sql | 85 +++++++++++++++++++ sample/init_sample.sql | 3 +- sample/take_sample.0.sql | 22 +++++ schema/core.sql | 19 ++++- sql/hostname_create_extension.sql | 7 ++ sql/hostname_drop_extension.sql | 19 +++++ sql/hostname_feature.sql | 77 +++++++++++++++++ 18 files changed, 537 insertions(+), 22 deletions(-) create mode 100644 expected/hostname_create_extension.out create mode 100644 expected/hostname_drop_extension.out create mode 100644 expected/hostname_feature.out create mode 100644 sample/hostname.sql create mode 100644 sql/hostname_create_extension.sql create mode 100644 sql/hostname_drop_extension.sql create mode 100644 sql/hostname_feature.sql diff --git a/CHANGELOG.md b/CHANGELOG.md index 59d9a0e..d4a8c9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,12 @@ - Connection strings can be excluded from dumps now - Restartpoints stats added to the reports (when available) - Improvements in summary dashboard +- Server network identity collection (server_hostname, server_ip, server_port) + via dblink. Hostname is populated when the *hostname* extension is present + on the monitored server; IP/port are populated via *inet_server_addr()* + /*inet_server_port()* with a *pg_settings* fallback. Reports show a + "Network Information" section when data is available. Enable regression + tests with *make installcheck USE_HOSTNAME=1*. - Other fixes and improvements ## 4.10 diff --git a/Makefile b/Makefile index 52a7bcb..1129a24 100644 --- a/Makefile +++ b/Makefile @@ -34,6 +34,19 @@ REGRESS += \ kcache_drop_extension endif +# hostname extension tests +ifdef USE_HOSTNAME +REGRESS += \ + hostname_create_extension \ + server_management \ + samples_and_reports \ + sizes_collection \ + hostname_feature \ + export_import \ + retention_and_baselines \ + hostname_drop_extension +endif + PG_CONFIG ?= pg_config ifdef USE_PGXS @@ -75,6 +88,7 @@ sample = \ sample/get_sized_bounds.sql \ sample/init_sample.sql \ sample/pg_wait_sampling.sql \ + sample/hostname.sql \ sample/query_pg_stat_archiver.sql \ sample/query_pg_stat_bgwriter.sql \ sample/query_pg_stat_io.sql \ diff --git a/data/report_struct.sql b/data/report_struct.sql index c60b636..1dd32cb 100644 --- a/data/report_struct.sql +++ b/data/report_struct.sql @@ -21,7 +21,7 @@ VALUES ']}' ']}' ']'::jsonb), -(1, 'rep_settings', NULL, 11, NULL, NULL, NULL, NULL, NULL, +(1, 'rep_settings', NULL, 12, NULL, NULL, NULL, NULL, NULL, '[{' '"type": "row_table",' '"source": "settings",' @@ -45,6 +45,16 @@ VALUES '{"id": "stmt_cnt", "class": "table_obj_value", "caption": "Stmts Captured"}, ' '{"id": "max_cnt", "class": "table_obj_value", "caption": "pg_stat_statements.max"}' ']}]'::jsonb), +(1, 'netinfo', NULL, 11, NULL, NULL, 'server_hostname', NULL, NULL, + '[{ + "type": "row_table", + "source": "netinfo", + "columns": [ + {"id": "server_hostname", "class": "table_obj_value", "caption": "Hostname"}, + {"id": "server_ip", "class": "table_obj_value", "caption": "IP Address"}, + {"id": "server_port", "class": "table_obj_value", "caption": "Port"} + ] + }]'::jsonb), (1, 'srvstat', NULL, 200, 'Server statistics', 'Server statistics', NULL, NULL, NULL, NULL), (1, 'actsesshdr', NULL, 250, 'Session states', 'Session states observed by subsamples', 'act_backend', NULL, '{"class": "notice", "text": "Statistics about session states exceeding capturing thresholds."}', NULL), @@ -2225,7 +2235,7 @@ VALUES '{"id": ["report_end1", "report_end2"], "class": "table_obj_value", "caption": "end"}' ']}' ']}]'::jsonb), -(2, 'rep_settings', NULL, 11, NULL, NULL, NULL, NULL, NULL, +(2, 'rep_settings', NULL, 12, NULL, NULL, NULL, NULL, NULL, '[{' '"type": "row_table",' '"source": "settings",' @@ -2249,6 +2259,16 @@ VALUES '{"id": "stmt_cnt", "class": "table_obj_value", "caption": "Stmts Captured"}, ' '{"id": "max_cnt", "class": "table_obj_value", "caption": "pg_stat_statements.max"}' ']}]'::jsonb), +(2, 'netinfo', NULL, 11, NULL, NULL, 'server_hostname', NULL, NULL, + '[{ + "type": "row_table", + "source": "netinfo", + "columns": [ + {"id": "server_hostname", "class": "table_obj_value", "caption": "Hostname"}, + {"id": "server_ip", "class": "table_obj_value", "caption": "IP Address"}, + {"id": "server_port", "class": "table_obj_value", "caption": "Port"} + ] + }]'::jsonb), (2, 'srvstat', NULL, 300, 'Server statistics', 'Server statistics', NULL, NULL, NULL, NULL), (2, 'actsesshdr', NULL, 350, 'Session states', 'Session states observed by subsamples', 'act_backend', NULL, '{"class": "notice", "text": "Statistics about session states exceeding capturing thresholds."}', NULL), diff --git a/expected/hostname_create_extension.out b/expected/hostname_create_extension.out new file mode 100644 index 0000000..5a8e0e7 --- /dev/null +++ b/expected/hostname_create_extension.out @@ -0,0 +1,7 @@ +CREATE SCHEMA IF NOT EXISTS profile; +CREATE SCHEMA IF NOT EXISTS dblink; +CREATE SCHEMA IF NOT EXISTS statements; +CREATE EXTENSION dblink SCHEMA dblink; +CREATE EXTENSION pg_stat_statements SCHEMA statements; +CREATE EXTENSION hostname; +CREATE EXTENSION pg_profile SCHEMA profile; diff --git a/expected/hostname_drop_extension.out b/expected/hostname_drop_extension.out new file mode 100644 index 0000000..c98b2b2 --- /dev/null +++ b/expected/hostname_drop_extension.out @@ -0,0 +1,24 @@ +/* Drop test objects */ +DROP TABLE profile.grow_table; +DROP TABLE profile.test_rel_storage_params1; +DROP TABLE profile.test_rel_storage_params2; +DROP FUNCTION profile.dummy_func(); +DROP FUNCTION profile.grow_table_trg_f(); +DROP FUNCTION profile.get_ids; +DROP FUNCTION profile.get_sources; +DROP FUNCTION profile.get_report_sections; +DROP FUNCTION profile.check_dataset_queries; +/* Testing drop server with data */ +SELECT * FROM profile.drop_server('local'); + drop_server +------------- + 1 +(1 row) + +DROP EXTENSION pg_profile; +DROP EXTENSION hostname; +DROP EXTENSION IF EXISTS pg_stat_statements; +DROP EXTENSION IF EXISTS dblink; +DROP SCHEMA profile; +DROP SCHEMA dblink; +DROP SCHEMA statements; diff --git a/expected/hostname_feature.out b/expected/hostname_feature.out new file mode 100644 index 0000000..2e89ca2 --- /dev/null +++ b/expected/hostname_feature.out @@ -0,0 +1,113 @@ +/* == Testing hostname/network identity feature == */ +/* This test validates the server network identity feature + when the hostname extension is available. + NOTE: This runs after samples_and_reports which already took samples, + so we use existing sample data rather than taking new samples. */ +SET client_min_messages = WARNING; +/* === Verify schema columns exist === */ +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_schema = 'profile' + AND table_name = 'servers' + AND column_name IN ('server_hostname', 'server_ip', 'server_port') +ORDER BY column_name; + column_name | data_type +-----------------+----------- + server_hostname | text + server_ip | text + server_port | integer +(3 rows) + +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_schema = 'profile' + AND table_name = 'samples' + AND column_name IN ('server_hostname', 'server_ip', 'server_port') +ORDER BY column_name; + column_name | data_type +-----------------+----------- + server_hostname | text + server_ip | text + server_port | integer +(3 rows) + +/* === Verify functions exist === */ +SELECT proname, pronargs +FROM pg_proc +WHERE pronamespace = 'profile'::regnamespace + AND proname = 'collect_system_info' +ORDER BY proname; + proname | pronargs +---------------------+---------- + collect_system_info | 3 +(1 row) + +/* === Test that show_servers includes network columns === */ +SELECT + server_hostname IS NOT NULL AS has_hostname, + server_ip IS NOT NULL AS has_ip, + server_port IS NOT NULL AS has_port +FROM profile.show_servers() +WHERE server_name = 'local'; + has_hostname | has_ip | has_port +--------------+--------+---------- + t | t | t +(1 row) + +/* === Verify servers table was populated by earlier samples === */ +SELECT + server_hostname IS NOT NULL AS has_hostname, + server_ip IS NOT NULL AS has_ip, + server_port IS NOT NULL AS has_port +FROM profile.servers +WHERE server_name = 'local'; + has_hostname | has_ip | has_port +--------------+--------+---------- + t | t | t +(1 row) + +/* === Verify samples table was populated === */ +SELECT + server_hostname IS NOT NULL AS has_hostname, + server_ip IS NOT NULL AS has_ip, + server_port IS NOT NULL AS has_port +FROM profile.samples +WHERE server_id = 1 +ORDER BY sample_id DESC +LIMIT 1; + has_hostname | has_ip | has_port +--------------+--------+---------- + t | t | t +(1 row) + +/* === Verify server_hostname feature flag is set === */ +SELECT + (profile.get_report_context(1, 1, 2) #>> '{report_features,server_hostname}')::boolean + AS hostname_feature_enabled; + hostname_feature_enabled +-------------------------- + t +(1 row) + +/* === Test export includes new columns === */ +CREATE TABLE profile.hostname_export AS SELECT * FROM profile.export_data(); +SELECT count(*) > 0 AS export_has_data +FROM profile.hostname_export; + export_has_data +----------------- + t +(1 row) + +SELECT + (row_data::json->>'server_hostname') IS NOT NULL AS export_has_hostname, + (row_data::json->>'server_ip') IS NOT NULL AS export_has_ip, + (row_data::json->>'server_port') IS NOT NULL AS export_has_port +FROM profile.hostname_export +WHERE section_id = 1 + AND (row_data::json->>'server_hostname') IS NOT NULL +LIMIT 1; + export_has_hostname | export_has_ip | export_has_port +---------------------+---------------+----------------- +(0 rows) + +DROP TABLE profile.hostname_export; diff --git a/expected/server_management.out b/expected/server_management.out index 53afbeb..35deacb 100644 --- a/expected/server_management.out +++ b/expected/server_management.out @@ -125,9 +125,9 @@ FROM profile.servers WHERE server_name != 'local'; (1 row) SELECT * FROM profile.show_servers() where server_name != 'local'; - server_name | connstr | enabled | max_sample_age | description -----------------+------------------------------------------+---------+----------------+---------------------- - srvtestrenamed | dbname=postgres host=localhost port=5433 | t | 3 | Server description 2 + server_name | server_hostname | server_ip | server_port | connstr | enabled | max_sample_age | description +----------------+-----------------+-----------+-------------+------------------------------------------+---------+----------------+---------------------- + srvtestrenamed | | | | dbname=postgres host=localhost port=5433 | t | 3 | Server description 2 (1 row) SELECT * FROM profile.drop_server('srvtestrenamed'); diff --git a/management/export.sql b/management/export.sql index e44b44f..3ab03ad 100644 --- a/management/export.sql +++ b/management/export.sql @@ -214,7 +214,10 @@ BEGIN rows.size_smp_wnd_start, rows.size_smp_wnd_dur, rows.size_smp_interval, - rows.srv_settings + rows.srv_settings, + rows.server_hostname, + rows.server_ip, + rows.server_port FROM %I AS rows WHERE $2 IS NULL OR server_id = $2) dt$sql$, r_result.relname ) @@ -333,7 +336,10 @@ BEGIN imp_srv.size_smp_wnd_start imp_size_smp_wnd_start, imp_srv.size_smp_wnd_dur imp_size_smp_wnd_dur, imp_srv.size_smp_interval imp_size_smp_interval, - imp_srv.srv_settings imp_srv_settings + imp_srv.srv_settings imp_srv_settings, + imp_srv.server_hostname imp_server_hostname, + imp_srv.server_ip imp_server_ip, + imp_srv.server_port imp_server_port FROM jsonb_to_recordset($1) as imp_srv( @@ -349,7 +355,10 @@ BEGIN size_smp_wnd_start time with time zone, size_smp_wnd_dur interval hour to second, size_smp_interval interval day to minute, - srv_settings jsonb + srv_settings jsonb, + server_hostname text, + server_ip text, + server_port integer ) JOIN %s d ON (d.section_id = $2 AND d.row_data->>'name' = 'system_identifier' @@ -392,7 +401,10 @@ BEGIN size_smp_wnd_start, size_smp_wnd_dur, size_smp_interval, - srv_settings + srv_settings, + server_hostname, + server_ip, + server_port ) = ( r_result.imp_server_db_exclude, r_result.imp_server_connstr, @@ -401,7 +413,10 @@ BEGIN r_result.imp_size_smp_wnd_start, r_result.imp_size_smp_wnd_dur, r_result.imp_size_smp_interval, - r_result.imp_srv_settings + r_result.imp_srv_settings, + r_result.imp_server_hostname, + r_result.imp_server_ip, + r_result.imp_server_port ) WHERE server_id = r_result.local_server_id AND last_sample_id < r_result.imp_server_last_sample_id; @@ -425,7 +440,10 @@ BEGIN size_smp_wnd_start, size_smp_wnd_dur, size_smp_interval, - srv_settings) + srv_settings, + server_hostname, + server_ip, + server_port) VALUES ( r_result.imp_server_name, r_result.imp_server_description, @@ -438,7 +456,10 @@ BEGIN r_result.imp_size_smp_wnd_start, r_result.imp_size_smp_wnd_dur, r_result.imp_size_smp_interval, - r_result.imp_srv_settings + r_result.imp_srv_settings, + r_result.imp_server_hostname, + r_result.imp_server_ip, + r_result.imp_server_port ) RETURNING server_id INTO new_server_id; tmp_srv_map := jsonb_set( @@ -657,15 +678,22 @@ BEGIN LOOP FETCH data INTO datarow; EXIT WHEN NOT FOUND; - INSERT INTO samples(server_id, sample_id, sample_time) + INSERT INTO samples(server_id, sample_id, sample_time, + server_hostname, server_ip, server_port) SELECT (srv_map ->> dr.server_id::text)::integer, dr.sample_id, - dr.sample_time + dr.sample_time, + dr.server_hostname, + dr.server_ip, + dr.server_port FROM json_to_record(datarow.row_data) AS dr( server_id integer, sample_id integer, - sample_time timestamp(0) with time zone + sample_time timestamp(0) with time zone, + server_hostname text, + server_ip text, + server_port integer ) JOIN servers s_ctl ON diff --git a/management/server.sql b/management/server.sql index 6f238c0..b54cc0c 100644 --- a/management/server.sql +++ b/management/server.sql @@ -559,21 +559,21 @@ COMMENT ON FUNCTION set_server_size_sampling(IN server name, IN window_start tim IS 'Set relation sizes sampling settings for a server'; CREATE FUNCTION show_servers() -RETURNS TABLE(server_name name, connstr text, enabled boolean, max_sample_age integer, description text) +RETURNS TABLE(server_name name, server_hostname text, server_ip text, server_port integer, connstr text, enabled boolean, max_sample_age integer, description text) SET search_path=@extschema@ AS $$ DECLARE c_priv CURSOR FOR - SELECT server_name, connstr, enabled, max_sample_age, server_description FROM servers; + SELECT server_name, server_hostname, server_ip, server_port, connstr, enabled, max_sample_age, server_description FROM servers; c_unpriv CURSOR FOR - SELECT server_name, '' as connstr, enabled, max_sample_age, server_description FROM servers; + SELECT server_name, server_hostname, server_ip, server_port, '' as connstr, enabled, max_sample_age, server_description FROM servers; BEGIN IF has_column_privilege('servers', 'connstr', 'SELECT') THEN - FOR server_name, connstr, enabled, max_sample_age, description IN SELECT s.server_name, s.connstr, s.enabled, s.max_sample_age, s.server_description FROM servers s LOOP + FOR server_name, server_hostname, server_ip, server_port, connstr, enabled, max_sample_age, description IN SELECT s.server_name, s.server_hostname, s.server_ip, s.server_port, s.connstr, s.enabled, s.max_sample_age, s.server_description FROM servers s LOOP RETURN NEXT; END LOOP; ELSE - FOR server_name, connstr, enabled, max_sample_age, description IN SELECT s.server_name, '' as connstr, s.enabled, s.max_sample_age, s.server_description FROM servers s LOOP + FOR server_name, server_hostname, server_ip, server_port, connstr, enabled, max_sample_age, description IN SELECT s.server_name, s.server_hostname, s.server_ip, s.server_port, '' as connstr, s.enabled, s.max_sample_age, s.server_description FROM servers s LOOP RETURN NEXT; END LOOP; END IF; @@ -582,6 +582,32 @@ END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION show_servers() IS 'Displays all servers'; +CREATE FUNCTION show_servers_by_hostname(IN hostname_filter text) +RETURNS TABLE(server_name name, server_hostname text, server_ip text, server_port integer, connstr text, enabled boolean, max_sample_age integer, description text) +SET search_path=@extschema@ AS $$ +BEGIN + IF has_column_privilege('servers', 'connstr', 'SELECT') THEN + FOR server_name, server_hostname, server_ip, server_port, connstr, enabled, max_sample_age, description IN + SELECT s.server_name, s.server_hostname, s.server_ip, s.server_port, s.connstr, s.enabled, s.max_sample_age, s.server_description + FROM servers s + WHERE s.server_hostname = hostname_filter OR hostname_filter IS NULL + LOOP + RETURN NEXT; + END LOOP; + ELSE + FOR server_name, server_hostname, server_ip, server_port, connstr, enabled, max_sample_age, description IN + SELECT s.server_name, s.server_hostname, s.server_ip, s.server_port, '' as connstr, s.enabled, s.max_sample_age, s.server_description + FROM servers s + WHERE s.server_hostname = hostname_filter OR hostname_filter IS NULL + LOOP + RETURN NEXT; + END LOOP; + END IF; + RETURN; +END; +$$ LANGUAGE plpgsql; +COMMENT ON FUNCTION show_servers_by_hostname(text) IS 'Displays servers filtered by hostname'; + CREATE FUNCTION show_servers_size_sampling() RETURNS TABLE ( server_name name, diff --git a/migration/migration.sql b/migration/migration.sql index a675168..e0c40b8 100644 --- a/migration/migration.sql +++ b/migration/migration.sql @@ -6,6 +6,20 @@ DELETE FROM report_struct; DELETE FROM report; DELETE FROM report_static; +-- Add server network identity columns +ALTER TABLE servers ADD COLUMN IF NOT EXISTS server_hostname text; +ALTER TABLE servers ADD COLUMN IF NOT EXISTS server_ip text; +ALTER TABLE servers ADD COLUMN IF NOT EXISTS server_port integer; + +ALTER TABLE samples ADD COLUMN IF NOT EXISTS server_hostname text; +ALTER TABLE samples ADD COLUMN IF NOT EXISTS server_ip text; +ALTER TABLE samples ADD COLUMN IF NOT EXISTS server_port integer; + +CREATE INDEX IF NOT EXISTS ix_servers_hostname ON servers(server_hostname) WHERE server_hostname IS NOT NULL; +CREATE INDEX IF NOT EXISTS ix_servers_ip ON servers(server_ip) WHERE server_ip IS NOT NULL; +CREATE INDEX IF NOT EXISTS ix_samples_hostname ON samples(server_id, server_hostname) WHERE server_hostname IS NOT NULL; +CREATE INDEX IF NOT EXISTS ix_samples_ip ON samples(server_id, server_ip) WHERE server_ip IS NOT NULL; + -- PWR-238 truncate table sample_timings; drop VIEW v_sample_timings; diff --git a/report/section.sql b/report/section.sql index 308ac3c..af74bda 100644 --- a/report/section.sql +++ b/report/section.sql @@ -174,6 +174,13 @@ BEGIN sample_id BETWEEN start1_id AND end1_id AND (first_seen > start1_time OR last_sample_id < end1_id) ), + 'server_hostname', ( + SELECT count(*) > 0 + FROM samples + WHERE server_id = sserver_id AND + sample_id BETWEEN start1_id AND end1_id AND + server_hostname IS NOT NULL + ), 'table_storage_parameters', ( SELECT count(*) > 0 FROM v_table_storage_parameters @@ -473,6 +480,14 @@ BEGIN sample_id BETWEEN start2_id AND end2_id) AND (first_seen > least(start1_time, start2_time) OR last_sample_id < greatest(end1_id, end2_id)) ), + 'server_hostname', ( + SELECT count(*) > 0 + FROM samples + WHERE server_id = sserver_id AND + (sample_id BETWEEN start1_id AND end1_id OR + sample_id BETWEEN start2_id AND end2_id) AND + server_hostname IS NOT NULL + ), 'table_storage_parameters', ( SELECT count(*) > 0 FROM v_table_storage_parameters @@ -742,6 +757,16 @@ BEGIN IF num_nulls(start1_id, end1_id) = 0 AND num_nulls(start2_id, end2_id) > 0 THEN -- Regular report + -- network information dataset (only when hostname extension populated data) + IF (report_context #>> '{report_features,server_hostname}')::boolean THEN + SELECT jsonb_set(datasets, '{netinfo}', jsonb_build_array(jsonb_build_object( + 'server_hostname', server_hostname, + 'server_ip', server_ip, + 'server_port', server_port + ))) INTO datasets + FROM samples WHERE server_id = sserver_id AND sample_id = end1_id; + END IF; + -- database statistics dataset SELECT coalesce(jsonb_set(datasets, '{dbstat}', jsonb_agg(to_jsonb(dt))), datasets) INTO datasets @@ -1029,6 +1054,16 @@ BEGIN ELSIF num_nulls(start1_id, end1_id, start2_id, end2_id) = 0 THEN -- Differential report + -- network information dataset (only when hostname extension populated data) + IF (report_context #>> '{report_features,server_hostname}')::boolean THEN + SELECT jsonb_set(datasets, '{netinfo}', jsonb_build_array(jsonb_build_object( + 'server_hostname', server_hostname, + 'server_ip', server_ip, + 'server_port', server_port + ))) INTO datasets + FROM samples WHERE server_id = sserver_id AND sample_id = end2_id; + END IF; + -- database statistics dataset SELECT coalesce(jsonb_set(datasets, '{dbstat}', jsonb_agg(to_jsonb(dt))), datasets) INTO datasets diff --git a/sample/hostname.sql b/sample/hostname.sql new file mode 100644 index 0000000..2ec3b61 --- /dev/null +++ b/sample/hostname.sql @@ -0,0 +1,85 @@ +/* System information support - hostname and network information */ + +-- Collector (runs on the pg_profile host). Pulls the remote's hostname, +-- listen address, and port via dblink without requiring pg_profile on +-- the remote (Method B): +-- * Hostname: when the 'hostname' extension is present on the remote, +-- call its hostname() function directly via dblink. +-- * IP/port: query the remote session's inet_server_addr() and +-- inet_server_port() (which report the remote's listening endpoint), +-- with a pg_settings fallback for Unix-socket connections. +CREATE FUNCTION collect_system_info(IN properties jsonb, IN sserver_id integer, IN s_id integer) +RETURNS void SET search_path=@extschema@ AS $$ +DECLARE + qres record; + hostname_schema text; + hostname_query text; + netinfo_query text; + current_hostname text; + current_ip text; + current_port integer; +BEGIN + -- Adding dblink extension schema to search_path if it is not already there + SELECT extnamespace::regnamespace AS dblink_schema INTO STRICT qres + FROM pg_catalog.pg_extension WHERE extname = 'dblink'; + IF NOT string_to_array(current_setting('search_path'),', ') @> ARRAY[qres.dblink_schema::text] THEN + EXECUTE 'SET LOCAL search_path TO ' || current_setting('search_path')||','|| qres.dblink_schema; + END IF; + + -- 1. Hostname (only if the hostname extension is on the remote). + SELECT extnamespace INTO hostname_schema + FROM jsonb_to_recordset(properties #> '{extensions}') + AS x(extname text, extnamespace text) + WHERE extname = 'hostname'; + + IF hostname_schema IS NOT NULL THEN + hostname_query := format('SELECT %I.hostname()::text', hostname_schema); + BEGIN + SELECT h INTO current_hostname + FROM dblink('server_connection', hostname_query) AS dbl(h text); + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Failed to retrieve hostname from remote: %', SQLERRM; + current_hostname := NULL; + END; + END IF; + + -- 2. IP/port from the remote's perspective. + -- inet_server_addr()/inet_server_port() report the remote's listening + -- endpoint for the dblink connection. They return NULL for Unix-socket + -- connections (typical of self-monitoring), in which case we fall back + -- to pg_settings. + netinfo_query := + 'SELECT ' + 'COALESCE(host(inet_server_addr()), ' + ' (SELECT setting FROM pg_catalog.pg_settings WHERE name = ''listen_addresses'')), ' + 'COALESCE(inet_server_port(), ' + ' (SELECT setting::integer FROM pg_catalog.pg_settings WHERE name = ''port''))'; + + BEGIN + SELECT ip, port INTO current_ip, current_port + FROM dblink('server_connection', netinfo_query) AS dbl(ip text, port integer); + -- Normalize listen_addresses wildcards. + IF current_ip = '*' THEN + current_ip := '0.0.0.0'; + END IF; + EXCEPTION WHEN OTHERS THEN + RAISE WARNING 'Failed to retrieve network info from remote: %', SQLERRM; + current_ip := NULL; + current_port := NULL; + END; + + -- 3. Write to local tables. + UPDATE servers SET + server_hostname = current_hostname, + server_ip = current_ip, + server_port = current_port + WHERE server_id = sserver_id; + + UPDATE samples SET + server_hostname = current_hostname, + server_ip = current_ip, + server_port = current_port + WHERE server_id = sserver_id AND sample_id = s_id; +END; +$$ LANGUAGE plpgsql; + diff --git a/sample/init_sample.sql b/sample/init_sample.sql index cd6a121..121197c 100644 --- a/sample/init_sample.sql +++ b/sample/init_sample.sql @@ -146,7 +146,8 @@ BEGIN 'WHERE extname IN (' '''pg_stat_statements'',' '''pg_wait_sampling'',' - '''pg_stat_kcache''' + '''pg_stat_kcache'',' + '''hostname''' ')') AS dbl(extname name, extnamespace name, extversion text) LOOP diff --git a/sample/take_sample.0.sql b/sample/take_sample.0.sql index 0b143e8..68209a3 100644 --- a/sample/take_sample.0.sql +++ b/sample/take_sample.0.sql @@ -310,6 +310,28 @@ BEGIN server_properties := log_sample_timings(server_properties, 'processing subsamples', 'end'); + -- Collect hostname (when remote has the hostname extension) and + -- network identity (always attempted; gracefully NULL on failure). + server_properties := log_sample_timings(server_properties, 'collect system info', 'start'); + PERFORM collect_system_info(server_properties, sserver_id, s_id); + -- For the local server, prefer the calling session's + -- inet_server_addr()/inet_server_port() over whatever dblink saw, + -- since dblink connects via Unix-socket loopback and reports NULL + -- (which falls back to listen_addresses, often '*' / 0.0.0.0). + -- The take_sample() session reflects the IP/port the operator + -- connected to, which is the meaningful value. + IF (SELECT server_name = 'local' FROM servers WHERE server_id = sserver_id) THEN + UPDATE servers SET + server_ip = COALESCE(host(inet_server_addr()), server_ip), + server_port = COALESCE(inet_server_port(), server_port) + WHERE server_id = sserver_id; + UPDATE samples SET + server_ip = COALESCE(host(inet_server_addr()), server_ip), + server_port = COALESCE(inet_server_port(), server_port) + WHERE server_id = sserver_id AND sample_id = s_id; + END IF; + server_properties := log_sample_timings(server_properties, 'collect system info', 'end'); + server_properties := log_sample_timings(server_properties, 'disconnect', 'start'); PERFORM dblink('server_connection', 'COMMIT'); PERFORM dblink_disconnect('server_connection'); diff --git a/schema/core.sql b/schema/core.sql index b0b339c..b7e498c 100644 --- a/schema/core.sql +++ b/schema/core.sql @@ -16,20 +16,37 @@ CREATE TABLE servers ( size_smp_wnd_start time with time zone, size_smp_wnd_dur interval hour to second, size_smp_interval interval day to minute, - srv_settings jsonb + srv_settings jsonb, + server_hostname text, + server_ip text, + server_port integer ); COMMENT ON TABLE servers IS 'Monitored servers (Postgres clusters) list'; +COMMENT ON COLUMN servers.server_hostname IS 'Physical server hostname from hostname() extension. NULL when hostname extension is not installed.'; +COMMENT ON COLUMN servers.server_ip IS 'Server listen address from pg_settings. May be a comma-separated list (e.g. ''*'' or ''0.0.0.0'').'; +COMMENT ON COLUMN servers.server_port IS 'Server port number from pg_settings.'; + +CREATE INDEX ix_servers_hostname ON servers(server_hostname) WHERE server_hostname IS NOT NULL; +CREATE INDEX ix_servers_ip ON servers(server_ip) WHERE server_ip IS NOT NULL; CREATE TABLE samples ( server_id integer NOT NULL REFERENCES servers(server_id) ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE, sample_id integer NOT NULL, sample_time timestamp (0) with time zone, + server_hostname text, + server_ip text, + server_port integer, CONSTRAINT pk_samples PRIMARY KEY (server_id, sample_id) ); CREATE INDEX ix_sample_time ON samples(server_id, sample_time); +CREATE INDEX ix_samples_hostname ON samples(server_id, server_hostname) WHERE server_hostname IS NOT NULL; +CREATE INDEX ix_samples_ip ON samples(server_id, server_ip) WHERE server_ip IS NOT NULL; COMMENT ON TABLE samples IS 'Sample times list'; +COMMENT ON COLUMN samples.server_hostname IS 'Server hostname at time of sample collection. NULL when hostname extension is not installed.'; +COMMENT ON COLUMN samples.server_ip IS 'Server listen address at time of sample collection from pg_settings.'; +COMMENT ON COLUMN samples.server_port IS 'Server port number at time of sample collection from pg_settings.'; CREATE TABLE baselines ( server_id integer NOT NULL REFERENCES servers(server_id) ON DELETE CASCADE DEFERRABLE INITIALLY IMMEDIATE, diff --git a/sql/hostname_create_extension.sql b/sql/hostname_create_extension.sql new file mode 100644 index 0000000..5a8e0e7 --- /dev/null +++ b/sql/hostname_create_extension.sql @@ -0,0 +1,7 @@ +CREATE SCHEMA IF NOT EXISTS profile; +CREATE SCHEMA IF NOT EXISTS dblink; +CREATE SCHEMA IF NOT EXISTS statements; +CREATE EXTENSION dblink SCHEMA dblink; +CREATE EXTENSION pg_stat_statements SCHEMA statements; +CREATE EXTENSION hostname; +CREATE EXTENSION pg_profile SCHEMA profile; diff --git a/sql/hostname_drop_extension.sql b/sql/hostname_drop_extension.sql new file mode 100644 index 0000000..d4cc4fe --- /dev/null +++ b/sql/hostname_drop_extension.sql @@ -0,0 +1,19 @@ +/* Drop test objects */ +DROP TABLE profile.grow_table; +DROP TABLE profile.test_rel_storage_params1; +DROP TABLE profile.test_rel_storage_params2; +DROP FUNCTION profile.dummy_func(); +DROP FUNCTION profile.grow_table_trg_f(); +DROP FUNCTION profile.get_ids; +DROP FUNCTION profile.get_sources; +DROP FUNCTION profile.get_report_sections; +DROP FUNCTION profile.check_dataset_queries; +/* Testing drop server with data */ +SELECT * FROM profile.drop_server('local'); +DROP EXTENSION pg_profile; +DROP EXTENSION hostname; +DROP EXTENSION IF EXISTS pg_stat_statements; +DROP EXTENSION IF EXISTS dblink; +DROP SCHEMA profile; +DROP SCHEMA dblink; +DROP SCHEMA statements; diff --git a/sql/hostname_feature.sql b/sql/hostname_feature.sql new file mode 100644 index 0000000..acd9a03 --- /dev/null +++ b/sql/hostname_feature.sql @@ -0,0 +1,77 @@ +/* == Testing hostname/network identity feature == */ +/* This test validates the server network identity feature + when the hostname extension is available. + NOTE: This runs after samples_and_reports which already took samples, + so we use existing sample data rather than taking new samples. */ + +SET client_min_messages = WARNING; + +/* === Verify schema columns exist === */ +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_schema = 'profile' + AND table_name = 'servers' + AND column_name IN ('server_hostname', 'server_ip', 'server_port') +ORDER BY column_name; + +SELECT column_name, data_type +FROM information_schema.columns +WHERE table_schema = 'profile' + AND table_name = 'samples' + AND column_name IN ('server_hostname', 'server_ip', 'server_port') +ORDER BY column_name; + +/* === Verify functions exist === */ +SELECT proname, pronargs +FROM pg_proc +WHERE pronamespace = 'profile'::regnamespace + AND proname = 'collect_system_info' +ORDER BY proname; + +/* === Test that show_servers includes network columns === */ +SELECT + server_hostname IS NOT NULL AS has_hostname, + server_ip IS NOT NULL AS has_ip, + server_port IS NOT NULL AS has_port +FROM profile.show_servers() +WHERE server_name = 'local'; + +/* === Verify servers table was populated by earlier samples === */ +SELECT + server_hostname IS NOT NULL AS has_hostname, + server_ip IS NOT NULL AS has_ip, + server_port IS NOT NULL AS has_port +FROM profile.servers +WHERE server_name = 'local'; + +/* === Verify samples table was populated === */ +SELECT + server_hostname IS NOT NULL AS has_hostname, + server_ip IS NOT NULL AS has_ip, + server_port IS NOT NULL AS has_port +FROM profile.samples +WHERE server_id = 1 +ORDER BY sample_id DESC +LIMIT 1; + +/* === Verify server_hostname feature flag is set === */ +SELECT + (profile.get_report_context(1, 1, 2) #>> '{report_features,server_hostname}')::boolean + AS hostname_feature_enabled; + +/* === Test export includes new columns === */ +CREATE TABLE profile.hostname_export AS SELECT * FROM profile.export_data(); + +SELECT count(*) > 0 AS export_has_data +FROM profile.hostname_export; + +SELECT + (row_data::json->>'server_hostname') IS NOT NULL AS export_has_hostname, + (row_data::json->>'server_ip') IS NOT NULL AS export_has_ip, + (row_data::json->>'server_port') IS NOT NULL AS export_has_port +FROM profile.hostname_export +WHERE section_id = 1 + AND (row_data::json->>'server_hostname') IS NOT NULL +LIMIT 1; + +DROP TABLE profile.hostname_export;