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;