Injektáž zdrojového kódu PL/pgSQL funkcí za účelem získání seznamu používaných funkcí

Z PostgreSQL
Skočit na navigaci Skočit na vyhledávání

Počínaje 8.4 máme k dispozici jednoduchý intergrovaný profiler, který nám umožňuje zjistit, které zákaznické funkce se používají, a které nikoliv. Ve starších verzích tuto možnost nemáme a tak si musíme pomoci jinak. Jednou z možností je vložit do zdrojového kódu funkce volání jakési registrační funkce, která zaznamená do vybrané tabulky informaci o volání funkce. Pozor - na produkčním serveru může dojít (při vyšším zatížení) ke ztrátě výkonu. Oproti integrovanému profileru můžeme zjistit registrační funkcí více informací:

Registrační funkce je ukázkou funkce, která je napsána tak, aby neselhala za žádných okolností - nechceme, aby běh aplikace byl ovlivněn profilací:

/*
 * The invocation of this function is injected to custom function's source code. It increase 
 * a function staticstic counter and logs a current query (only once per 5 minutes). 
 */
CREATE OR REPLACE FUNCTION signal_func_used(_funcid oid, _funcname varchar, _lanname varchar)
RETURNS void AS $$
BEGIN
  UPDATE user_stat_function_data 
     SET calls = calls + 1
    WHERE funcid = _funcid;
  IF NOT FOUND THEN
    -- use a advisory lock but only for this function (specified by func oid)
    PERFORM pg_advisory_lock(_funcid);
    -- chec it again - registration can be processed by other session 
    UPDATE user_stat_function_data 
       SET calls = calls + 1
      WHERE funcid = _funcid;
    IF NOT FOUND THEN
      INSERT INTO user_stat_function_data(funcid,funcname,calls,lanname)
          VALUES(_funcid, _funcname, 1, _lanname);
    END IF;
    PERFORM pg_advisory_unlock(_funcid);
  END IF;
  /* 
   * store current query to table activity_stat_data. Don't do it 
   * more than once per five minutes - it reduce a log size
   */
  IF NOT EXISTS(SELECT * 
                   FROM activity_stat_data 
                  WHERE funcid = _funcid AND query_start < current_timestamp - interval '5min') THEN
    INSERT INTO activity_stat_data 
       SELECT _funcid, query_start, usename, client_addr, current_query
          FROM pg_stat_activity 
         WHERE procpid = pg_backend_pid();
  END IF; 
  RETURN;
END;
$$ LANGUAGE plpgsql VOLATILE;

Pro hromadnou úpravu zdrojových kódů použijeme funkce:

/*
 * This function injects all custom functions in current database. It check if some 
 * function is injected or not. When not, then put to begin of function's code a call
 * of function signal_function_used.
 */
CREATE OR REPLACE FUNCTION inject_watcher()
RETURNS void AS $$
DECLARE r record; _sigfunc varchar;
BEGIN
  FOR r IN 
    SELECT p.oid, proname, nspname, prosrc, lanname
       FROM pg_proc p JOIN pg_language l ON l.oid = p.prolang
                      JOIN pg_user u ON u.usesysid =  p.proowner
		      JOIN pg_namespace n ON n.oid = p.pronamespace
      WHERE lanname IN ('sql','plpgsql') AND usename <> 'postgres' AND nspname <> 'pg_catalog'
        AND proname NOT IN ('inject_watcher','signal_func_used')
  LOOP
    IF position('signal_func_used' in r.prosrc) = 0 THEN
      IF r.lanname = 'sql' THEN
        UPDATE pg_proc 
           SET prosrc = 'SELECT signal_func_used(' || r.oid || ',' || quote_literal(r.proname) || ',' || quote_literal(r.lanname) || ');' || prosrc
          WHERE oid = r.oid;
      ELSEIF r.lanname = 'plpgsql' THEN
        _sigfunc := 'PERFORM signal_func_used(' || r.oid || ',' || quote_literal(r.proname) || ',' || quote_literal(r.lanname) || ');';
        /* regularnim vyrazem musim najit prvni begin a za nej umistit signal */
        UPDATE pg_proc 
           SET prosrc = regexp_replace(prosrc,'begin','BEGIN ' || _sigfunc, 'i')
          WHERE oid = r.oid;
      END IF;
     END IF;
  END LOOP;
  RETURN;
END;
$$ LANGUAGE plpgsql;

SELECT inject_watcher();

/*
SELECT test1();
SELECT * FROM test2();
SELECT test3(10);
*/
/*
 * remove injected code
 */
CREATE OR REPLACE FUNCTION remove_watcher()
RETURNS void AS $$
DECLARE r record; _sigfunc varchar;
BEGIN
  FOR r IN 
    SELECT p.oid, proname, nspname, prosrc, lanname
       FROM pg_proc p JOIN pg_language l ON l.oid = p.prolang
                      JOIN pg_user u ON u.usesysid =  p.proowner
		      JOIN pg_namespace n ON n.oid = p.pronamespace
      WHERE lanname IN ('sql','plpgsql') AND usename <> 'postgres' AND nspname <> 'pg_catalog'
        AND proname NOT IN ('inject_watcher','signal_func_used')
  LOOP
    IF position('signal_func_used' in r.prosrc) <> 0 THEN
      IF r.lanname = 'sql' THEN
        _sigfunc := 'SELECT signal_func_used(' || r.oid || ',' || quote_literal(r.proname) || ',' || quote_literal(r.lanname) || ');';
      ELSEIF r.lanname = 'plpgsql' THEN
        _sigfunc := 'PERFORM signal_func_used(' || r.oid || ',' || quote_literal(r.proname) || ',' || quote_literal(r.lanname) || ');';
      END IF;
      UPDATE pg_proc
         SET prosrc = replace(prosrc, _sigfunc, '')
          WHERE oid = r.oid;
     END IF;
  END LOOP;
  RETURN;
END;
$$ LANGUAGE plpgsql;

/*
SELECT remove_watcher();
*/

Zbývá pouze vytvořit logovací tabulky:

/*
 * necessary tables
 */
DROP TABLE IF EXISTS user_stat_function_data;
DROP TABLE IF EXISTS activity_stat_data;

CREATE TABLE user_stat_function_data(
  funcid oid PRIMARY KEY,
  funcname varchar,
  calls bigint,
  lanname varchar
);

CREATE TABLE activity_stat_data(
  funcid oid,
  query_start timestamp with time zone,
  usename varchar,
  client_addr inet,
  query varchar
);