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

Şirket vericisini kim bozdu? Kim, ne zaman, neyi değiştirdi? PostgreSQL trigger ve jsonb mimarisiyle tek fonksiyondan devasa Audit-Log (Denetim İzi)...

İş Problemi: “O Ürünün Fiyatını Kim Artırdı?”

ERP veya B2B yönetim sistemlerinde patronların klasik bir fırçası vardır: “Dün bu mamanın fiyatı 1500 TL idi, bugün sabah müşteri fişine 1900 TL’den basılmış, kim değiştirdi bunu?!”

Eğer urunler tablonuzda sadece fiyat sütunu ve en fazla updated_at sütunu varsa patrona vereceğiniz cevap koca bir iç geçirmeden ibarettir. Geleneksel web geliştiriciler üşendikleri için veri tablosuna updated_by_user diye bir sütun ekler, işi çözerler. Fakat ya müşteri ürünün 10 gün önceki durumunu, geçmiş rotasını bilmek isterse? “Fiyat 1200’den 1500’e Cuma günü çıkmış, Pazartesi 1900 olmuş” demek istiyorsa?

Çözüm: Merkezi Loglama Tablosu (Audit Logging) Uygulamada birisi bir şeyi sildiğinde, değiştirdiğinde veya eklediğinde arka planda dedektif gibi bunu yazan bir veritabanı Trigger’ına ihtiyacınız vardır.

Çözüm: Zekice Tasarlanmış JSONB Trigger (Supabase Özel)

Auditing (Denetim İzleme) yazmak çok teferruatlı bir iştir. Fiyatı değişen ürünün adını AuditLog tablosuna VARCHAR olarak yazarsanız esneklik kaybolur. PostgreSQL’in efsanevi JSONB veritipini kullanarak, o andaki tüm kaydı tek hücreye “Fotoğraf (Snapshot)” olarak hapsederiz.

Ve dahası Supabase kurgusunda auth.uid() yakalayarak “Hangi kullanıcı değiştirdi?” sorusunu da çözeriz:

1. Master Denetim Tablomuzu Oluşturun

CREATE TABLE audit_log (
    id BIGSERIAL PRIMARY KEY,
    islem_tipi VARCHAR(10) NOT NULL,      -- INSERT / UPDATE / DELETE
    tablo_adi VARCHAR(50) NOT NULL,       -- Hangi tabloda oldu?
    kayit_id TEXT,                        -- Hangi ID'li satırla oynandı?
    islemi_yapan UUID,                    -- Supabase Auth User ID (Kim?)
    eski_veriler JSONB,                   -- İşlemden önceki durumun tam fotoğrafı
    yeni_veriler JSONB,                   -- İşlemden sonraki durumun tam fotoğrafı
    degisen_alanlar TEXT[],               -- Sadece değişmiş Kolon İsimleri (Array)
    islem_tarihi TIMESTAMPTZ DEFAULT NOW()
);

2. Akıllı PostgreSQL Trigger Fonksiyonu

Bu fonksiyon herhangi bir tablodaki değişikliği otomatik tarayıp JSON’a döker. (Supabase Supavisor mimarisiyle test edildi, çok stabildir).

CREATE OR REPLACE FUNCTION astaflow_global_auditor()
RETURNS TRIGGER AS $$
DECLARE
    degisenler TEXT[];
    key_val TEXT;
BEGIN
    -- SADECE DEĞİŞEN KOLONLARI BULMA ZEKASI (Performans için şarttır!)
    IF TG_OP = 'UPDATE' THEN
        FOR key_val IN SELECT jsonb_object_keys(to_jsonb(NEW))
        LOOP
            IF to_jsonb(OLD) ->> key_val IS DISTINCT FROM to_jsonb(NEW) ->> key_val THEN
                degisenler := array_append(degisenler, key_val);
            END IF;
        END LOOP;
        
        -- ÖNEMLİ: Eğer birisi sadece formda 'Kaydet'e bastı ama değerler aynıysa, log atma!
        degisenler := array_remove(degisenler, 'updated_at'); 
        IF degisenler IS NULL OR array_length(degisenler, 1) IS NULL THEN
            RETURN NEW; 
        END IF;
    END IF;

    -- Tabloya Kanıt Gönderiliyor
    INSERT INTO audit_log (
        islem_tipi, tablo_adi, kayit_id, islemi_yapan, 
        eski_veriler, yeni_veriler, degisen_alanlar
    ) VALUES (
        TG_OP, 
        TG_TABLE_NAME,
        CASE WHEN TG_OP = 'DELETE' THEN OLD.id::TEXT ELSE NEW.id::TEXT END,
        auth.uid(), -- Supabase sihirli kodu
        CASE WHEN TG_OP = 'INSERT' THEN NULL ELSE to_jsonb(OLD) END,
        CASE WHEN TG_OP = 'DELETE' THEN NULL ELSE to_jsonb(NEW) END,
        degisenler
    );
    
    RETURN CASE WHEN TG_OP = 'DELETE' THEN OLD ELSE NEW END;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

3. İstediğiniz Tabloya Bağlama

CREATE TRIGGER t_urunler_audit
AFTER INSERT OR UPDATE OR DELETE ON siparis_kalemleri
FOR EACH ROW EXECUTE FUNCTION astaflow_global_auditor();

Performans Raporlama (Supabase Dashboard Görüntüsü)

Sonucu incelediğinizde şöyle bir güzellik olur:

SELECT * FROM audit_log WHERE kayit_id = '5f4d...'

Sorgu Sonucu:

  • İşlem Tipi: UPDATE
  • Tablo: siparis_kalemleri
  • Yapan: 4ba2... (Kasiyer Ayşe)
  • Eski Veri: {"fiyat": 1500, "miktar": 2, "odeme": "nakit"}
  • Yeni Veri: {"fiyat": 1900, "miktar": 2, "odeme": "nakit"}
  • Değişen Alanlar: {"fiyat"}

Mükemmel İz sürme!

Edge Cases (Ölümcül Riskler) ve Diskin Şişmesi

Denetim masası çok havalıdır ama veritabanını çok kolay batırabilir:

  1. Disk İsrafı (Storage Bloat): AstaFlow veritabanında toplu bir CSV İçe Aktarma (Bulk Insert) yaptık. 50.000 satır müşteri içeri aktarıldı. Ne oldu? Her satır için INSERT TRIGGER çalıştı ve AuditLog tablomuz anında 50.000 satır + Devşirme JSONB nesneleri ile dolup kapasite yedi! Çözüm: Batch (Toplu) transfer araçları kullanırken Trigger’ların geçici olarak askıya alınması veya sadece kritik CRUD için log kurallarının sınırlandırılmasıdır. Asla her logu sonsuza kadar tutmayın, pg_cron eklentisiyle “6 aydan eski logları sil” rutini yazın.
  2. Kişisel Veri İhlalleri (KVKK/GDPR): Eğer kullanıcılar tablosunu Auditing içine alırsanız, kullanıcının “Eski Şifresi”, “Kredi Kartı Tokeni” gibi gizli JSON alanlarını da açık-text olarak old_data/new_data bloğuna sokarsınız. Bu bir yasal güvenlik ihlalidir.

Bu Bilgiyi Nereden Biliyoruz? (Kaynaklar)

📚 İlgili Yazılar