Другое

Система аудита данных: реализация и лучшие практики

Оптимальные подходы к реализации системы аудита для отслеживания изменений данных в Prisma. Сравнение методов, производительность и лучшие практики.

Как реализовать систему аудита для отслеживания изменений данных во всех таблицах базы данных?

Я хочу отслеживать, кто и когда создает, изменяет или удаляет сущности. Текущее решение с копированием полей createdBy, editedBy, deletedBy в каждую таблицу приведет к большому дублированию кода.

Текущая схема Prisma (только затронутые поля):

prisma
model UserAccount {
  id Int @id @unique @default(autoincrement()) @map("id")

  creatorAccount UserAccount? @relation("CreatorAccount", fields: [createdBy], references: [id])
  editorAccount  UserAccount? @relation("EditorAccount", fields: [editedBy], references: [id])
  deleterAccount UserAccount? @relation("DeleterAccount", fields: [deletedBy], references: [id])

  isDeleted      Boolean   @default(false) @map("is_deleted")
  deletedBy      Int       @map("deleted_by")
  deletedAt      DateTime? @map("deleted_at") @db.Timestamptz()
  deletionReason String?   @map("deletion_reason") @db.VarChar(128)

  createdUsers UserAccount[] @relation("CreatorAccount")
  editedUsers  UserAccount[] @relation("EditorAccount")
  deletedUsers UserAccount[] @relation("DeleterAccount")

  createdBy Int?      @map("created_by")
  createdAt DateTime @map("created_at") @db.Timestamptz()

  editedBy Int?      @map("edited_by")
  editedAt DateTime? @map("edited_at") @db.Timestamptz()

  @@map("user_accounts")
}

Рассматриваю несколько вариантов реализации:

  1. Отдельная таблица истории для каждой таблицы базы данных
  2. Одна общая таблица с полями oldData (JSON) и newData (JSON)
  3. Использование массива как поля в записи

Какой подход является наиболее оптимальным с точки зрения производительности, масштабируемости и поддержки? Какие существуют лучшие практики реализации аудита в базах данных?

Реализовать систему аудита для отслеживания изменений данных можно несколькими способами, но наиболее оптимальным подходом является использование отдельной общей таблицы с полями oldData и newData в формате JSON, так как он обеспечивает лучшую масштабируемость, производительность и упрощает поддержку кода.

Содержание


Анализ существующих подходов

Из исследования выявлено несколько распространенных подходов к реализации аудита баз данных:

  1. Триггеры базы данных - автоматическое создание записей аудита при изменении данных через триггеры
  2. Переопределение методов сохранения - перехват операций сохранения в коде приложения (как в EF Core)
  3. Нативные механизмы аудита - использование встроенных возможностей СУБД (Oracle Audit Trail, SQL Server Auditing)

Согласно документации AWS, аудит должен включать:

  • Исходные и обновленные значения данных
  • Процесс или хранимую процедуру, сделавшую изменение
  • Время и дату изменения

Сравнение трех основных методов

1. Отдельные таблицы истории для каждой таблицы

Преимущества:

  • Строгая типизация данных
  • Высокая производительность запросов
  • Простота понимания структуры

Недостатки:

  • Множество таблиц для поддержки
  • Дублирование кода
  • Сложность миграций
  • Проблемы с производительностью при большом количестве сущностей

2. Одна общая таблица с JSON-полями

Преимущества:

  • Единая структура для всех сущностей
  • Минимальный дублирование кода
  • Простота добавления новых сущностей
  • Хорошая масштабируемость

Недостатки:

  • Потеря строгой типизации
  • Сложность индексации
  • Потенциальные проблемы с производительностью запросов

3. Использование массивов как полей

Преимущества:

  • Компактное хранение истории
  • Эффективные запросы к истории изменений

Недостатки:

  • Ограниченная гибкость
  • Сложность работы с историческими данными
  • Проблемы с производительством при больших объемах

Рекомендация: Подход с JSON-полями оптимальен для большинства современных приложений, особенно при использовании Prisma, который хорошо работает с JSON-типами данных.

На основе исследования предлагается следующая архитектура:

sql
CREATE TABLE audit_logs (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    entity_type VARCHAR(100) NOT NULL,
    entity_id UUID NOT NULL,
    action_type VARCHAR(20) NOT NULL, -- 'CREATE', 'UPDATE', 'DELETE'
    old_data JSONB,
    new_data JSONB,
    user_id UUID,
    user_name VARCHAR(255),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    ip_address INET,
    session_id VARCHAR(255),
    additional_info JSONB
);

CREATE INDEX idx_audit_logs_entity ON audit_logs(entity_type, entity_id);
CREATE INDEX idx_audit_logs_created_at ON audit_logs(created_at);
CREATE INDEX idx_audit_logs_action_type ON audit_logs(action_type);

Эта архитектура предоставляет:

  • Единое хранилище для всех аудиторских записей
  • Гибкое хранение данных через JSONB
  • Эффективную индексацию
  • Дополнительные метаданные для безопасности

Реализация на Prisma

Базовая модель аудита

prisma
model AuditLog {
  id              String   @id @default(cuid())
  entityType      String
  entityId        String
  actionType      AuditActionType
  oldData         Json?
  newData         Json?
  userId          String?
  userName        String?
  createdAt       DateTime @default(now())
  ipAddress       String?
  sessionId       String?
  additionalInfo  Json?

  @@map("audit_logs")
}

enum AuditActionType {
  CREATE
  UPDATE
  DELETE
}

Базовый интерфейс для аудируемых сущностей

prisma
interface AuditableEntity {
  id String @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt
}

Механизм автоматического аудита

typescript
// utils/audit.ts
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export async function createAuditLog(data: {
  entityType: string;
  entityId: string;
  actionType: 'CREATE' | 'UPDATE' | 'DELETE';
  oldData?: any;
  newData?: any;
  userId?: string;
  userName?: string;
  ipAddress?: string;
  sessionId?: string;
  additionalInfo?: any;
}) {
  return await prisma.auditLog.create({
    data: {
      ...data,
      createdAt: new Date(),
    },
  });
}

// middleware/auditMiddleware.ts
export function withAudit(modelName: string) {
  return async (args: any) => {
    const before = args.args.where ? await prisma[modelName].findFirst({
      where: args.args.where,
    }) : null;

    const result = await(args);

    const after = args.args.where ? await prisma[modelName].findFirst({
      where: args.args.where,
    }) : result;

    const actionType = 
      !before ? 'CREATE' :
      !after ? 'DELETE' : 'UPDATE';

    await createAuditLog({
      entityType: modelName,
      entityId: after?.id || before?.id,
      actionType,
      oldData: before,
      newData: after,
    });

    return result;
  };
}

Интеграция с Prisma Client

typescript
// Enhanced Prisma Client
import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

// Override create method
prisma.$use(async (params, next) => {
  if (params.model && ['User', 'Product', 'Order'].includes(params.model)) {
    const before = params.action === 'update' 
      ? await prisma[params.model].findFirst({
          where: params.args.where,
        })
      : null;

    const result = await next(params);

    const after = params.action === 'update'
      ? await prisma[params.model].findFirst({
          where: params.args.where,
        })
      : result;

    const actionType = 
      params.action === 'create' ? 'CREATE' :
      params.action === 'delete' ? 'DELETE' : 'UPDATE';

    await createAuditLog({
      entityType: params.model,
      entityId: result.id,
      actionType,
      oldData: before,
      newData: after,
      userId: 'current-user-id', // Get from context
      userName: 'current-user-name',
    });

    return result;
  }

  return next(params);
});

Оптимизация производительности

Индексация и оптимизация запросов

sql
-- Композитные индексы для частых запросов
CREATE INDEX idx_audit_logs_entity_time ON audit_logs(entity_type, entity_id, created_at DESC);
CREATE INDEX idx_audit_logs_user_time ON audit_logs(user_id, created_at DESC);

-- Частичная индексация для уменьшения размера
CREATE INDEX idx_audit_logs_recent ON audit_logs(created_at DESC) WHERE created_at > NOW() - INTERVAL '30 days';

Партиционирование больших таблиц

sql
-- Партиционирование по времени
CREATE TABLE audit_logs (
  -- ... колонки
) PARTITION BY RANGE (created_at);

-- Создание партиций
CREATE TABLE audit_logs_2024 PARTITION OF audit_logs
  FOR VALUES FROM ('2024-01-01') TO ('2025-01-01');

Асинхронное логирование

typescript
// Использование очереди для асинхронного аудита
const auditQueue = new Queue('audit');

prisma.$use(async (params, next) => {
  // ... логика определения изменений
  
  // Асинхронное добавление в очередь
  auditQueue.add({
    entityType: params.model,
    entityId: result.id,
    actionType,
    oldData: before,
    newData: after,
  });

  return result;
});

Масштабируемость и поддержка

Архитектура с микросервисами

При использовании микросервисной архитектуры рекомендуется:

  1. Централизованный сервис аудита - отдельный сервис, отвечающий за сбор и хранение аудиторских данных
  2. Event Sourcing - публикация событий аудита через брокер сообщений
  3. Агрегация данных - периодическая агрегация и архивация старых данных
typescript
// Пример использования брокера сообщений
import { EventEmitter } from 'events';

const auditEmitter = new EventEmitter();

prisma.$use(async (params, next) => {
  // ... логика определения изменений
  
  // Публикация события аудита
  auditEmitter.emit('audit', {
    entityType: params.model,
    entityId: result.id,
    actionType,
    oldData: before,
    newData: after,
    timestamp: new Date(),
  });

  return result;
});

Архивация и очистка данных

typescript
// Регулярная архивация старых данных
async function archiveOldAuditLogs() {
  const oneYearAgo = new Date();
  oneYearAgo.setFullYear(oneYearAgo.getFullYear() - 1);

  const oldLogs = await prisma.auditLog.findMany({
    where: {
      createdAt: {
        lt: oneYearAgo,
      },
    },
  });

  // Перенос в архивную таблицу
  await prisma.auditLogArchive.createMany({
    data: oldLogs,
  });

  // Удаление из основной таблицы
  await prisma.auditLog.deleteMany({
    where: {
      createdAt: {
        lt: oneYearAgo,
      },
    },
  });
}

Лучшие практики аудита

1. Минимизация объема данных

Храните только необходимые данные для аудита. Согласно исследованию, можно хранить только измененные поля, а не всю сущность.

2. Безопасность и конфиденциальность

typescript
// Маскирование чувствительных данных
function maskSensitiveData(data: any, schema: any) {
  const masked = { ...data };
  
  Object.keys(schema).forEach(key => {
    if (schema[key].sensitive) {
      masked[key] = '***MASKED***';
    }
  });
  
  return masked;
}

3. Валидация целостности данных

typescript
// Хранение хеша данных для проверки целостности
import { createHash } from 'crypto';

function calculateDataHash(data: any): string {
  return createHash('sha256')
    .update(JSON.stringify(data))
    .digest('hex');
}

4. Мониторинг и аналитика

typescript
// Аналитика изменений
async function getEntityChangeStats(entityType: string, period: string) {
  return await prisma.auditLog.groupBy({
    by: ['actionType'],
    where: {
      entityType,
      createdAt: {
        gte: new Date(Date.now() - parsePeriod(period)),
      },
    },
    _count: {
      actionType: true,
    },
  });
}

5. Регуляторное соответствие

Для соответствия GDPR, HIPAA и другим требованиям:

  • Автоматическое удаление персональных данных
  • Предоставление функций экспорта данных субъекта
  • Журнализация всех попыток доступа к данным

Заключение

На основе анализа различных подходов к реализации аудита баз данных можно сделать следующие выводы:

  1. Оптимальный подход: Использование единой таблицы с JSON-полями обеспечивает лучший баланс между гибкостью, производительностью и сложностью поддержки. Этот подход особенно хорошо подходит для современных ORM, таких как Prisma.

  2. Ключевые преимущества:

    • Минимизация дублирования кода
    • Легкое добавление новых аудируемых сущностей
    • Хорошая масштабируемость
    • Поддержка сложных сценариев аудита
  3. Рекомендации по реализации:

    • Используйте триггеры или middleware для автоматического логирования
    • Реализуйте асинхронную обработку аудиторских событий
    • Внедрите индексацию и партиционирование для больших объемов данных
    • Добавьте механизмы архивации и очистки старых данных
  4. Дополнительные соображения:

    • Для высоконагруженных систем рассмотрите распределенное решение
    • Внедрите мониторинг и аналитику по изменениям данных
    • Обеспечьте соответствие регуляторным требованиям

Представленный подход позволяет создать надежную, масштабируемую систему аудита, которая будет эффективно работать как в небольших, так и в крупных проектах на базе Prisma.

Источники

  1. Best way to design a database and table to keep records of changes? - Database Administrators Stack Exchange

  2. Seeking Best Practices for Efficient Logging and Auditing in a Small Team Environment - Reddit

  3. What is the proper way to save audit information in a database - Software Engineering Stack Exchange

  4. How to track data changes using database auditing techniques - ApexSQL

  5. EF Core: How to implement basic Auditing on your Entities - DEV Community

  6. How to Implement Audit Trail in ASP.NET Core with EF Core - Anton Dev Tips

  7. Best design for a changelog / auditing database table? - Stack Overflow

  8. Effective strategy for leaving an audit trail/change history for DB applications - Stack Overflow

  9. Best practice 4.5 – Track data and database changes - AWS Well-Architected

  10. Database Design for Audit Logging - Vertabelo

Авторы
Проверено модерацией
Модерация