Skip to content
Merged
89 changes: 89 additions & 0 deletions aperag/api/components/schemas/audit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
auditLog:
type: object
description: Audit log entry
properties:
id:
type: string
description: Audit log ID
user_id:
type: string
nullable: true
description: User ID who performed the action
username:
type: string
nullable: true
description: Username for display
resource_type:
type: string
enum: [collection, document, bot, chat, message, api_key, llm_provider, llm_provider_model, model_service_provider, user, flow, search_test]
nullable: true
description: Type of resource
resource_id:
type: string
nullable: true
description: ID of the resource (extracted at query time)
api_name:
type: string
description: API operation name
http_method:
type: string
description: HTTP method (POST, PUT, DELETE)
path:
type: string
description: API path
status_code:
type: integer
nullable: true
description: HTTP status code
start_time:
type: integer
format: int64
description: Request start time (milliseconds since epoch)
end_time:
type: integer
format: int64
nullable: true
description: Request end time (milliseconds since epoch)
duration_ms:
type: integer
nullable: true
description: Request duration in milliseconds (calculated)
request_data:
type: string
nullable: true
description: Request data (JSON string)
response_data:
type: string
nullable: true
description: Response data (JSON string)
error_message:
type: string
nullable: true
description: Error message if failed
ip_address:
type: string
nullable: true
description: Client IP address
user_agent:
type: string
nullable: true
description: User agent string
request_id:
type: string
description: Request ID for tracking
created:
type: string
format: date-time
description: Created timestamp

auditLogList:
type: object
description: List of audit logs
properties:
items:
type: array
description: Audit log entries
items:
$ref: '#/auditLog'


6 changes: 6 additions & 0 deletions aperag/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ paths:
/prompt-templates:
$ref: './paths/prompt_templates.yaml#/promptTemplates'

# audit
/audit-logs:
$ref: './paths/audit.yaml#/audit_logs'
/audit-logs/{audit_id}:
$ref: './paths/audit.yaml#/audit_log_detail'

# users
/invite:
$ref: './paths/auth.yaml#/invite'
Expand Down
73 changes: 73 additions & 0 deletions aperag/api/paths/audit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
audit_logs:
get:
tags:
- audit
summary: List audit logs
description: List audit logs with filtering options
operationId: list_audit_logs
parameters:
- name: api_name
in: query
required: false
schema:
type: string
description: Filter by API name
- name: start_date
in: query
required: false
schema:
type: string
format: date-time
description: Filter by start date
- name: end_date
in: query
required: false
schema:
type: string
format: date-time
description: Filter by end date
- name: limit
in: query
required: false
schema:
type: integer
maximum: 5000
default: 1000
description: Maximum number of records
responses:
"200":
description: Audit logs retrieved successfully
content:
application/json:
schema:
$ref: "../components/schemas/audit.yaml#/auditLogList"
"403":
description: Admin access required

audit_log_detail:
get:
tags:
- audit
summary: Get audit log detail
description: Get a specific audit log by ID
operationId: get_audit_log
parameters:
- name: audit_id
in: path
required: true
schema:
type: string
description: Audit log ID
responses:
"200":
description: Audit log retrieved successfully
content:
application/json:
schema:
$ref: "../components/schemas/audit.yaml#/auditLog"
"403":
description: Admin access required
"404":
description: Audit log not found


2 changes: 2 additions & 0 deletions aperag/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from aperag.exception_handlers import register_exception_handlers
from aperag.llm.litellm_track import register_opik_llm_track
from aperag.views.api_key import router as api_key_router
from aperag.views.audit import router as audit_router
from aperag.views.auth import router as auth_router
from aperag.views.chat_completion import router as chat_completion_router
from aperag.views.config import router as config_router
Expand All @@ -38,6 +39,7 @@
app.include_router(auth_router, prefix="/api/v1")
app.include_router(main_router, prefix="/api/v1")
app.include_router(api_key_router, prefix="/api/v1")
app.include_router(audit_router, prefix="/api/v1") # Add audit router
app.include_router(flow_router, prefix="/api/v1")
app.include_router(llm_router, prefix="/api/v1")
app.include_router(chat_completion_router, prefix="/v1")
Expand Down
65 changes: 65 additions & 0 deletions aperag/db/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
Boolean,
Column,
DateTime,
Index,
Integer,
String,
Text,
Expand Down Expand Up @@ -746,3 +747,67 @@ def update_spec(self, desired_state: IndexDesiredState = None, created_by: str =
self.created_by = created_by
self.version += 1
self.gmt_updated = utc_now()


class AuditResource(str, Enum):
"""Audit resource types"""

COLLECTION = "collection"
DOCUMENT = "document"
BOT = "bot"
CHAT = "chat"
MESSAGE = "message"
API_KEY = "api_key"
LLM_PROVIDER = "llm_provider"
LLM_PROVIDER_MODEL = "llm_provider_model"
MODEL_SERVICE_PROVIDER = "model_service_provider"
USER = "user"
CONFIG = "config"
INVITATION = "invitation"
AUTH = "auth"
CHAT_COMPLETION = "chat_completion"
SEARCH_TEST = "search_test"
LLM = "llm"
FLOW = "flow"
SYSTEM = "system"


class AuditLog(Base):
"""Audit log model to track all system operations"""

__tablename__ = "audit_log"

id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
user_id = Column(String(36), nullable=True, comment="User ID")
username = Column(String(255), nullable=True, comment="Username")
resource_type = Column(EnumColumn(AuditResource), nullable=True, comment="Resource type")
resource_id = Column(String(255), nullable=True, comment="Resource ID (extracted at query time)")
api_name = Column(String(255), nullable=False, comment="API operation name")
http_method = Column(String(10), nullable=False, comment="HTTP method (POST, PUT, DELETE)")
path = Column(String(512), nullable=False, comment="API path")
status_code = Column(Integer, nullable=True, comment="HTTP status code")
request_data = Column(Text, nullable=True, comment="Request data (JSON)")
response_data = Column(Text, nullable=True, comment="Response data (JSON)")
error_message = Column(Text, nullable=True, comment="Error message if failed")
ip_address = Column(String(45), nullable=True, comment="Client IP address")
user_agent = Column(String(500), nullable=True, comment="User agent string")
request_id = Column(String(255), nullable=False, comment="Request ID for tracking")
start_time = Column(BigInteger, nullable=False, comment="Request start time (milliseconds since epoch)")
end_time = Column(BigInteger, nullable=True, comment="Request end time (milliseconds since epoch)")
gmt_created = Column(DateTime(timezone=True), nullable=False, default=utc_now, comment="Created time")

# Index for better query performance
__table_args__ = (
Index("idx_audit_user_id", "user_id"),
Index("idx_audit_resource_type", "resource_type"),
Index("idx_audit_api_name", "api_name"),
Index("idx_audit_http_method", "http_method"),
Index("idx_audit_status_code", "status_code"),
Index("idx_audit_gmt_created", "gmt_created"),
Index("idx_audit_resource_id", "resource_id"),
Index("idx_audit_request_id", "request_id"),
Index("idx_audit_start_time", "start_time"),
)

def __repr__(self):
return f"<AuditLog(id={self.id}, user={self.username}, api={self.api_name}, method={self.http_method}, status={self.status_code})>"
2 changes: 0 additions & 2 deletions aperag/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import traceback
from enum import Enum
from http import HTTPStatus
from typing import Any, Dict, Optional
Expand Down Expand Up @@ -90,7 +89,6 @@ def __init__(self, error_code: ErrorCode, message: Optional[str] = None, details
self.error_code = error_code
self.message = message or error_code.error_name.replace("_", " ").title()
self.details = details or {}
traceback.print_stack()
super().__init__(self.message)

@property
Expand Down
70 changes: 70 additions & 0 deletions aperag/migration/versions/20250621002836-2768dfee8bbc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""empty message

Revision ID: 2768dfee8bbc
Revises: 12ea6d2bf365
Create Date: 2025-06-21 00:28:36.443046

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = '2768dfee8bbc'
down_revision: Union[str, None] = '12ea6d2bf365'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('audit_log',
sa.Column('id', sa.String(length=36), nullable=False),
sa.Column('user_id', sa.String(length=36), nullable=True, comment='User ID'),
sa.Column('username', sa.String(length=255), nullable=True, comment='Username'),
sa.Column('resource_type', sa.Enum('collection', 'document', 'bot', 'chat', 'message', 'api_key', 'llm_provider', 'llm_provider_model', 'model_service_provider', 'user', 'config', 'invitation', 'auth', 'chat_completion', 'search_test', 'llm', 'flow', 'system', name='auditresource'), nullable=True, comment='Resource type'),
sa.Column('resource_id', sa.String(length=255), nullable=True, comment='Resource ID (extracted at query time)'),
sa.Column('api_name', sa.String(length=255), nullable=False, comment='API operation name'),
sa.Column('http_method', sa.String(length=10), nullable=False, comment='HTTP method (POST, PUT, DELETE)'),
sa.Column('path', sa.String(length=512), nullable=False, comment='API path'),
sa.Column('status_code', sa.Integer(), nullable=True, comment='HTTP status code'),
sa.Column('request_data', sa.Text(), nullable=True, comment='Request data (JSON)'),
sa.Column('response_data', sa.Text(), nullable=True, comment='Response data (JSON)'),
sa.Column('error_message', sa.Text(), nullable=True, comment='Error message if failed'),
sa.Column('ip_address', sa.String(length=45), nullable=True, comment='Client IP address'),
sa.Column('user_agent', sa.String(length=500), nullable=True, comment='User agent string'),
sa.Column('request_id', sa.String(length=255), nullable=False, comment='Request ID for tracking'),
sa.Column('start_time', sa.BigInteger(), nullable=False, comment='Request start time (milliseconds since epoch)'),
sa.Column('end_time', sa.BigInteger(), nullable=True, comment='Request end time (milliseconds since epoch)'),
sa.Column('gmt_created', sa.DateTime(timezone=True), nullable=False, comment='Created time'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_audit_api_name', 'audit_log', ['api_name'], unique=False)
op.create_index('idx_audit_gmt_created', 'audit_log', ['gmt_created'], unique=False)
op.create_index('idx_audit_http_method', 'audit_log', ['http_method'], unique=False)
op.create_index('idx_audit_request_id', 'audit_log', ['request_id'], unique=False)
op.create_index('idx_audit_resource_id', 'audit_log', ['resource_id'], unique=False)
op.create_index('idx_audit_resource_type', 'audit_log', ['resource_type'], unique=False)
op.create_index('idx_audit_start_time', 'audit_log', ['start_time'], unique=False)
op.create_index('idx_audit_status_code', 'audit_log', ['status_code'], unique=False)
op.create_index('idx_audit_user_id', 'audit_log', ['user_id'], unique=False)
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index('idx_audit_user_id', table_name='audit_log')
op.drop_index('idx_audit_status_code', table_name='audit_log')
op.drop_index('idx_audit_start_time', table_name='audit_log')
op.drop_index('idx_audit_resource_type', table_name='audit_log')
op.drop_index('idx_audit_resource_id', table_name='audit_log')
op.drop_index('idx_audit_request_id', table_name='audit_log')
op.drop_index('idx_audit_http_method', table_name='audit_log')
op.drop_index('idx_audit_gmt_created', table_name='audit_log')
op.drop_index('idx_audit_api_name', table_name='audit_log')
op.drop_table('audit_log')
# ### end Alembic commands ###
Loading
Loading