Oracle 8i | Oracle 9i | Oracle 10g | Oracle 11g | Oracle 12c | Miscellaneous | PL/SQL | SQL | Oracle RAC | Oracle Apps | Linux

Complete Data Audit

In some situations it's necessary for your application to audit before and after snapshots of data for every transaction. This level of detail is not available from Oracle's own auditing features so this article presents a simple generic solution.

Relational Schema

First we must create a relational schema to work on.

CREATE TABLE audit_logs (
  id              NUMBER(10)    NOT NULL,
  log_timestamp   TIMESTAMP     NOT NULL,
  username        VARCHAR2(30)  NOT NULL,
  object_name     VARCHAR2(30)  NOT NULL,
  action          VARCHAR2(10)  NOT NULL,
  data            CLOB
)
/

ALTER TABLE audit_logs ADD (
  CONSTRAINT audit_logs_pk PRIMARY KEY (id)
)
/

CREATE SEQUENCE audit_logs_seq;

Package

Next we create the package that actually performs the inserts into the audit_logs table. This has been separated out into a package so that addition control features, like on/off audit switches, can be added centrally.

CREATE OR REPLACE PACKAGE tsh_audit AS

PROCEDURE insert_log (p_username  IN  VARCHAR2,
                      p_object    IN  VARCHAR2,
                      p_action    IN  VARCHAR2,
                      p_data      IN  CLOB);
               
END;
/

SHOW ERRORS


CREATE OR REPLACE PACKAGE BODY tsh_audit AS

PROCEDURE insert_log (p_username  IN  VARCHAR2,
                      p_object    IN  VARCHAR2,
                      p_action    IN  VARCHAR2,
                      p_data      IN  CLOB) IS
BEGIN
  INSERT INTO audit_logs (
    id,
    log_timestamp,
    username,
    object_name,
    action,
    data)
  VALUES (
    audit_logs_seq.NEXTVAL,
    SYSTIMESTAMP,
    p_username,
    p_object,
    p_action,
    p_data
  );
END;
               
END;
/
SHOW ERRORS

Trigger Script

A trigger must be defined for each audited table. This can be done using the following script.

SET SERVEROUTPUT ON SIZE 1000000
SET VERIFY OFF
SET TRIMSPOOL ON
DECLARE
  v_owner  VARCHAR2(30) := UPPER('&1');
  v_table  VARCHAR2(30) := UPPER('&2');

  CURSOR c_columns IS
    SELECT column_name
    FROM   all_tab_columns
    WHERE  owner      = v_owner
    AND    table_name = v_table;
BEGIN
  DBMS_OUTPUT.PUT_LINE('CREATE OR REPLACE TRIGGER ' || v_table || '_aiudr_trg');
  DBMS_OUTPUT.PUT_LINE('AFTER INSERT OR UPDATE OR DELETE ON ' || v_table);
  DBMS_OUTPUT.PUT_LINE('FOR EACH ROW');
  DBMS_OUTPUT.PUT_LINE('DECLARE');
  DBMS_OUTPUT.PUT_LINE('	PRAGMA AUTONOMOUS_TRANSACTION;');
  DBMS_OUTPUT.PUT_LINE('	v_action VARCHAR2(30) := ''NONE'';');
  DBMS_OUTPUT.PUT_LINE('BEGIN');
  DBMS_OUTPUT.PUT_LINE('	IF INSERTING THEN');
  DBMS_OUTPUT.PUT_LINE('		v_action := ''INSERT'';');
  DBMS_OUTPUT.PUT_LINE('	ELSIF UPDATING THEN');
  DBMS_OUTPUT.PUT_LINE('		v_action := ''UPDATE'';');
  DBMS_OUTPUT.PUT_LINE('	ELSIF DELETING THEN');
  DBMS_OUTPUT.PUT_LINE('		v_action := ''DELETE'';');
  DBMS_OUTPUT.PUT_LINE('	END IF;');
    
  DBMS_OUTPUT.PUT_LINE('	tsh_audit.insert_log (');
  DBMS_OUTPUT.PUT_LINE('		SYS_CONTEXT(''USERENV'',''CLIENT_IDENTIFIER''),');
  DBMS_OUTPUT.PUT_LINE('		''' || v_table || ''',');
  DBMS_OUTPUT.PUT_LINE('		v_action,');
  FOR cur_rec IN c_columns LOOP
    IF c_columns%ROWCOUNT > 1 THEN
      DBMS_OUTPUT.PUT_LINE('||');
    END IF;
    DBMS_OUTPUT.PUT('		''NEW.' || cur_rec.column_name || '=['' || :NEW.' || cur_rec.column_name || ' || ''] ' ||
                         'OLD.' || cur_rec.column_name || '=['' || :OLD.' || cur_rec.column_name || ' || '']'' ');
  END LOOP;
  DBMS_OUTPUT.NEW_LINE;
  DBMS_OUTPUT.PUT_LINE('	);');
  
  DBMS_OUTPUT.PUT_LINE('	COMMIT;');
  DBMS_OUTPUT.PUT_LINE('END;');
  DBMS_OUTPUT.PUT_LINE('/');
  DBMS_OUTPUT.PUT_LINE('SHOW ERRORS');
END;
/

The script should be saved on the filesystem as audit_trigger.sql and called as follows.

SQL> @audit_trigger my_schema my_table

The resulting trigger can be compiled into the schema.

Note the use of the SYS_CONTEXT('USERENV','CLIENT_IDENTIFIER') function. The value of the current user must be set in this context using the following procedure call.

DBMS_SESSION.SET_IDENTIFIER('USER_NAME');

This method is used rather than the USER function so that the system will also work where middle tier application users share a common DB user. In the case of separate DB users this can be altered to use the USER.

Test It

With the schema, package and triggers in place you can test the auditing process by inserting, updating and deleting from the audited tables then querying the audit_logs table.

Hope this helps. Regards Tim...

Back to the Top.