100 lines
2.8 KiB
Python
100 lines
2.8 KiB
Python
# Copyright (c) 2023-present Plane Software, Inc. and contributors
|
|
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# See the LICENSE file for details.
|
|
|
|
# Python imports
|
|
import logging
|
|
from typing import Optional, Dict, Any
|
|
|
|
# Third party imports
|
|
from pymongo.collection import Collection
|
|
from celery import shared_task
|
|
|
|
# Django imports
|
|
from plane.settings.mongo import MongoConnection
|
|
from plane.utils.exception_logger import log_exception
|
|
from plane.db.models import APIActivityLog
|
|
|
|
|
|
logger = logging.getLogger("plane.worker")
|
|
|
|
|
|
def get_mongo_collection() -> Optional[Collection]:
|
|
"""
|
|
Returns the MongoDB collection for external API activity logs.
|
|
"""
|
|
if not MongoConnection.is_configured():
|
|
logger.info("MongoDB not configured")
|
|
return None
|
|
|
|
try:
|
|
return MongoConnection.get_collection("api_activity_logs")
|
|
except Exception as e:
|
|
logger.error(f"Error getting MongoDB collection: {str(e)}")
|
|
log_exception(e)
|
|
return None
|
|
|
|
|
|
def safe_decode_body(content: bytes) -> Optional[str]:
|
|
"""
|
|
Safely decodes request/response body content, handling binary data.
|
|
Returns "[Binary Content]" if the content is binary, or a string representation of the content.
|
|
Returns None if the content is None or empty.
|
|
"""
|
|
# If the content is None, return None
|
|
if content is None:
|
|
return None
|
|
|
|
# If the content is an empty bytes object, return None
|
|
if content == b"":
|
|
return None
|
|
|
|
# Check if content is binary by looking for common binary file signatures
|
|
if content.startswith(b"\x89PNG") or content.startswith(b"\xff\xd8\xff") or content.startswith(b"%PDF"):
|
|
return "[Binary Content]"
|
|
|
|
try:
|
|
return content.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
return "[Could not decode content]"
|
|
|
|
|
|
def log_to_mongo(log_document: Dict[str, Any]) -> bool:
|
|
"""
|
|
Logs the request to MongoDB if available.
|
|
"""
|
|
mongo_collection = get_mongo_collection()
|
|
if mongo_collection is None:
|
|
logger.error("MongoDB not configured")
|
|
return False
|
|
|
|
try:
|
|
mongo_collection.insert_one(log_document)
|
|
return True
|
|
except Exception as e:
|
|
log_exception(e)
|
|
return False
|
|
|
|
|
|
def log_to_postgres(log_data: Dict[str, Any]) -> bool:
|
|
"""
|
|
Fallback to logging to PostgreSQL if MongoDB is unavailable.
|
|
"""
|
|
try:
|
|
APIActivityLog.objects.create(**log_data)
|
|
return True
|
|
except Exception as e:
|
|
log_exception(e)
|
|
return False
|
|
|
|
|
|
@shared_task
|
|
def process_logs(log_data: Dict[str, Any], mongo_log: Dict[str, Any]) -> None:
|
|
"""
|
|
Process logs to save to MongoDB or Postgres based on the configuration
|
|
"""
|
|
|
|
if MongoConnection.is_configured():
|
|
log_to_mongo(mongo_log)
|
|
else:
|
|
log_to_postgres(log_data)
|