Skip to content

Commit ce378f1

Browse files
authored
feat: support auditing (#970)
1 parent dacf840 commit ce378f1

27 files changed

Lines changed: 2474 additions & 44 deletions
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
auditLog:
2+
type: object
3+
description: Audit log entry
4+
properties:
5+
id:
6+
type: string
7+
description: Audit log ID
8+
user_id:
9+
type: string
10+
nullable: true
11+
description: User ID who performed the action
12+
username:
13+
type: string
14+
nullable: true
15+
description: Username for display
16+
resource_type:
17+
type: string
18+
enum: [collection, document, bot, chat, message, api_key, llm_provider, llm_provider_model, model_service_provider, user, flow, search_test]
19+
nullable: true
20+
description: Type of resource
21+
resource_id:
22+
type: string
23+
nullable: true
24+
description: ID of the resource (extracted at query time)
25+
api_name:
26+
type: string
27+
description: API operation name
28+
http_method:
29+
type: string
30+
description: HTTP method (POST, PUT, DELETE)
31+
path:
32+
type: string
33+
description: API path
34+
status_code:
35+
type: integer
36+
nullable: true
37+
description: HTTP status code
38+
start_time:
39+
type: integer
40+
format: int64
41+
description: Request start time (milliseconds since epoch)
42+
end_time:
43+
type: integer
44+
format: int64
45+
nullable: true
46+
description: Request end time (milliseconds since epoch)
47+
duration_ms:
48+
type: integer
49+
nullable: true
50+
description: Request duration in milliseconds (calculated)
51+
request_data:
52+
type: string
53+
nullable: true
54+
description: Request data (JSON string)
55+
response_data:
56+
type: string
57+
nullable: true
58+
description: Response data (JSON string)
59+
error_message:
60+
type: string
61+
nullable: true
62+
description: Error message if failed
63+
ip_address:
64+
type: string
65+
nullable: true
66+
description: Client IP address
67+
user_agent:
68+
type: string
69+
nullable: true
70+
description: User agent string
71+
request_id:
72+
type: string
73+
description: Request ID for tracking
74+
created:
75+
type: string
76+
format: date-time
77+
description: Created timestamp
78+
79+
auditLogList:
80+
type: object
81+
description: List of audit logs
82+
properties:
83+
items:
84+
type: array
85+
description: Audit log entries
86+
items:
87+
$ref: '#/auditLog'
88+
89+

aperag/api/openapi.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,12 @@ paths:
8888
/prompt-templates:
8989
$ref: './paths/prompt_templates.yaml#/promptTemplates'
9090

91+
# audit
92+
/audit-logs:
93+
$ref: './paths/audit.yaml#/audit_logs'
94+
/audit-logs/{audit_id}:
95+
$ref: './paths/audit.yaml#/audit_log_detail'
96+
9197
# users
9298
/invite:
9399
$ref: './paths/auth.yaml#/invite'

aperag/api/paths/audit.yaml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
audit_logs:
2+
get:
3+
tags:
4+
- audit
5+
summary: List audit logs
6+
description: List audit logs with filtering options
7+
operationId: list_audit_logs
8+
parameters:
9+
- name: api_name
10+
in: query
11+
required: false
12+
schema:
13+
type: string
14+
description: Filter by API name
15+
- name: start_date
16+
in: query
17+
required: false
18+
schema:
19+
type: string
20+
format: date-time
21+
description: Filter by start date
22+
- name: end_date
23+
in: query
24+
required: false
25+
schema:
26+
type: string
27+
format: date-time
28+
description: Filter by end date
29+
- name: limit
30+
in: query
31+
required: false
32+
schema:
33+
type: integer
34+
maximum: 5000
35+
default: 1000
36+
description: Maximum number of records
37+
responses:
38+
"200":
39+
description: Audit logs retrieved successfully
40+
content:
41+
application/json:
42+
schema:
43+
$ref: "../components/schemas/audit.yaml#/auditLogList"
44+
"403":
45+
description: Admin access required
46+
47+
audit_log_detail:
48+
get:
49+
tags:
50+
- audit
51+
summary: Get audit log detail
52+
description: Get a specific audit log by ID
53+
operationId: get_audit_log
54+
parameters:
55+
- name: audit_id
56+
in: path
57+
required: true
58+
schema:
59+
type: string
60+
description: Audit log ID
61+
responses:
62+
"200":
63+
description: Audit log retrieved successfully
64+
content:
65+
application/json:
66+
schema:
67+
$ref: "../components/schemas/audit.yaml#/auditLog"
68+
"403":
69+
description: Admin access required
70+
"404":
71+
description: Audit log not found
72+
73+

aperag/app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from aperag.exception_handlers import register_exception_handlers
1818
from aperag.llm.litellm_track import register_opik_llm_track
1919
from aperag.views.api_key import router as api_key_router
20+
from aperag.views.audit import router as audit_router
2021
from aperag.views.auth import router as auth_router
2122
from aperag.views.chat_completion import router as chat_completion_router
2223
from aperag.views.config import router as config_router
@@ -38,6 +39,7 @@
3839
app.include_router(auth_router, prefix="/api/v1")
3940
app.include_router(main_router, prefix="/api/v1")
4041
app.include_router(api_key_router, prefix="/api/v1")
42+
app.include_router(audit_router, prefix="/api/v1") # Add audit router
4143
app.include_router(flow_router, prefix="/api/v1")
4244
app.include_router(llm_router, prefix="/api/v1")
4345
app.include_router(chat_completion_router, prefix="/v1")

aperag/db/models.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
Boolean,
2525
Column,
2626
DateTime,
27+
Index,
2728
Integer,
2829
String,
2930
Text,
@@ -746,3 +747,67 @@ def update_spec(self, desired_state: IndexDesiredState = None, created_by: str =
746747
self.created_by = created_by
747748
self.version += 1
748749
self.gmt_updated = utc_now()
750+
751+
752+
class AuditResource(str, Enum):
753+
"""Audit resource types"""
754+
755+
COLLECTION = "collection"
756+
DOCUMENT = "document"
757+
BOT = "bot"
758+
CHAT = "chat"
759+
MESSAGE = "message"
760+
API_KEY = "api_key"
761+
LLM_PROVIDER = "llm_provider"
762+
LLM_PROVIDER_MODEL = "llm_provider_model"
763+
MODEL_SERVICE_PROVIDER = "model_service_provider"
764+
USER = "user"
765+
CONFIG = "config"
766+
INVITATION = "invitation"
767+
AUTH = "auth"
768+
CHAT_COMPLETION = "chat_completion"
769+
SEARCH_TEST = "search_test"
770+
LLM = "llm"
771+
FLOW = "flow"
772+
SYSTEM = "system"
773+
774+
775+
class AuditLog(Base):
776+
"""Audit log model to track all system operations"""
777+
778+
__tablename__ = "audit_log"
779+
780+
id = Column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
781+
user_id = Column(String(36), nullable=True, comment="User ID")
782+
username = Column(String(255), nullable=True, comment="Username")
783+
resource_type = Column(EnumColumn(AuditResource), nullable=True, comment="Resource type")
784+
resource_id = Column(String(255), nullable=True, comment="Resource ID (extracted at query time)")
785+
api_name = Column(String(255), nullable=False, comment="API operation name")
786+
http_method = Column(String(10), nullable=False, comment="HTTP method (POST, PUT, DELETE)")
787+
path = Column(String(512), nullable=False, comment="API path")
788+
status_code = Column(Integer, nullable=True, comment="HTTP status code")
789+
request_data = Column(Text, nullable=True, comment="Request data (JSON)")
790+
response_data = Column(Text, nullable=True, comment="Response data (JSON)")
791+
error_message = Column(Text, nullable=True, comment="Error message if failed")
792+
ip_address = Column(String(45), nullable=True, comment="Client IP address")
793+
user_agent = Column(String(500), nullable=True, comment="User agent string")
794+
request_id = Column(String(255), nullable=False, comment="Request ID for tracking")
795+
start_time = Column(BigInteger, nullable=False, comment="Request start time (milliseconds since epoch)")
796+
end_time = Column(BigInteger, nullable=True, comment="Request end time (milliseconds since epoch)")
797+
gmt_created = Column(DateTime(timezone=True), nullable=False, default=utc_now, comment="Created time")
798+
799+
# Index for better query performance
800+
__table_args__ = (
801+
Index("idx_audit_user_id", "user_id"),
802+
Index("idx_audit_resource_type", "resource_type"),
803+
Index("idx_audit_api_name", "api_name"),
804+
Index("idx_audit_http_method", "http_method"),
805+
Index("idx_audit_status_code", "status_code"),
806+
Index("idx_audit_gmt_created", "gmt_created"),
807+
Index("idx_audit_resource_id", "resource_id"),
808+
Index("idx_audit_request_id", "request_id"),
809+
Index("idx_audit_start_time", "start_time"),
810+
)
811+
812+
def __repr__(self):
813+
return f"<AuditLog(id={self.id}, user={self.username}, api={self.api_name}, method={self.http_method}, status={self.status_code})>"

aperag/exceptions.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import traceback
1615
from enum import Enum
1716
from http import HTTPStatus
1817
from typing import Any, Dict, Optional
@@ -90,7 +89,6 @@ def __init__(self, error_code: ErrorCode, message: Optional[str] = None, details
9089
self.error_code = error_code
9190
self.message = message or error_code.error_name.replace("_", " ").title()
9291
self.details = details or {}
93-
traceback.print_stack()
9492
super().__init__(self.message)
9593

9694
@property
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
"""empty message
2+
3+
Revision ID: 2768dfee8bbc
4+
Revises: 12ea6d2bf365
5+
Create Date: 2025-06-21 00:28:36.443046
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = '2768dfee8bbc'
16+
down_revision: Union[str, None] = '12ea6d2bf365'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
"""Upgrade schema."""
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.create_table('audit_log',
25+
sa.Column('id', sa.String(length=36), nullable=False),
26+
sa.Column('user_id', sa.String(length=36), nullable=True, comment='User ID'),
27+
sa.Column('username', sa.String(length=255), nullable=True, comment='Username'),
28+
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'),
29+
sa.Column('resource_id', sa.String(length=255), nullable=True, comment='Resource ID (extracted at query time)'),
30+
sa.Column('api_name', sa.String(length=255), nullable=False, comment='API operation name'),
31+
sa.Column('http_method', sa.String(length=10), nullable=False, comment='HTTP method (POST, PUT, DELETE)'),
32+
sa.Column('path', sa.String(length=512), nullable=False, comment='API path'),
33+
sa.Column('status_code', sa.Integer(), nullable=True, comment='HTTP status code'),
34+
sa.Column('request_data', sa.Text(), nullable=True, comment='Request data (JSON)'),
35+
sa.Column('response_data', sa.Text(), nullable=True, comment='Response data (JSON)'),
36+
sa.Column('error_message', sa.Text(), nullable=True, comment='Error message if failed'),
37+
sa.Column('ip_address', sa.String(length=45), nullable=True, comment='Client IP address'),
38+
sa.Column('user_agent', sa.String(length=500), nullable=True, comment='User agent string'),
39+
sa.Column('request_id', sa.String(length=255), nullable=False, comment='Request ID for tracking'),
40+
sa.Column('start_time', sa.BigInteger(), nullable=False, comment='Request start time (milliseconds since epoch)'),
41+
sa.Column('end_time', sa.BigInteger(), nullable=True, comment='Request end time (milliseconds since epoch)'),
42+
sa.Column('gmt_created', sa.DateTime(timezone=True), nullable=False, comment='Created time'),
43+
sa.PrimaryKeyConstraint('id')
44+
)
45+
op.create_index('idx_audit_api_name', 'audit_log', ['api_name'], unique=False)
46+
op.create_index('idx_audit_gmt_created', 'audit_log', ['gmt_created'], unique=False)
47+
op.create_index('idx_audit_http_method', 'audit_log', ['http_method'], unique=False)
48+
op.create_index('idx_audit_request_id', 'audit_log', ['request_id'], unique=False)
49+
op.create_index('idx_audit_resource_id', 'audit_log', ['resource_id'], unique=False)
50+
op.create_index('idx_audit_resource_type', 'audit_log', ['resource_type'], unique=False)
51+
op.create_index('idx_audit_start_time', 'audit_log', ['start_time'], unique=False)
52+
op.create_index('idx_audit_status_code', 'audit_log', ['status_code'], unique=False)
53+
op.create_index('idx_audit_user_id', 'audit_log', ['user_id'], unique=False)
54+
# ### end Alembic commands ###
55+
56+
57+
def downgrade() -> None:
58+
"""Downgrade schema."""
59+
# ### commands auto generated by Alembic - please adjust! ###
60+
op.drop_index('idx_audit_user_id', table_name='audit_log')
61+
op.drop_index('idx_audit_status_code', table_name='audit_log')
62+
op.drop_index('idx_audit_start_time', table_name='audit_log')
63+
op.drop_index('idx_audit_resource_type', table_name='audit_log')
64+
op.drop_index('idx_audit_resource_id', table_name='audit_log')
65+
op.drop_index('idx_audit_request_id', table_name='audit_log')
66+
op.drop_index('idx_audit_http_method', table_name='audit_log')
67+
op.drop_index('idx_audit_gmt_created', table_name='audit_log')
68+
op.drop_index('idx_audit_api_name', table_name='audit_log')
69+
op.drop_table('audit_log')
70+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)