script generador de trigger para auditoría en Postgres
Al terminar de seguir estos pasos, tendremos una tabla dedicada exclusivamente para el registro de las operaciones en nuestra base de datos. Los datos a ser guardados son:
- Nombre de la tabla afectada.
- id registro afectado
- Nombre del campo afectado.
- Valor anterior del cambio.
- Nuevo valor del campo.
- Dirección ip del cliente que realizó el cambio.
- Fecha y hora de cambio (con huso horario, timestamptz)
- Tipo de operación
Requisito para el uso del script es tener instalado TemplateToolkit, en Debian el paquete es libtemplate-perl
Entonces procedemos a crear la tabla de auditoría.
CREATE TABLE auditoria(
id serial NOT NULL,
nombre_tabla varchar,
id_campo int8,
nombre_campo varchar,
valor_anterior varchar,
valor_nuevo varchar,
fecha_operacion timestamptz,
tipo_operacion char(1) DEFAULT 1, -- i = insert ; u = update
ip inet,
CONSTRAINT auditoria_pkey PRIMARY KEY (id),
CONSTRAINT auditoria_tipo_operacion_check CHECK (tipo_operacion = 'i'::bpchar OR tipo_operacion = 'u'::bpchar)
)
WITHOUT OIDS;
ALTER TABLE auditoria OWNER TO postgres;
COMMENT ON COLUMN auditoria.tipo_operacion IS 'i = insert ; u = update ';
El lenguaje usado para este trigger es pl/pgsql , así que para trabajar con él debemos primero agregarlo a la base de datos. Con el front-end pgadmin3 es mas que suficiente colocando ‘plpgsql’ en el campo de nombre de nuevo lenguaje en la base de datos a aplicar el trigger.
Ahora usamos el siguiente script para generar el trigger correspondiente para cada tabla a ser auditada.
#ARCHIVO: genera_trigger_auditoria.pl #!/usr/bin/perl
use Template ;
$trigger = "genera_trigger_auditoria/trigger.tt" ;
$archivo_salida = $ARGV[0] . "-audit.trigger";
for ( $i = 1 ; $i <= $#ARGV ; $i++ ){
push(@campos , $ARGV[$i]);
}
$variables = {
tabla => $ARGV[0],
campos => \@campos
};
$template = Template->new();
$template->process($trigger, $variables , "triggers/".$archivo_salida)
|| die "Hubo un error al generar la plantilla " . $template->error() . "\n";
#genera_trigger_auditoria/trigger.tt
BEGIN
IF (TG_OP = 'DELETE') THEN
RAISE EXCEPTION 'NO SE PERMITEN BORRADOS FISICOS';
END IF ;
IF (TG_OP = 'UPDATE') THEN
[% FOREACH cp = campos %]
IF (OLD.[% cp %] <> NEW.[% cp %]) THEN
IF ('[% cp %]' = 'eliminado') THEN
insert into auditoria(nombre_tabla,id_campo,nombre_campo,valor_anterior,valor_nuevo,ip,fecha_operacion,tipo_operacion)
values( '[% tabla %]', OLD.id, '[% cp %]', OLD.[% cp %]::int, NEW.[% cp %]::int, inet_client_addr(), current_timestamp, 'u');
ELSE
insert into auditoria(nombre_tabla,id_campo,nombre_campo,valor_anterior,valor_nuevo,ip,fecha_operacion,tipo_operacion)
values( '[% tabla %]', OLD.id, '[% cp %]', OLD.[% cp %], NEW.[% cp %], inet_client_addr(), current_timestamp, 'u');
END IF;
END IF;
[% END %]
return new ;
END IF ;
IF (TG_OP = 'INSERT') THEN
[% FOREACH cp = campos %]
IF (NEW.[% cp %] IS NOT null) THEN
IF ('[% cp %]' = 'eliminado') THEN
insert into auditoria(nombre_tabla,id_campo,nombre_campo,valor_anterior,valor_nuevo,ip,fecha_operacion,tipo_operacion)
values( '[% tabla %]', NEW.id, '[% cp %]', null, NEW.[% cp %]::int, inet_client_addr(), current_timestamp, 'i');
ELSE
insert into auditoria(nombre_tabla,id_campo,nombre_campo,valor_anterior,valor_nuevo,ip,fecha_operacion,tipo_operacion)
values( '[% tabla %]', NEW.id, '[% cp %]', null, NEW.[% cp %], inet_client_addr(), current_timestamp, 'i');
END IF;
END IF;
[% END %]
return new ;
end IF ;
END;
La sintaxis de uso es la siguiente:
$ perl genera_trigger_auditoria.pl NOMBRE_TABLA campo1 campo2 campoN
Con esto se crea un archivo con el formato <triggers/NOMBRE_TABLA-audit.trigger>
El contenido de este archivo debemos agregarlo a la lista de triggers disponibles en la base de datos, para posteriormente ser asociados a tabla/eventos. Esto lo hacemos usando pgadmin3 de la siguiente manera:
NOMBRE_BASE_DATOS > schemas > public > Trigger functions
menú: Edit > create
En language colocamos plpgsql y en la pestaña de parametros colocamos el texto generado por el script usado anteriormente.
Ahora asociamos la tabla/evento al trigger, hay que hacer esto por cada tabla.
NOMBRE_BASE_DATOS > schemas > public > tables > NOMBRE_TABLA > triggers
menú: Edit > create
- Verificamos el la opción “row” esté marcada.
- En el campo “trigger” asociamos el trigger recien creado.
- Marcamos la opción para que se dispare antes (BEFORE) de realizar la sentencia.
- Marcamos todos los eventos: INSERT, UPDATE, DELETE.
Ya con esto debería estar funcionado.
Si tienen alguna sugerencia de como se puede guardar un usuario enviado a nivel de aplicación, los comentarios son bien recibidos.
PD: El trigger evita la operación de “DELETE” pues se recomienda trabajar con borrado lógico, es decir, un campo extra boleano para cada tabla para identificar si está eliminado o no.

:-**************
:$ Sorry, sé que este blog es para “asuntos técnicos”.
Algunos amigos al revisar este post me dieron algunas sugerencias, por ejemplo, Miguel Ortega que me comentó el la capturación de la ip es en vano si se habla de una aplicación de php administrando los usuarios a nivel de aplicación, es decir, con usuarios asignados en tablas de la base de datos, y no con usuarios a nivel de base de datos, el problema es que la ip que guarda a la hora de hacer la auditoría es la del servidor Web (127.0.0.1 si está en el mismo equipo que el manejador de base de datos), y no la del usuario (la maquina que se usa para hacer la visita a la aplicación).
Esta es una solución no tan elegante, pero funciona: http://www.forosdelweb.com/f99/auditoria-datos-funciones-disparadores-464837/
PD: se me olvidó poner esto:
después de esto tu sistema NO se va a poner 99% mas lento por que todas las transacciones activan el trigger y es otra operación, de hecho el rendimiento se ve poco afectado.
hola me pareció muy interesante, pero no lo puedo probar no se donde descargar el TemplateToolkit paquete libtemplate-perl
Si usas debian es el paquete libtemplate-perl, es decir, lo instalas de la siguiente forma:
aptitude install libtemplate-perl
Si no usas debian o alguna distribución que use dpkg usa CPAN, instala así:
cpan Template::Toolkit
Y tengo una solución aplicada a mi necesidad, espero les sea de utilidad. Suponiendo que se tiene una tabla de auditoria con la misma estructura que una tabla normal de su base de datos adicionando un campo donde se guarda la operacion realizada sobre cada registro. la solución es la siguiente:
CREATE OR REPLACE FUNCTION crear_trigger(character varying, character varying, character varying)
RETURNS integer AS
$BODY1$
DECLARE
n integer;
nombre VARCHAR(200);
cadena VARCHAR(8000);
aux VARCHAR(1000);
aux2 VARCHAR(1000);
aux3 VARCHAR(20);
esquema varchar(50);
tabla varchar(50);
tipo varchar(1);
campo varchar(50);
tipoc VARCHAR(10);
campos CURSOR IS SELECT a.attname
FROM pg_class c, pg_attribute a, pg_type t, pg_catalog.pg_namespace e
WHERE c.relname = ‘aud_’||$2
AND a.attnum > 0
AND a.attrelid = c.oid
AND a.atttypid = t.oid
and c.relnamespace=e.oid
and e.nspname =$1;
BEGIN
esquema:=$1;
tabla:=$2;
tipo:=$3;
IF UPPER(TIPO)=’I’ OR UPPER(TIPO)=’U’ THEN
tipoc=’new.’;
ELSE IF UPPER(TIPO)=’D’ THEN
tipoc=’old.’;
END IF;
END IF;
cadena=’
CREATE OR REPLACE FUNCTION ‘;
nombre:=lower(esquema||’.f_’||esquema||’_'||tipo||’_aud_’||tabla)||’()’;
cadena:=cadena||nombre||’
RETURNS “trigger” AS
‘;
cadena:=cadena||’$BODY$
BEGIN
‘;
cadena:=cadena||’INSERT INTO ‘||esquema||’.aud_’||tabla||’(‘;
aux:=”;
aux2:=”;
aux3:=”;
OPEN campos;
LOOP
FETCH campos INTO campo;
IF campo IS NOT NULL AND campo not like ‘%_operacion’ THEN
aux:=aux||campo||’, ‘;
aux2:=aux2||tipoc||campo||’, ‘;
END IF;
IF campo like ‘%_operacion’THEN
aux3:=campo;
END IF;
EXIT WHEN NOT FOUND;
END LOOP;
CLOSE campos;
n:=octet_length(aux);
aux:= substring(aux,0,n);
n:=octet_length(aux2);
aux2:= substring(aux2,0,n);
cadena:=cadena||aux||aux3||’)
VALUES(‘||aux2||””||UPPER(tipo)||”’);
‘;
cadena:=cadena||’RETURN ‘||substr(tipoc,1,length(tipoc)-1)||’;
END;
$BODY$
LANGUAGE ”plpgsql” VOLATILE;
‘;
cadena:=cadena||’CREATE TRIGGER tr_’||LOWER(tipo)||’_'||tabla||’
AFTER ‘;
IF UPPER(TIPO)=’I’ THEN
cadena:=cadena||’INSERT
‘;
ELSE IF UPPER(TIPO)=’U’ THEN
cadena:=cadena||’UPDATE
‘;
ELSE IF UPPER(TIPO)=’D’ THEN
cadena:=cadena||’DELETE
‘;
END IF;
END IF;
END IF;
cadena:=cadena||’ ON ‘||esquema||’.'||tabla||’
FOR EACH ROW
EXECUTE PROCEDURE ‘||nombre||’;
‘;
RAISE NOTICE ‘%’,cadena;
IF N=0 THEN
return 0;
ELSE return 1;
END IF;
END;
$BODY1$
LANGUAGE ‘plpgsql’ VOLATILE;
ALTER FUNCTION crear_trigger(character varying, character varying, character varying) OWNER TO postgres;
——-
para hacer el llamado es de la siguiente manera
select public.crear_trigger(‘esquema’,'tabla’,'OP’);
donde OP puede ser I, U ó D para indicar la operación sobre el registro, inserción, actualización o eliminación.
Humberto Prada Niño
Ingeniero de Sistemas
humbertprada@hotmail.com
ejemplo del comentario anterior:
create table uno(codigo numeric(10,0) primary key, nombre varchar(100));
create table aud_uno(codigo numeric(10,0) primary key, nombre varchar(100), operacion varchar(1));
select crear_trigger(‘public’,'uno’,'I’);
select crear_trigger(‘public’,'uno’,'U’);
select crear_trigger(‘public’,'uno’,'D’);
de esta manera genera los trigger para la auditoria
Humberto Prada Niño
Ingeniero de Sistemas
humbertprada@hotmail.com
Excelente explicacion
Muy buen articulo
hola, tengo un problema con los triggers de postgres.
He creado un trigger al cual le paso por parametro el campo indice de la tabla. Por ejemplo ‘id_cliente’.
Dentro del trigger necesito operar con ese campo, de la forma
NEW.id_cliente, pero no se como enlazar el NEW con el id_cliente.
lo he intentado de varias maneras pero nada, tipo:
NEW.||TG_ARGV[0]
Alguien me podria ayudar!!!!!!!!!
Gracias.