본문으로 건너뛰기

Audit 모듈

📝 초안 (Draft)

이 문서는 검토 중입니다. 내용이 변경될 수 있습니다.

관리 표면의 권한·모델 변경·커스텀 액션을 정책에 따라 자동 기록하고 준수 상태를 점검하는 감사 로그 Core 모듈입니다.

개요​

Audit 모듈은 모델의 CRUD 작업을 자동으로 추적하여 감사 로그를 생성합니다. 관리 Resource에 등록된 모델은 기본적으로 자동 편입되며, 관리 표면 밖의 모델은 Auditable Trait으로 명시 편입할 수 있습니다.

┌─────────────────────────────────────────────────────────────┐
│ Audit 모듈 │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Model (관리 Resource 자동 편입 / Auditable Trait) │ │
│ │ │ │
│ │ create() ──┐ │ │
│ │ update() ──┼──► AuditObserver ──► AuditLog │ │
│ │ delete() ──┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ 기록 정보: │
│ • 이벤트 (created, updated, deleted) │
│ • 변경 전/후 값 │
│ • 실행 사용자, 테넌트, IP, URL │
└─────────────────────────────────────────────────────────────┘

핵심 컴포넌트​

컴포넌트역할
Auditable모델에 적용하는 Trait
AuditableInterfaceAuditable 모델 계약
AuditObserver모델 이벤트 감지 및 로그 생성
AuditLog감사 로그 저장 모델
AdminSurfaceRegistry등록된 관리 Resource·모델·권한 게이트 인벤토리
AutoAuditRegistrar관리 표면 모델 자동 감사 편입, 이중 기록 방지
AuditPolicyResolver프로젝트·독립사이트별 기록 강도 해석

관리 표면 자동 거버넌스​

Filament 관리 Resource를 등록하면 모델은 CORE_AUDIT_AUTO_ENROLL=true(기본값)에서 자동으로 감사 대상이 됩니다. 이미 Auditable 또는 플러그인 전용 이력으로 기록하는 모델은 한 번만 기록합니다.

관리 Resource의 권한 계약도 같은 인벤토리에서 확인합니다.

  • 표준: BaseAdminResource 상속 또는 HasFeaturePermissionCheck 사용
  • 프로젝트 고유 복합 게이트: 비어 있지 않은 adminSurfaceGateExceptionReason()으로 사유 명시
  • 모델 감사 제외: auditExempt(): ?string으로 사유 명시
  • 액션 감사 제외: ->auditExempt('사유')

빈 사유나 사람만 읽을 수 있는 주석은 예외 계약으로 인정하지 않습니다. 모델 제외 사유가 비어 있거나 호출에 실패하면 진단은 실패하되 감사 기록은 계속합니다.

기록 정책​

정책모델 CUD커스텀 액션단건 열람
minimal✅——
standard(기본)✅✅기본 Platform 패널
strict✅✅모든 관리 패널

독립사이트의 settings.security.audit.level이 프로젝트의 CORE_AUDIT_POLICY보다 우선하며, 미설정이면 상위 SaaS 설정과 프로젝트 기본값을 차례로 상속합니다. 표준 CRUD Action은 모델 이벤트와 중복되므로 액션 자동 기록에서 제외합니다. 관리 커스텀 액션은 정상 실행이 끝난 뒤에만 기록됩니다.

배포 전 준수 점검​

php artisan admin-surface:audit
종료 코드의미
0권한 게이트·감사 편입 준수
1무가드 Resource, 미편입 모델 또는 사유 없는 opt-out 발견
2Filament 관리 표면 인벤토리 미가용

이 명령은 읽기 전용입니다. 별도 파일 캐시를 만들지 않으며 admin-surface:cache 명령도 없습니다. 기존 프로젝트가 비표준 Resource의 사유를 주석으로만 남겼다면 adminSurfaceGateExceptionReason()으로 이관할 때까지 1이 나올 수 있습니다.

감사 저장 실패는 관리 업무를 막지 않습니다. 대신 동일 원인을 기본 10분 동안 묶어 Platform 임계 알림으로 전송하며, 알림 경로의 재귀 실패도 차단합니다.

Auditable Trait 적용​

기본 사용법​

<?php

namespace App\Models;

use App\Core\Base\Audit\Contracts\AuditableInterface;
use App\Core\Base\Audit\Traits\Auditable;
use Illuminate\Database\Eloquent\Model;

class Product extends Model implements AuditableInterface
{
use Auditable;

protected $fillable = [
'name',
'price',
'description',
];
}

적용 즉시 동작​

// 생성 → created 이벤트 기록
$product = Product::create([
'name' => 'New Product',
'price' => 10000,
]);

// 수정 → updated 이벤트 기록 (변경된 필드만)
$product->update(['price' => 15000]);

// 삭제 → deleted 이벤트 기록
$product->delete();

기록되는 정보​

필드설명예시
auditable_type모델 클래스명App\Models\Product
auditable_id모델 ID123
user_id실행한 사용자 ID1
tenant_id테넌트 ID5
event이벤트 유형created, updated, deleted
old_values변경 전 값 (JSON){"price": 10000}
new_values변경 후 값 (JSON){"price": 15000}
ip_address클라이언트 IP192.168.1.1
user_agent브라우저 정보Mozilla/5.0...
url요청 URL/admin/products/123
metadata추가 메타데이터{"order_number": "ORD-001"}
created_at기록 시간2024-01-15 10:30:00

커스터마이징​

특정 필드만 기록​

class Order extends Model implements AuditableInterface
{
use Auditable;

/**
* 이 필드들만 감사 로그에 기록
*/
protected array $auditInclude = [
'status',
'total_amount',
'payment_status',
];
}

민감 정보 제외​

class User extends Model implements AuditableInterface
{
use Auditable;

/**
* 이 필드들은 감사 로그에서 제외
*/
protected array $auditExclude = [
'password',
'remember_token',
'two_factor_secret',
'api_key',
];
}

특정 이벤트만 기록​

class Document extends Model implements AuditableInterface
{
use Auditable;

/**
* 이 이벤트들만 기록 (삭제는 기록 안 함)
*/
protected array $auditEvents = [
'created',
'updated',
];
}

추가 메타데이터 포함​

class Order extends Model implements AuditableInterface
{
use Auditable;

/**
* 감사 로그에 추가할 메타데이터
*/
public function getAuditMetadata(): array
{
return [
'order_number' => $this->order_number,
'customer_id' => $this->customer_id,
'customer_name' => $this->customer?->name,
];
}
}

조건부 활성화/비활성화​

class Message extends Model implements AuditableInterface
{
use Auditable;

/**
* 감사 로그 활성화 여부
*/
public function isAuditEnabled(): bool
{
// Draft 상태에서는 기록 안 함
if ($this->status === 'draft') {
return false;
}

return config('core.audit.enabled', true);
}
}

일시 비활성화​

특정 인스턴스만 비활성화​

// 이 저장에서만 감사 로그 비활성화
$user->withoutAudit()->save();

// 체인 가능
$user->withoutAudit()->update(['name' => 'New Name']);

전역 비활성화 (마이그레이션, 시더)​

// 콜백 내에서 모든 Auditable 모델의 로깅 비활성화
User::withoutAuditing(function () {
// 대량 데이터 삽입
User::factory()->count(1000)->create();
});

// 마이그레이션에서 사용
public function run(): void
{
Product::withoutAuditing(function () {
Product::insert([
['name' => 'Product 1', 'price' => 1000],
['name' => 'Product 2', 'price' => 2000],
// ...
]);
});
}

감사 로그 조회​

AuditLog 모델 스코프​

use App\Core\Base\Audit\Models\AuditLog;

// 특정 모델의 로그
$logs = AuditLog::forModel(Product::class)->get();

// 특정 이벤트의 로그
$logs = AuditLog::event('deleted')->get();

// 특정 사용자의 로그
$logs = AuditLog::byUser($userId)->get();

// 특정 테넌트의 로그
$logs = AuditLog::byTenant($tenantId)->get();

// 날짜 범위 조회
$logs = AuditLog::between('2024-01-01', '2024-01-31')->get();

// 조합 사용
$logs = AuditLog::forModel(User::class)
->event('updated')
->byTenant($tenantId)
->between($startDate, $endDate)
->latest()
->paginate(20);

모델에서 로그 조회​

$product = Product::find(1);

// 해당 모델의 모든 감사 로그 (최신순)
$auditLogs = $product->auditLogs;

// 페이지네이션
$auditLogs = $product->auditLogs()->paginate(10);

// 특정 이벤트만
$deletedLogs = $product->auditLogs()
->where('event', 'deleted')
->get();

변경 내역 확인​

$auditLog = AuditLog::find(1);

// 모든 변경 사항 (old와 new가 다른 필드만)
$changes = $auditLog->changes;
// [
// 'price' => ['old' => 10000, 'new' => 15000],
// 'name' => ['old' => 'Old Name', 'new' => 'New Name'],
// ]

// 개별 값
$oldValues = $auditLog->old_values;
$newValues = $auditLog->new_values;

관계 조회​

$auditLog = AuditLog::find(1);

// 변경을 수행한 사용자
$user = $auditLog->user;

// 테넌트
$tenant = $auditLog->tenant;

// 감사 대상 모델
$product = $auditLog->auditable;

설정 (config/core.php)​

'audit' => [
// 감사 로그 활성화
'enabled' => env('CORE_AUDIT_ENABLED', true),

// minimal | standard | strict (미설정/오타는 standard)
'policy' => env('CORE_AUDIT_POLICY'),

// 관리 Resource 모델 자동 편입
'auto_enroll' => env('CORE_AUDIT_AUTO_ENROLL', true),

// 동일 사용자·레코드 단건 열람 중복 억제(분)
'view_dedup_minutes' => (int) env('CORE_AUDIT_VIEW_DEDUP_MINUTES', 10),

// 동일 감사 실패 알림 스로틀(분)
'failure_alert_throttle_minutes' => (int) env('CORE_AUDIT_FAILURE_ALERT_THROTTLE_MINUTES', 10),

// 저장 테이블명
'table' => 'audit_logs',

// 기록할 이벤트 (기본값)
'events' => ['created', 'updated', 'deleted'],

// 전역 제외 속성 (모든 모델에 적용)
'exclude_attributes' => [
'password',
'remember_token',
'two_factor_secret',
],
],

마이그레이션​

// database/migrations/xxxx_create_audit_logs_table.php
Schema::create('audit_logs', function (Blueprint $table) {
$table->id();
$table->morphs('auditable'); // auditable_type, auditable_id
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('tenant_id')->nullable()->constrained()->nullOnDelete();
$table->string('event'); // created, updated, deleted, restored, force_deleted
$table->json('old_values')->nullable();
$table->json('new_values')->nullable();
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->text('url')->nullable();
$table->json('metadata')->nullable();
$table->timestamp('created_at');

// 인덱스
$table->index(['auditable_type', 'auditable_id']);
$table->index('user_id');
$table->index('tenant_id');
$table->index('event');
$table->index('created_at');
});

지원 이벤트​

이벤트설명old_valuesnew_values
created모델 생성-✓
updated모델 수정✓✓
deleted소프트/하드 삭제✓-
restored소프트 삭제 복원-✓
force_deleted영구 삭제✓-

Filament 통합​

Filament Admin에서 감사 로그 표시:

// app/Filament/Resources/ProductResource.php
use App\Core\Base\Audit\Models\AuditLog;

class ProductResource extends Resource
{
public static function getRelations(): array
{
return [
AuditLogsRelationManager::class,
];
}
}

// app/Filament/Resources/ProductResource/RelationManagers/AuditLogsRelationManager.php
class AuditLogsRelationManager extends RelationManager
{
protected static string $relationship = 'auditLogs';

public function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('event')
->badge()
->color(fn (string $state): string => match ($state) {
'created' => 'success',
'updated' => 'warning',
'deleted' => 'danger',
default => 'gray',
}),
Tables\Columns\TextColumn::make('user.name')
->label('사용자'),
Tables\Columns\TextColumn::make('created_at')
->label('시간')
->dateTime('Y-m-d H:i:s'),
])
->defaultSort('created_at', 'desc');
}
}

적용 기준​

관리 Resource 모델은 자동 편입됩니다. 아래 표는 관리 표면 밖의 모델에 Auditable을 직접 적용할 때의 기준입니다.

적용 필수적용 권장적용 불필요
User, 권한 관련비즈니스 엔티티캐시 테이블
Tenant, OrganizationOrder, Product로그 테이블
민감 데이터일반 CRUDSession, Job
금융 데이터문서, 게시물Pivot 테이블

테스트 작성​

감사 로그 생성 테스트​

use App\Core\Base\Audit\Models\AuditLog;
use App\Models\Product;
use App\Models\User;

test('모델 생성 시 감사 로그가 기록된다', function () {
$user = User::factory()->create();
$this->actingAs($user);

$product = Product::create([
'name' => 'Test Product',
'price' => 10000,
]);

$auditLog = AuditLog::where('auditable_type', Product::class)
->where('auditable_id', $product->id)
->where('event', 'created')
->first();

expect($auditLog)->not->toBeNull()
->and($auditLog->user_id)->toBe($user->id)
->and($auditLog->new_values['name'])->toBe('Test Product');
});

test('모델 수정 시 변경된 필드만 기록된다', function () {
$user = User::factory()->create();
$this->actingAs($user);

$product = Product::create([
'name' => 'Test Product',
'price' => 10000,
]);

$product->update(['price' => 15000]);

$auditLog = AuditLog::where('auditable_type', Product::class)
->where('auditable_id', $product->id)
->where('event', 'updated')
->first();

expect($auditLog)->not->toBeNull()
->and($auditLog->old_values['price'])->toBe(10000)
->and($auditLog->new_values['price'])->toBe(15000);
});

민감 정보 제외 테스트​

test('민감 정보는 감사 로그에서 제외된다', function () {
$user = User::factory()->create([
'password' => bcrypt('password123'),
]);

$auditLog = AuditLog::where('auditable_type', User::class)
->where('auditable_id', $user->id)
->where('event', 'created')
->first();

expect($auditLog->new_values)->not->toHaveKey('password')
->and($auditLog->new_values)->not->toHaveKey('remember_token');
});

일시 비활성화 테스트​

test('withoutAudit()로 감사 로그를 비활성화할 수 있다', function () {
$product = Product::create(['name' => 'Test', 'price' => 1000]);

$beforeCount = AuditLog::count();

$product->withoutAudit()->update(['price' => 2000]);

expect(AuditLog::count())->toBe($beforeCount);
});

test('withoutAuditing()으로 전역 비활성화할 수 있다', function () {
$beforeCount = AuditLog::count();

Product::withoutAuditing(function () {
Product::factory()->count(5)->create();
});

expect(AuditLog::count())->toBe($beforeCount);
});

보존 정책​

대량의 감사 로그 관리를 위한 정리 전략:

Artisan 명령어​

// app/Console/Commands/PruneAuditLogs.php
class PruneAuditLogs extends Command
{
protected $signature = 'audit:prune {--days=90}';

public function handle()
{
$days = $this->option('days');
$cutoff = now()->subDays($days);

$deleted = AuditLog::where('created_at', '<', $cutoff)->delete();

$this->info("{$deleted}개의 오래된 감사 로그가 삭제되었습니다.");
}
}

스케줄 등록​

// app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
// 매일 새벽 2시에 90일 이상 된 로그 삭제
$schedule->command('audit:prune --days=90')
->dailyAt('02:00');
}

모범 사례​

1. 관리 표면 밖의 중요 비즈니스 모델은 명시 적용​

// 관리 Resource에 등록되지 않은 중요 모델은 Auditable을 직접 적용
class Customer extends Model implements AuditableInterface
{
use Auditable;
}

class Order extends Model implements AuditableInterface
{
use Auditable;
}

class Payment extends Model implements AuditableInterface
{
use Auditable;
}

2. 민감 정보 철저히 제외​

class User extends Model implements AuditableInterface
{
use Auditable;

protected array $auditExclude = [
'password',
'remember_token',
'two_factor_secret',
'api_key',
'secret_question_answer',
];
}

3. 메타데이터로 컨텍스트 보강​

class Order extends Model implements AuditableInterface
{
use Auditable;

public function getAuditMetadata(): array
{
return [
'order_number' => $this->order_number,
'customer_email' => $this->customer?->email,
'total_items' => $this->items()->count(),
];
}
}

4. 대량 작업 시 비활성화​

// 시더, 마이그레이션, 배치 작업에서
Product::withoutAuditing(function () {
Product::insert($bulkData);
});

감사로그 계층 가시성 (Hierarchical Visibility)​

v1.39 (Phase B)

관리페이지의 CRUD 와 개별 기능 사용내역을 운영자 계층에 맞는 범위로만 열람하도록 가시성을 분리했습니다. 독립사이트 관리자(L1 SaaS Admin)가 자기 SaaS 범위를 기본 열람하고, 보안/시스템 내부 이벤트는 Platform(L0) 전용으로 격리됩니다.

계층 컨텍스트 컬럼​

기존 audit_logs 에 계층 가시성을 위한 두 컬럼이 추가되었습니다.

컬럼설명가시성 용도
saas_product_id기록 시점의 SaaS Product(독립사이트)L1 SaaS Admin 스코프
organization_id기록 시점의 조직L3+ 조직 서브트리 스코프
  • 마이그레이션: 2026_06_01_000002_add_saas_org_context_to_audit_logs.php (파티션 부모 ALTER → 자식 자동 상속, 멱등 hasColumn 가드, 인덱스 추가, FK 생략, 보존형 down)
  • 컨텍스트는 App\Core\Base\Audit\Support\AuditContext 가 현재 요청에서 해석합니다.
    • saasProductId(): current.saas_product 바인딩 → 세션(current_saas_product_id) → 인증 사용자 saas_product_id
    • organizationId(): 인증 사용자 organization_id
    • tenantId(): 세션(tenant_id) → 사용자 getTenantId()
  • 기록 지점(AuditObserver, AuditLog::logAction() / logSystem() / logView(), ActionLogger)이 이 컨텍스트를 자동으로 함께 저장합니다.

graceful 가드 — 미마이그레이션 프로젝트 보호​

백포트 안전 (v1.39.1 hotfix)

Core 는 symlink 로 여러 프로젝트가 공유합니다. 위 두 컬럼은 Phase B 마이그레이션으로 추가되는데, 아직 migrate 하지 않은 프로젝트의 운영 DB 에는 컬럼이 없습니다. 컬럼 부재 시 saas_product_id 를 INSERT 하면 column does not exist 500 이 발생하므로, 모든 기록 지점은 컬럼이 존재할 때만 계층 컨텍스트를 기록합니다.

App\Core\Base\Audit\Models\AuditLog::supportsHierarchicalContext() 가 Schema::hasColumn() 결과를 memoize 하여 이를 판정합니다. (true 로 확인되면 캐시, false/null 은 매 호출 재확인 — 테스트의 RefreshDatabase 나 마이그레이션 직후의 "없음→있음" 전이를 놓치지 않기 위함)

if (AuditLog::supportsHierarchicalContext()) {
$attributes['saas_product_id'] = AuditContext::saasProductId();
$attributes['organization_id'] = AuditContext::organizationId();
}
  • 미마이그레이션 프로젝트: 계층 컨텍스트는 생략되고 나머지 로깅은 정상 동작(500 없음)
  • 마이그레이션 후: 자동으로 계층 가시성 활성
  • 기존 로그(saas/org 가 NULL 인 과거 기록)는 backfill 하지 않으며, 신규 로그부터 계층 스코프가 적용됩니다.

조회 표면 노출 게이트 (사이트 유형)​

데이터 스코프(아래 buildQuery)가 적용되기 이전에, 감사 로그 화면(Resource) 자체의 노출 여부를 사이트 유형으로 게이트합니다 (BaseAuditLogResource::passesSiteTypeGate).

  • 노출 = 독립사이트(미러 표면 포함) OR Platform(L0). 비독립 Tenant/Org 패널에서는 감사 로그 화면이 아예 노출되지 않습니다(네비게이션 메뉴 + 직접 URL 모두 차단 — shouldRegisterNavigation/canAccess/canViewAny).
  • 게이트는 패널 정체성(소속 패널 Platform/SaaS/Tenant/Org — 런타임 현재 패널, 정적 컨텍스트는 namespace 추론)으로 판정하며, Resource 의 권한 레벨(getMinimumLevel)에 의존하지 않습니다. 따라서 프로젝트가 감사 Resource 를 (쿼리 범위 제한 등 목적으로) 커스터마이즈해도 노출 게이트는 일관되게 동작합니다.
  • 미러 도메인 컨텍스트는 독립사이트로 resolve되어 자연히 통과합니다(미러는 독립사이트에 귀속).

설계: ADR-090(사이트 모델 — 2유형 + route mount) · ADR-087(관리자/감사 가시성). 노출 게이트를 통과한 패널에서만 아래 buildQuery 데이터 스코프가 적용됩니다.

패널별 AuditLogResource 스코프​

각 패널의 감사 로그 화면은 App\Core\Base\Audit\Filament\Resources\BaseAuditLogResource 의 buildQuery() 로 운영자 레벨에 따라 자동 스코프됩니다.

레벨패널열람 범위스코프 기준
L0Platform전체스코프 없음
L1SaaS자기 SaaSsaas_product_id = {내 SaaS}
L2Tenant자기 테넌트tenant_id = {내 테넌트}
L3+Org자기 조직 서브트리(depth-무관)organization_id ∈ ViewerScopeResolver::viewerOrgIds()
  • L3+ 에서 org 가 해석되지 않으면(null) 보수적으로 차단(1 = 0)하여 과다 노출을 방지합니다.
  • 감사 로그 화면은 읽기 전용입니다(canCreate() = false, 생성/수정/삭제 액션 없음).

이벤트 민감도 분리선 (visible_events)​

패널별로 노출 가능한 이벤트 allowlist 를 config/core.php 의 audit.visible_events 로 분리합니다. 계층 스코프(어떤 레코드)와 별개로, 민감 이벤트(어떤 종류)를 패널 단위로 통제합니다.

// config/core.php — audit.visible_events
'visible_events' => [
'platform' => ['created', 'updated', 'deleted', 'restored', 'force_deleted', 'viewed', 'login', 'logout', 'login_failed', 'password_reset', 'action'],
'saas' => ['created', 'updated', 'deleted', 'restored', 'login', 'logout', 'action'],
'tenant' => ['created', 'updated', 'deleted', 'action'],
'org' => ['created', 'updated', 'deleted', 'action'],
],
패널노출 이벤트비고
platform (L0)CUD + restored/force_deleted + viewed + login/logout/login_failed/password_reset + action전체 — 보안/시스템 내부 이벤트 포함
saas (L1)CUD + restored + login/logout + action보안 민감 이벤트 제외
tenant (L2)CUD + action로그인/보안 이벤트 제외
org (L3)CUD + action로그인/보안 이벤트 제외 — 조직 운영 범위

Platform 전용(보안/시스템 내부) 이벤트: login_failed, password_reset, viewed, force_deleted — 다른 패널 allowlist 에는 포함되지 않습니다.

'action' 이 포함된 패널은 CUD/인증 표준 이벤트가 아닌 커스텀 액션 이벤트(점 표기, 예: inquiry.escalate)도 일괄 노출합니다. BaseAuditLogResource 의 표준 이벤트 집합(created, updated, deleted, restored, force_deleted, login, logout, login_failed, password_reset, viewed, action) 이외의 모든 이벤트가 "action" 범주로 취급됩니다.

운영자가 보는 범위 요약​

운영자어떤 레코드어떤 이벤트
Platform Admin모든 SaaS/테넌트/조직모든 이벤트(보안/시스템 포함)
SaaS Admin자기 SaaS ProductCUD + 로그인/로그아웃 + 커스텀 액션
Tenant Admin자기 테넌트CUD + 커스텀 액션
Org Admin (L3+)자기 조직 + 하위 조직CUD + 커스텀 액션

재사용 로깅 모듈 (ActionLogger / audited)​

관리 액션과 서비스 액션의 경계

Filament 관리 커스텀 액션은 standard·strict 정책에서 자동 기록됩니다. 아래 API는 서비스·Job·다운로드처럼 Filament 실행 이벤트 밖에 있거나, 도메인 이벤트명·메타데이터를 명시해야 하는 경우에 사용합니다. 관리 액션 안에서 직접 기록하면 자동 기록기는 그 실행을 다시 기록하지 않습니다.

세 가지 적용 방법​

방법적용 대상사용
ActionLogger::log()어디서나app(ActionLogger::class)->log('export.users', $subject, $meta)
LogsActions trait서비스 / Job / 핸들러use LogsActions; → $this->logAction($event, $subject, $meta)
->audited('event')Filament Action자동 생성 이벤트명 대신 도메인 이벤트명·메타데이터 지정

세 방법 모두 내부적으로 App\Core\Base\Audit\Services\ActionLogger 로 위임되며, 계층 컨텍스트(saas_product_id / organization_id / tenant_id)를 자동으로 함께 기록합니다. 로깅 실패가 기능 흐름을 막지 않도록 try/catch 로 감싸져 있습니다(실패 시 null 반환).

ActionLogger 서비스 / LogsActions trait​

<?php

namespace App\Services;

use App\Core\Base\Audit\Traits\LogsActions;
use App\Models\Inquiry;
use App\Models\User;

class InquiryService
{
use LogsActions;

public function assign(Inquiry $inquiry, User $staff): void
{
// ... 배정 로직 ...

// 점 표기 액션 이름 권장 → visible_events 의 'action' 으로 노출
$this->logAction('inquiry.assign', $inquiry, ['staff_id' => $staff->id]);
}
}

ActionLogger::log() 의 시그니처:

인자타입설명
$eventstring점 표기 액션 이름 (예: inquiry.assign, users.export)
$subject?Model액션 대상 모델(선택) — auditable_type / auditable_id 로 기록
$metadataarray구조화 메타데이터(선택)

Filament Action ->audited('event') 매크로​

App\Core\Base\Audit 의 매크로는 CoreServiceProvider::boot() 에서 Filament\Actions\Action::macro('audited', ...) 로 등록됩니다. 자동 기록의 기본 이벤트명 대신 도메인 이벤트명과 메타데이터가 필요하면 Action 체인에 추가합니다. Action 실행 후(after() 훅) ActionLogger로 기록되며, 관리 표면 자동 기록은 같은 실행을 중복 기록하지 않습니다.

use Filament\Actions\Action;

Action::make('escalate')
->label('에스컬레이션')
->action(fn ($record) => $this->escalate($record))
->audited('inquiry.escalate');

메타데이터를 함께 기록하려면 두 번째 인자에 클로저를 전달합니다.

Action::make('escalate')
->audited('inquiry.escalate', fn ($record) => ['priority' => $record->priority]);
CRUD 중복 주의

생성/수정/삭제(CRUD)는 AuditObserver 가 이미 자동 기록합니다(created / updated / deleted 이벤트). ->audited() 와 LogsActions 는 CRUD 외 비-CRUD 액션 기록에 사용하세요. CRUD 액션에 중복 적용하면 동일 작업이 두 번(updated + 커스텀 이벤트) 기록될 수 있습니다.

커스텀 액션의 가시성 노출​

점 표기 커스텀 액션 이벤트(예: inquiry.escalate, users.export)는 표준 이벤트가 아니므로 패널 화면에서 visible_events 에 'action' 이 포함된 패널에서만 노출됩니다(앞 절의 민감도 분리선 참조). 즉 Platform / SaaS / Tenant / Org 모든 패널이 커스텀 액션을 열람할 수 있습니다.

실제 적용 예​

기능이벤트적용 방식
문의 에스컬레이션inquiry.escalateFilament Action ->audited('inquiry.escalate')
회원 CSV 내보내기users.exportBaseAllUsersResource::exportUsersCsv 에서 스트림 직전 ActionLogger::log('users.export', ...) 명시 호출
다운로드 액션은 명시 호출

->audited() 는 Action 의 after() 훅에 의존합니다. 파일 스트림 다운로드처럼 after() 훅 실행이 보장되지 않는 경우, 회원 CSV 내보내기처럼 스트림 직전에 ActionLogger::log() 를 직접 호출하는 것이 안전합니다.

관련 문서​