技能创建器
skill-creator
创建、改进和评估自定义技能,把专业流程转化为可重复使用的 AI 能力。
backend-audit-logging【数据与研究】Use this skill when the user says 'audit log', 'audit trail', 'compliance logging', 'tamper-evident log', 'immutable log', 'audit table', 'change tracking', 'who changed what', 'data provenance', 'audit events', 'reporting log'. This skill implements immutable audit trails for compliance (SOC 2, SOX, HIPAA, GDPR). Applies to any backend stack. Do NOT…
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Record every sensitive data access and state change in an immutable, tamper-evident audit trail for regulatory compliance (SOC 2, SOX, HIPAA, GDPR).
Exact user phrases: "audit log", "audit trail", "compliance logging", "tamper-evident log", "immutable log", "audit table", "change tracking", "who changed what", "data provenance", "audit events", "reporting log".
Audit log schema or implementation code. No file unless requested.
Event: {event type}
Actor: {user/system}
Resource: {resource type}:{id}
Action: {read|create|update|delete}
What is the primary compliance requirement?
├── SOC 2 / SOX → PostgreSQL with hash chain (append-only table, partition by month)
├── HIPAA → Encrypted database with strict access controls + hash chain + 6-year retention
├── GDPR → Searchable storage with data deletion capability (anonymization, not deletion)
├── PCI-DSS → Immutable log with strict access controls, 1-year retention, quarterly review
└── General compliance → Cloud-native audit service (AWS CloudTrail, Azure Monitor)
What is the write volume?
├── < 1000 events/sec → Relational database (PostgreSQL, MySQL) with hash chain
├── 1000–10000 events/sec → Time-series database (TimescaleDB, ClickHouse)
└── > 10000 events/sec → Log aggregator (Elasticsearch) + batched hash chain
Is tamper evidence required by compliance framework?
├── Yes → Is regulatory framework HIPAA or PCI-DSS?
│ ├── Yes → Hash chain with periodic verification (quarterly) + external audit service
│ └── No → Hash chain (SHA-256 linked list) or digital signature per entry
└── No → Append-only table with database-level access controls (no hash chain needed)
Volume consideration:
├── Single DB (<1000/s) → Hash chain linking each row via previous_hash
├── Medium (1000–10000/s) → Batch Merkle tree, publish root hash periodically
└── High (>10000/s) → Periodic hash anchor (chain per batch, not per event)
What needs to be recorded?
├── Just who did what? → Minimal: actor, action, resource, timestamp
├── Plus what changed? → Standard: + resource_id, changes (diff), metadata, correlation_id
├── Plus proof of integrity? → Complete: + previous_hash, hash, digital_signature
└── Multi-tenant? → Complete + tenant_id
events:
user.access: User accessed PII data
user.update: User profile updated
payment.process: Payment initiated or completed
role.change: User role or permission changed
data.export: Data exported (GDPR SAR)
data.deletion: Data deleted (right to be forgotten)
settings.change: System configuration changed
auth.login: User authentication succeeded
auth.logout: User session ended
auth.failed: Authentication failed
admin.action: Administrative action performed
integration.call: External API call made
data.masked: Sensitive data masked
consent.granted: User consent recorded
consent.revoked: User consent revoked
CREATE TABLE audit_log (
id BIGSERIAL PRIMARY KEY,
event_type VARCHAR(50) NOT NULL,
actor_id VARCHAR(100) NOT NULL,
actor_type VARCHAR(20) NOT NULL, -- 'user' | 'system' | 'api'
resource_type VARCHAR(50) NOT NULL,
resource_id VARCHAR(100),
action VARCHAR(10) NOT NULL, -- 'create' | 'read' | 'update' | 'delete'
changes JSONB, -- diff of old/new values
metadata JSONB, -- IP, user-agent, requestId
correlation_id VARCHAR(64), -- trace across services
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
hash VARCHAR(64), -- SHA-256 of previous row + this row data
previous_hash VARCHAR(64),
tenant_id VARCHAR(50), -- for multi-tenant systems
event_version INT DEFAULT 1
);
-- Prevent UPDATE/DELETE via trigger
CREATE OR REPLACE FUNCTION prevent_audit_mutation()
RETURNS TRIGGER AS $
BEGIN
RAISE EXCEPTION 'Audit log is append-only. UPDATE and DELETE are not permitted.';
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER prevent_audit_update
BEFORE UPDATE ON audit_log
FOR EACH ROW EXECUTE FUNCTION prevent_audit_mutation();
CREATE TRIGGER prevent_audit_delete
BEFORE DELETE ON audit_log
FOR EACH ROW EXECUTE FUNCTION prevent_audit_mutation();
-- Indexes for common queries
CREATE INDEX idx_audit_event_type ON audit_log(event_type);
CREATE INDEX idx_audit_actor ON audit_log(actor_id, actor_type);
CREATE INDEX idx_audit_resource ON audit_log(resource_type, resource_id);
CREATE INDEX idx_audit_occurred_at ON audit_log(occurred_at);
CREATE INDEX idx_audit_tenant ON audit_log(tenant_id) WHERE tenant_id IS NOT NULL;
-- Partition by month
CREATE TABLE audit_log_2026_01 PARTITION OF audit_log
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
CREATE TABLE audit_log_2026_02 PARTITION OF audit_log
FOR VALUES FROM ('2026-02-01') TO ('2026-03-01');
import { createHash } from 'crypto';
async function appendAuditLog(event: AuditEvent, db: Database): Promise<void> {
const lastRow = await db.query('SELECT id, hash FROM audit_log ORDER BY id DESC LIMIT 1');
const previousHash = lastRow.rows[0]?.hash || '';
// Build entry with hash chain
const entry = { ...event, previousHash };
const canonicalData = canonicalize(entry);
entry.hash = createHash('sha256')
.update(previousHash + canonicalData)
.digest('hex');
await db.query('INSERT INTO audit_log SET ?', entry);
}
function canonicalize(obj: Record<string, unknown>): string {
return JSON.stringify(obj, Object.keys(obj).sort());
}
// Hash chain verification
async function verifyChainIntegrity(db: Database): Promise<{ chainIntact: boolean; brokenLinks: number }> {
const result = await db.raw(`
SELECT COUNT(*) as broken FROM (
SELECT id, hash, previous_hash,
LAG(hash) OVER (ORDER BY id) as expected_previous
FROM audit_log
) sub
WHERE sub.previous_hash != COALESCE(sub.expected_previous, '')
`);
return { chainIntact: Number(result.rows[0].broken) === 0, brokenLinks: Number(result.rows[0].broken) };
}
// ORM hook or middleware
auditService.append({
eventType: 'user.update',
actorId: req.user.id,
actorType: 'user',
resourceType: 'user_profile',
resourceId: profileId,
action: 'update',
changes: diff(oldProfile, newProfile), // Only non-sensitive fields
metadata: {
ip: req.ip,
userAgent: req.headers['user-agent'],
requestId: req.id,
},
correlationId: req.correlationId,
});
Python example:
from dataclasses import dataclass, asdict
from datetime import datetime, timezone
import hashlib, json
@dataclass
class AuditEvent:
event_type: str
actor_id: str
actor_type: str
resource_type: str
resource_id: str
action: str
changes: dict | None = None
metadata: dict | None = None
correlation_id: str = ""
tenant_id: str = ""
occurred_at: str = ""
class AuditService:
def __init__(self, db):
self.db = db
async def append(self, event: AuditEvent):
event.occurred_at = datetime.now(timezone.utc).isoformat()
last = await self.db.fetch("SELECT hash FROM audit_log ORDER BY id DESC LIMIT 1")
previous_hash = last[0]["hash"] if last else ""
data = {**asdict(event), "previous_hash": previous_hash}
canonical = json.dumps(data, sort_keys=True)
data["hash"] = hashlib.sha256((previous_hash + canonical).encode()).hexdigest()
await self.db.execute("INSERT INTO audit_log VALUES (...)", data)
class AuditQueryService {
async search({ actorId, resourceType, eventType, from, to, page, limit }: AuditSearchParams): Promise<AuditSearchResult> {
let query = knex('audit_log')
.where('occurred_at', '>=', from)
.where('occurred_at', '<=', to);
if (actorId) query.where('actor_id', actorId);
if (resourceType) query.where('resource_type', resourceType);
if (eventType) query.where('event_type', eventType);
const total = await query.clone().count('id as count').first();
const rows = await query
.orderBy('occurred_at', 'desc')
.offset((page - 1) * limit)
.limit(limit);
return { rows, total: Number(total.count), page, limit };
}
async verifyIntegrity(): Promise<{ chainIntact: boolean }> {
const result = await db.raw(`
SELECT COUNT(*) as broken FROM (
SELECT id, hash, previous_hash,
LAG(hash) OVER (ORDER BY id) as expected_previous
FROM audit_log
) sub
WHERE sub.previous_hash != COALESCE(sub.expected_previous, '')
`);
return { chainIntact: Number(result.rows[0].broken) === 0 };
}
}
Tier 1: Hot storage (7-30 days) — queryable via API, online (SSD)
Tier 2: Warm storage (1 year) — queryable with delay (S3/Parquet export, query via Athena)
Tier 3: Cold storage (7 years) — archived, not directly queryable (Glacier/Deep Archive)
class AuditArchivalService {
async archiveToS3(endDate: Date): Promise<void> {
const thirtyDaysAgo = new Date();
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
const rows = await knex('audit_log')
.where('occurred_at', '<', endDate)
.where('occurred_at', '>=', thirtyDaysAgo);
// Write to Parquet in S3
await s3.putObject({
Bucket: 'audit-archive',
Key: `year=${endDate.getUTCFullYear()}/month=${endDate.getUTCMonth() + 1}/audit.parquet`,
Body: await toParquet(rows),
});
// Delete from hot storage
await knex('audit_log')
.where('occurred_at', '<', endDate)
.delete();
}
}
| Framework | Retention | Review Frequency | Special Requirements |
|---|---|---|---|
| SOC 2 | 12 months minimum | Quarterly | Access control, tamper evidence |
| SOX | 7 years | Annual | Immutable, auditor access |
| HIPAA | 6 years | Annual | Encryption, access logs, BAA |
| GDPR | Until consent revoked | On request | Erasure capability, consent tracking |
| PCI-DSS | 12 months | Quarterly | Cardholder data protection, logging |
CREATE TABLE audit_log (
event_type String,
actor_id String,
actor_type String,
resource_type String,
resource_id String,
action String,
changes String,
metadata String,
correlation_id String,
tenant_id String,
occurred_at DateTime('UTC'),
event_version UInt8,
hash String,
previous_hash String
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(occurred_at)
ORDER BY (occurred_at, event_type, actor_id)
TTL occurred_at + INTERVAL 7 YEAR DELETE;
Recording actual SSN, credit card numbers, or passwords in the changes diff. Fix: Use tokenization or masking — show that a field changed, not what it changed to.
Each service logs audit events independently without a common trace ID. Fix: Propagate correlation ID through all services, include in every audit event.
Using synchronous DB inserts for audit logging impacts main request latency. Fix: Use async writes, message queues, or background batch processing.
Running hash chain on a single database without monitoring. Fix: Implement periodic verification, remote backups, alert on chain breaks.
Distributed systems log events with different server clocks. Fix: NTP-synchronized clocks, log in UTC, timestamp at application layer not database.
Auditing only user actions but missing cron jobs, background workers, admin scripts. Fix: Every state change must be audited regardless of initiator.
Single audit_log table for all event types creates query performance issues. Fix: Partition by time or use separate tables for different event categories.
Allowing UPDATE or DELETE on audit records defeats the purpose. Fix: Database triggers to prevent mutations, restricted permissions.
| Aspect | Hash Chain | WORM Storage | Digital Signatures |
|---|---|---|---|
| Tamper detection | Verifiable by anyone | Physical immutability | Verifiable with public key |
| Implementation | Application-level | Storage-level | Application-level |
| Cost | Low (in-DB) | Medium (specialized HW) | Low (compute only) |
| Performance | ~0.1ms per entry | Writes are cheap | ~1ms per entry |
| Key management | None | None | Private key protection |
| Compliance acceptance | Widely accepted | Gold standard | Accepted |
| Aspect | Database Audit (pgAudit) | Application Audit |
|---|---|---|
| Granularity | Row-level, all changes | Business context |
| Context | SQL statements only | Actor, reason, business action |
| Coupling | Zero (DB plugin) | Application code required |
| Tamper evidence | DB-level | Application-level hash chain |
| Queryability | SQL only | Rich API with business filters |
| Performance overhead | 5-15% on DB | Minimal (async) |
| Aspect | PostgreSQL | Elasticsearch | ClickHouse |
|---|---|---|---|
| Write throughput | <5000/s | 5000-50000/s | >50000/s |
| Query latency | <10ms | <100ms | <50ms |
| Compression | 2:1 | 3:1 | 5-10:1 |
| Hash chain support | Native | Requires external | Requires external |
| Operational complexity | Low | High | Medium |
references/audit-log-architecture.md — Architectural patterns for audit logging systemsreferences/audit-log-compliance.md — Compliance requirements mapping for audit logsreferences/audit-log-implementation.md — Implementation guides and code examplesreferences/audit-log-monitoring.md — Monitoring and alerting for audit log systemsreferences/audit-trail.md — Audit trail design principlesreferences/compliance-logging.md — Compliance logging best practicesreferences/audit-log-fundamentals.md — Audit Logging Fundamentalsreferences/audit-log-advanced.md — Audit Logging Advanced Patternsreferences/audit-log-storage-comparison.md — Audit Storage Backend ComparisonNo artifact produced unless requested. Next skill: plugin-architecture — extend the audit system with custom event handlers. Carry forward: audit event types, retention policy, tamper evidence strategy.
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).