PostgreSQL ile Merkezi Audit Log: Kim, Ne Zaman, Neyi Değiştirdi?

Uygulamanızda yapılan tüm değişiklikleri otomatik olarak kaydetmek ister misiniz? PostgreSQL trigger ile tek bir fonksiyonla tüm tablolarınız için audit log kurma rehberi.

Sorun: “Bu Veriyi Kim Değiştirdi?”

Kurumsal bir uygulamada kaçınılmaz sorular:

  • “Bu siparişin durumunu kim değiştirdi?”
  • “Stok miktarı ne zaman güncellendi?”
  • “Kullanıcı rolü kim tarafından değiştirildi?”

Loglama yoksa cevap: “Bilmiyoruz.”

Audit log, uygulamanızdaki her değişikliğin kaydını tutar. Kim, ne zaman, neyi, nasıl değiştirdi — hepsi kayıt altında.

Çözüm: Tek Bir Trigger Fonksiyonu

Her tablo için ayrı ayrı log yazmak yerine, tek bir generic fonksiyon yazıyoruz ve istediğimiz tablolara bağlıyoruz:

1. Log Tablosu

CREATE TABLE audit_log (
    id BIGSERIAL PRIMARY KEY,
    user_email TEXT,              -- Kim yaptı?
    table_name TEXT NOT NULL,     -- Hangi tablo?
    action TEXT NOT NULL,         -- INSERT / UPDATE / DELETE
    record_id TEXT,               -- Hangi kaydı?
    old_data JSONB,               -- Eski değerler
    new_data JSONB,               -- Yeni değerler
    changed_fields TEXT[],        -- Sadece değişen alanlar
    created_at TIMESTAMPTZ DEFAULT NOW()
);

2. Trigger Fonksiyonu

Bu fonksiyon herhangi bir tablodaki değişikliği otomatik olarak audit_log’a yazar:

CREATE OR REPLACE FUNCTION audit_trigger_fn()
RETURNS TRIGGER AS $$
DECLARE
    v_changed TEXT[];
    v_key TEXT;
BEGIN
    -- UPDATE ise sadece değişen alanları bul
    IF TG_OP = 'UPDATE' THEN
        FOR v_key IN SELECT jsonb_object_keys(to_jsonb(NEW))
        LOOP
            IF to_jsonb(OLD) ->> v_key IS DISTINCT FROM to_jsonb(NEW) ->> v_key THEN
                v_changed := array_append(v_changed, v_key);
            END IF;
        END LOOP;
        
        -- Hiçbir şey değişmediyse log oluşturma
        v_changed := array_remove(v_changed, 'updated_at');
        IF v_changed IS NULL OR array_length(v_changed, 1) IS NULL THEN
            RETURN NEW;
        END IF;
    END IF;

    -- Audit kaydı yaz
    INSERT INTO audit_log (
        user_email, table_name, action, record_id,
        old_data, new_data, changed_fields
    ) VALUES (
        COALESCE(auth.jwt() ->> 'email', 'system'),
        TG_TABLE_NAME,
        TG_OP,
        CASE WHEN TG_OP = 'DELETE' THEN OLD.id::TEXT ELSE NEW.id::TEXT END,
        CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE to_jsonb(OLD) END,
        CASE WHEN TG_OP = 'DELETE' THEN NULL ELSE to_jsonb(NEW) END,
        v_changed
    );
    
    RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

3. Tablolara Bağlama

-- İstediğiniz tablolara tek satırla bağlayın
CREATE TRIGGER audit_siparisler
    AFTER INSERT OR UPDATE OR DELETE ON orders
    FOR EACH ROW EXECUTE FUNCTION audit_trigger_fn();

CREATE TRIGGER audit_stok
    AFTER INSERT OR UPDATE OR DELETE ON inventory
    FOR EACH ROW EXECUTE FUNCTION audit_trigger_fn();

-- İstediğiniz kadar tablo ekleyebilirsiniz

Ne Görürsünüz?

Audit log’a baktığınızda şöyle bir tablo görürsünüz:

Tarih              │ Kullanıcı       │ Tablo    │ İşlem  │ Değişen
───────────────────┼─────────────────┼──────────┼────────┼──────────
12 Nis 14:30       │ ahmet@firma.com │ orders   │ UPDATE │ status
12 Nis 14:25       │ mehmet@firma.com│ inventory│ UPDATE │ miktar
12 Nis 14:20       │ ayse@firma.com  │ orders   │ INSERT │ —

Artık “kim değiştirdi?” sorusunun cevabı var.

Sorgulama Örnekleri

-- "Bu siparişe ne oldu?"
SELECT created_at, user_email, action, changed_fields
FROM audit_log
WHERE table_name = 'orders' AND record_id = '1234'
ORDER BY created_at DESC;

-- "Bugün kim ne yaptı?"
SELECT user_email, table_name, action, COUNT(*)
FROM audit_log
WHERE created_at >= CURRENT_DATE
GROUP BY user_email, table_name, action;

Bu Fonksiyonun Güçlü Yanları

  • Tek fonksiyon — istediğiniz tabloya bağlayın, hepsinde çalışır
  • Akıllı: UPDATE’lerde sadece değişen alanları kaydeder
  • Sessiz: Hiçbir şey değişmediyse log oluşturmaz (gereksiz gürültü yok)

Dikkat Edilecekler

  • Toplu işlemler: 5000 satır güncellediğinizde 5000 log satırı oluşur. Toplu işlemler için özet log fonksiyonu kullanın.
  • Tablo büyümesi: Audit tablosu zamanla büyür. 6 aydan eski logları temizleyin.
  • Performans: Her INSERT/UPDATE/DELETE’de 1-2ms ek süre ekler.

Bu Yaklaşım Temel Senaryolar İçin Yeter

5-10 tablo için bu yapı mükemmel çalışır. Ama 30+ tablo, toplu senkronizasyon (batch) operasyonları, ve admin panel entegrasyonu (filtreleme, diff viewer, export) gerektiğinde daha ileri çözümler gerekir.

Biz 30+ tabloluk bir yapıda bu sistemi çalıştırıyoruz. İhtiyacınız olursa deneyimimizden faydalanabilirsiniz.


Bu yazı AstaFlow Case Study serisinin bir parçasıdır.