Skip to content

Audit Logging

FraiseQL provides comprehensive audit logging for compliance and security monitoring, with multiple backend options.

Audit logging tracks:

  • GraphQL queries and mutations
  • Authentication events (login, logout, token refresh)
  • Authorization decisions (access granted/denied)
  • Administrative operations
  • Data access patterns

Store audit logs in the database for querying and retention. Enable via [security.enterprise]:

[security.enterprise]
audit_logging_enabled = true
audit_log_level = "info" # "debug" | "info" | "warn"

FraiseQL writes to the ta_audit_log table you create in your database (see schema below). Retention and batching are managed by your PostgreSQL maintenance jobs.

Schema (auto-created):

CREATE TABLE tb_audit_log (
pk_audit_log BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
timestamp TIMESTAMPTZ DEFAULT NOW() NOT NULL,
level TEXT NOT NULL,
event_type TEXT NOT NULL,
user_id UUID,
tenant_id UUID,
operation TEXT,
resource TEXT,
details JSONB,
ip_address INET,
user_agent TEXT,
request_id UUID,
duration_ms INTEGER
);
CREATE INDEX idx_audit_log_timestamp ON tb_audit_log(timestamp DESC);
CREATE INDEX idx_audit_log_user ON tb_audit_log(user_id, timestamp DESC);
CREATE INDEX idx_audit_log_event ON tb_audit_log(event_type, timestamp DESC);
EventDescription
auth.loginUser login attempt
auth.login.successSuccessful login
auth.login.failureFailed login
auth.logoutUser logout
auth.token.refreshToken refreshed
auth.token.expiredToken expired
auth.session.createdSession created
auth.session.destroyedSession ended
EventDescription
authz.access.grantedAccess allowed
authz.access.deniedAccess denied
authz.role.requiredRole check performed
authz.scope.requiredScope check performed
EventDescription
graphql.queryQuery executed
graphql.mutationMutation executed
graphql.subscriptionSubscription started
graphql.errorOperation error
EventDescription
admin.schema.reloadSchema reloaded
admin.cache.clearCache cleared
admin.key.rotateEncryption key rotated

The confirmed TOML configuration for audit logging is:

[security.enterprise]
audit_logging_enabled = true
audit_log_level = "info" # "debug" | "info" | "warn"

Log level filtering, event-type filtering, and field redaction are handled in your PostgreSQL ta_audit_log table and views — for example, exclude sensitive fields from the audit view, or use PostgreSQL column-level permissions to protect PII fields.

Protect sensitive data by not inserting it into the audit table in the first place. The ta_audit_log schema below has a details JSONB column — write your PostgreSQL audit trigger to omit or hash sensitive fields before inserting:

-- Hash email for correlation without storing PII
INSERT INTO ta_audit_log (event_type, user_id, details)
VALUES (
'auth.login.success',
p_user_id,
jsonb_build_object(
'email_hash', encode(sha256(p_email::bytea), 'hex')
-- never store p_email directly
)
);
-- Recent failed logins
SELECT * FROM tb_audit_log
WHERE event_type = 'auth.login.failure'
AND timestamp > NOW() - INTERVAL '1 hour'
ORDER BY timestamp DESC;
-- User activity
SELECT event_type, COUNT(*), MAX(timestamp)
FROM tb_audit_log
WHERE user_id = 'user-uuid-here'
AND timestamp > NOW() - INTERVAL '7 days'
GROUP BY event_type;
-- Access denied events
SELECT * FROM tb_audit_log
WHERE event_type = 'authz.access.denied'
ORDER BY timestamp DESC
LIMIT 100;
-- Mutations by resource
SELECT
resource,
COUNT(*) as mutation_count,
COUNT(DISTINCT user_id) as unique_users
FROM tb_audit_log
WHERE event_type = 'graphql.mutation'
AND timestamp > NOW() - INTERVAL '24 hours'
GROUP BY resource
ORDER BY mutation_count DESC;

Expose audit logs via GraphQL (admin only):

@fraiseql.query(
sql_source="v_audit_log",
requires_role="admin"
)
def audit_logs(
event_type: str | None = None,
user_id: ID | None = None,
since: DateTime | None = None,
limit: int = 100
) -> list[AuditLog]:
pass

Use a PostgreSQL cron job (via pg_cron) or an external scheduler:

-- Delete audit logs older than 365 days (run as a scheduled job)
DELETE FROM ta_audit_log
WHERE timestamp < NOW() - INTERVAL '365 days';
-- Manual cleanup
DELETE FROM ta_audit_log
WHERE timestamp < NOW() - INTERVAL '365 days';

Archive to S3 or cold storage using pg_dump or your data pipeline before deleting:

Terminal window
# Export audit logs older than 365 days to JSONB
psql $DATABASE_URL -c "COPY (
SELECT row_to_json(ta_audit_log)
FROM ta_audit_log
WHERE timestamp < NOW() - INTERVAL '365 days'
) TO '/tmp/audit-archive.json'"
# Upload to S3, then delete from DB
aws s3 cp /tmp/audit-archive.json s3://your-bucket/audit/$(date +%Y%m%d).json
psql $DATABASE_URL -c "DELETE FROM ta_audit_log WHERE timestamp < NOW() - INTERVAL '365 days'"

Enable audit logging via [security.enterprise], then implement compliance-specific controls at the PostgreSQL layer.

Log all PHI access events. FraiseQL writes every query and mutation to the audit table when audit_logging_enabled = true. Add retention enforcement via pg_cron:

-- Retain PHI access logs for 6 years (HIPAA minimum)
SELECT cron.schedule('hipaa-retention', '0 2 * * *', $$
DELETE FROM ta_audit_log
WHERE timestamp < NOW() - INTERVAL '6 years'
AND event_type LIKE 'graphql.%'
$$);

Redact sensitive fields before inserting (see Field Redaction above).

Enable audit_logging_enabled = true. The audit table captures all access control decisions (authz.access.granted, authz.access.denied) and mutations automatically. No additional configuration required.

For right-to-erasure support, implement a PostgreSQL function that anonymises the user_id column in audit rows when a deletion request is processed:

-- Anonymise audit log entries for deleted users
CREATE FUNCTION fn_anonymise_user_audit(p_user_id UUID) RETURNS VOID AS $$
BEGIN
UPDATE ta_audit_log
SET user_id = NULL,
details = details - 'email' - 'ip_address'
WHERE user_id = p_user_id;
END;
$$ LANGUAGE plpgsql;
MetricDescription
fraiseql_audit_events_totalTotal audit events
fraiseql_audit_events_by_typeEvents by type
fraiseql_audit_write_latency_msWrite latency
fraiseql_audit_queue_sizePending events
fraiseql_audit_errors_totalWrite errors

Enable audit_logging_enabled = true. FraiseQL logs all mutations, auth events, and access denials. For high-volume query logging, insert a sampling condition in your PostgreSQL audit trigger or view.

# Good - structured, queryable
audit.log("graphql.mutation", details={
"operation": "createUser",
"input": {"email": "user@example.com"},
"result": {"id": "user-123"}
})
# Bad - unstructured
audit.log("graphql.mutation", details="Created user user@example.com")
# Alert on audit write failures
rate(fraiseql_audit_errors_total[5m]) > 0
# Alert on queue backup
fraiseql_audit_queue_size > 1000
  1. Check enabled = true
  2. Verify log level isn’t filtering
  3. Check filter rules
  4. Review backend connectivity
  1. Increase batch_size
  2. Increase flush_interval_ms
  3. Check database/syslog performance
  4. Consider async writes
  1. Enable retention cleanup
  2. Reduce retention period
  3. Archive to cheaper storage
  4. Increase sampling for high-volume events