Files
wehub-resource-sync c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

645 lines
24 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import importlib.metadata
import logging
import logging.handlers
import os
import platform
import sys
import tempfile
import traceback
from collections.abc import MutableMapping
from datetime import datetime
from pathlib import Path
from typing import Any, Protocol
import structlog
def _get_cognee_version() -> str:
"""Get version without importing cognee (avoids triggering __init__.py)."""
import importlib.metadata as _meta
from contextlib import suppress
with suppress(FileNotFoundError, StopIteration):
_pyproject = Path(__file__).parent.parent.parent / "pyproject.toml"
with open(_pyproject, encoding="utf-8") as f:
_ver = (
next(line for line in f if line.startswith("version")).split("=")[1].strip("'\"\n ")
)
return f"{_ver}-local"
try:
return _meta.version("cognee")
except _meta.PackageNotFoundError:
return "unknown"
cognee_version = _get_cognee_version()
# Configure external library logging
def configure_external_library_logging() -> None:
"""Configure logging for external libraries to reduce verbosity.
Sets env vars eagerly (cheap) but only configures litellm's Python
objects if litellm is already imported. If not, the env vars are
enough — litellm reads them on its own import.
"""
# Set environment variables to suppress LiteLLM logging.
# litellm reads these on import, so setting them early is sufficient
# even if litellm hasn't been imported yet.
os.environ.setdefault("LITELLM_LOG", "ERROR")
os.environ.setdefault("LITELLM_SET_VERBOSE", "False")
# Suppress loggers by name (works even before litellm is imported —
# Python's logging module pre-creates the logger objects).
loggers_to_suppress = [
"litellm",
"litellm.litellm_core_utils.logging_worker",
"litellm.litellm_core_utils",
"litellm.proxy",
"litellm.router",
"openai._base_client",
"LiteLLM",
"LiteLLM.core",
"LiteLLM.logging_worker",
"litellm.logging_worker",
]
for logger_name in loggers_to_suppress:
logging.getLogger(logger_name).setLevel(logging.CRITICAL)
logging.getLogger(logger_name).disabled = True
# Only touch litellm's module-level flags if it's already imported.
# This avoids a ~900ms cold import just to set verbose=False.
litellm = sys.modules.get("litellm")
if litellm is not None:
litellm.set_verbose = False # ty:ignore[unresolved-attribute]
if hasattr(litellm, "suppress_debug_info"):
litellm.suppress_debug_info = True # ty:ignore[unresolved-attribute]
if hasattr(litellm, "turn_off_message"):
litellm.turn_off_message = True # ty:ignore[unresolved-attribute]
if hasattr(litellm, "_turn_on_debug"):
litellm._turn_on_debug = False # ty:ignore[unresolved-attribute]
# Export common log levels
DEBUG = logging.DEBUG
INFO = logging.INFO
WARNING = logging.WARNING
ERROR = logging.ERROR
CRITICAL = logging.CRITICAL
log_levels = {
"CRITICAL": logging.CRITICAL,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"INFO": logging.INFO,
"DEBUG": logging.DEBUG,
"NOTSET": logging.NOTSET,
}
# Track if structlog logging has been configured
_is_structlog_configured = False
def resolve_logs_dir() -> Path | None:
"""Resolve a writable logs directory.
Priority:
1) BaseConfig.logs_root_directory (respects COGNEE_LOGS_DIR)
2) /tmp/cognee_logs (default, best-effort create)
Returns a Path or None if none are writable/creatable.
"""
from cognee.base_config import get_base_config
base_config = get_base_config()
logs_root_directory = Path(base_config.logs_root_directory)
try:
logs_root_directory.mkdir(parents=True, exist_ok=True)
if os.access(logs_root_directory, os.W_OK):
return logs_root_directory
except Exception:
pass
try:
tmp_log_path = Path(os.path.join("/tmp", "cognee_logs"))
tmp_log_path.mkdir(parents=True, exist_ok=True)
if os.access(tmp_log_path, os.W_OK):
return tmp_log_path
except Exception:
pass
return None
# Maximum number of log files to keep
MAX_LOG_FILES = 10
# Log rotation defaults — override via COGNEE_LOG_MAX_BYTES / COGNEE_LOG_BACKUP_COUNT
LOG_MAX_BYTES = int(os.getenv("COGNEE_LOG_MAX_BYTES", 50 * 1024 * 1024)) # 50 MB
LOG_BACKUP_COUNT = int(os.getenv("COGNEE_LOG_BACKUP_COUNT", 5)) # 5 backups → 300 MB cap
# Version information
PYTHON_VERSION = platform.python_version()
STRUCTLOG_VERSION = structlog.__version__
COGNEE_VERSION = cognee_version
OS_INFO = f"{platform.system()} {platform.release()} ({platform.version()})"
class PlainFileHandler(logging.handlers.RotatingFileHandler):
"""A rotating file handler that writes simpler plain text log entries.
Inherits from RotatingFileHandler so log files are automatically rotated
when they reach maxBytes, keeping at most backupCount old files.
"""
def emit(self, record) -> None:
try:
# Check if stream is available before trying to write
if self.stream is None:
self.stream = self._open()
# Extract the message from the structlog record
if isinstance(record.msg, dict) and "event" in record.msg:
# Extract the basic message
message = record.msg.get("event", "")
# Extract additional context
context = {
k: v
for k, v in record.msg.items()
if k not in ("event", "logger", "level", "timestamp")
}
# Format context if present
context_str = ""
if context:
context_str = " " + " ".join(
f"{k}={v}" for k, v in context.items() if k != "exc_info"
)
# Get the logger name from the record or from the structlog context
logger_name = record.msg.get("logger", record.name)
# Format timestamp
timestamp = datetime.now().strftime(get_timestamp_format())
# Create the log entry
log_entry = f"{timestamp} [{record.levelname.ljust(8)}] {message}{context_str} [{logger_name}]\n"
# Write to file
self.stream.write(log_entry)
self.flush()
# Handle exception if present
# Check both record.exc_info and the 'exc_info' in the message dict
record_has_exc = record.exc_info and record.exc_info != (None, None, None)
msg_has_exc = "exc_info" in record.msg and record.msg["exc_info"]
if record_has_exc:
# Use the exception info from the record
tb_str = "".join(traceback.format_exception(*record.exc_info))
self.stream.write(tb_str + "\n")
self.flush()
elif msg_has_exc and isinstance(record.msg["exc_info"], tuple):
# Use the exception info from the message
tb_str = "".join(traceback.format_exception(*record.msg["exc_info"]))
self.stream.write(tb_str + "\n")
self.flush()
elif msg_has_exc and hasattr(record.msg["exc_info"], "__traceback__"):
# Handle exceptions that are passed directly
exc = record.msg["exc_info"]
tb_str = "".join(traceback.format_exception(type(exc), exc, exc.__traceback__))
self.stream.write(tb_str + "\n")
self.flush()
else:
# Fall back to standard handling for non-structlog messages
msg = self.format(record)
self.stream.write(msg + self.terminator)
self.flush()
# Handle exception if present in regular record
if record.exc_info and record.exc_info != (None, None, None):
tb_str = "".join(traceback.format_exception(*record.exc_info))
self.stream.write(tb_str + "\n")
self.flush()
except Exception as e:
self.handleError(record)
# Write error about handling this record
if self.stream:
self.stream.write(f"Error in log handler: {e}\n")
self.flush()
def get_logger(name=None, level=None) -> logging.Logger:
"""Get a logger.
If `setup_logging()` has not been called, returns a standard Python logger.
If `setup_logging()` has been called, returns a structlog logger.
"""
if _is_structlog_configured:
return structlog.get_logger(name if name else __name__)
else:
logger = logging.getLogger(name if name else __name__)
if level is not None:
logger.setLevel(level)
return logger
def log_database_configuration(logger) -> None:
"""Log the current database configuration for all database types"""
# NOTE: Has to be imporated at runtime to avoid circular import
from cognee.infrastructure.databases.graph.config import get_graph_config
from cognee.infrastructure.databases.relational.config import get_relational_config
from cognee.infrastructure.databases.vector.config import get_vectordb_config
try:
# Get base database directory path
from cognee.base_config import get_base_config
base_config = get_base_config()
databases_path = os.path.join(base_config.system_root_directory, "databases")
# Log concise database info
logger.info(f"Database storage: {databases_path}")
except Exception as e:
logger.debug(f"Could not retrieve database configuration: {str(e)}")
def cleanup_old_logs(logs_dir, max_files) -> bool:
"""
Removes old log files, keeping only the most recent ones.
Args:
logs_dir: Directory containing log files
max_files: Maximum number of log files to keep
"""
logger = structlog.get_logger()
try:
# Get all .log files in the directory (excluding README and other files)
log_files = [f for f in logs_dir.glob("*.log") if f.is_file()]
# Sort log files by modification time (newest first)
log_files.sort(key=lambda x: x.stat().st_mtime, reverse=True)
# Remove old files that exceed the maximum
if len(log_files) > max_files:
deleted_count = 0
for old_file in log_files[max_files:]:
try:
old_file.unlink()
deleted_count += 1
# Only log individual files in non-CLI mode
if os.getenv("COGNEE_CLI_MODE") != "true":
logger.info(f"Deleted old log file: {old_file}")
except Exception as e:
# Always log errors
logger.error(f"Failed to delete old log file {old_file}: {e}")
# In CLI mode, show compact summary
if os.getenv("COGNEE_CLI_MODE") == "true" and deleted_count > 0:
logger.info(f"Cleaned up {deleted_count} old log files")
return True
except Exception as e:
logger.error(f"Error cleaning up log files: {e}")
return False
def setup_logging(log_level=None, name=None) -> bool:
"""Sets up the logging configuration with structlog integration.
Args:
log_level: The logging level to use (default: None, uses INFO)
name: Optional logger name (default: None, uses __name__)
Returns:
A configured structlog logger instance
"""
global _is_structlog_configured
# Regular detailed logging for non-CLI usage
log_level = log_level if log_level else log_levels[os.getenv("LOG_LEVEL", "INFO").upper()]
# Configure external library logging early to suppress verbose output
configure_external_library_logging()
# Add custom filter to suppress LiteLLM worker cancellation errors
class LiteLLMCancellationFilter(logging.Filter):
"""Filter to suppress LiteLLM worker cancellation messages"""
def filter(self, record):
# Check if this is a LiteLLM-related logger
if hasattr(record, "name") and "litellm" in record.name.lower():
return False
# Check message content for cancellation errors
if hasattr(record, "msg") and record.msg:
msg_str = str(record.msg).lower()
if any(
keyword in msg_str
for keyword in [
"loggingworker cancelled",
"logging_worker.py",
"cancellederror",
"litellm:error",
]
):
return False
# Check formatted message
try:
if hasattr(record, "getMessage"):
formatted_msg = record.getMessage().lower()
if any(
keyword in formatted_msg
for keyword in [
"loggingworker cancelled",
"logging_worker.py",
"cancellederror",
"litellm:error",
]
):
return False
except Exception:
pass
return True
# Apply the filter to root logger and specific loggers
cancellation_filter = LiteLLMCancellationFilter()
logging.getLogger().addFilter(cancellation_filter)
logging.getLogger("litellm").addFilter(cancellation_filter)
# Add custom filter to suppress LiteLLM worker cancellation errors
class LiteLLMFilter(logging.Filter):
def filter(self, record) -> bool:
# Suppress LiteLLM worker cancellation errors
if hasattr(record, "msg") and isinstance(record.msg, str):
msg_lower = record.msg.lower()
if any(
phrase in msg_lower
for phrase in [
"loggingworker cancelled",
"cancellederror",
"logging_worker.py",
"loggingerror",
]
):
return False
return True
# Apply filter to root logger
litellm_filter = LiteLLMFilter()
logging.getLogger().addFilter(litellm_filter)
def exception_handler(
logger: Any, method_name: str, event_dict: MutableMapping[str, Any]
) -> dict[str, Any]:
"""Custom processor to handle uncaught exceptions."""
event_dict = dict(event_dict)
# Check if there's an exc_info that needs to be processed
if event_dict.get("exc_info"):
# If it's already a tuple, use it directly
if isinstance(event_dict["exc_info"], tuple):
exc_type, exc_value, tb = event_dict["exc_info"]
else:
exc_type, exc_value, tb = sys.exc_info()
if exc_type and hasattr(exc_type, "__name__"):
event_dict["exception_type"] = exc_type.__name__
event_dict["exception_message"] = str(exc_value)
event_dict["traceback"] = True
return event_dict
# Configure structlog
structlog.configure(
processors=[
structlog.stdlib.filter_by_level,
structlog.stdlib.add_logger_name,
structlog.stdlib.add_log_level,
structlog.stdlib.PositionalArgumentsFormatter(),
structlog.processors.TimeStamper(fmt=get_timestamp_format(), utc=True),
structlog.processors.StackInfoRenderer(),
exception_handler, # Add our custom exception handler
structlog.processors.UnicodeDecoder(),
structlog.stdlib.ProcessorFormatter.wrap_for_formatter,
],
context_class=dict,
logger_factory=structlog.stdlib.LoggerFactory(),
wrapper_class=structlog.stdlib.BoundLogger,
cache_logger_on_first_use=True,
)
# Set up system-wide exception handling
def handle_exception(exc_type, exc_value, tb) -> None:
"""Handle any uncaught exception safely."""
if issubclass(exc_type, KeyboardInterrupt):
# Let KeyboardInterrupt pass through
sys.__excepthook__(exc_type, exc_value, tb)
return
try:
logger = structlog.get_logger()
logger.error(
"Exception",
exc_info=(exc_type, exc_value, tb),
)
except Exception:
print("\n[Warning] Could not render rich traceback. Falling back to plain traceback.\n")
traceback.print_exception(exc_type, exc_value, tb)
sys.__excepthook__(exc_type, exc_value, tb)
return
# Normal path - hand back to original hook
sys.__excepthook__(exc_type, exc_value, tb)
# Install the exception handler so uncaught exceptions are logged
# through structlog before the default hook prints and exits.
sys.excepthook = handle_exception
# Create console formatter for standard library logging
console_formatter = structlog.stdlib.ProcessorFormatter(
processor=structlog.dev.ConsoleRenderer(
colors=True,
force_colors=True,
# show_locals=False: the default RichTracebackFormatter renders every
# frame's locals, which in the retrieval path include graph objects
# carrying embedding vectors and deep node/edge references. Rendering
# those recursively builds millions of objects + stringifies the
# embeddings, spiking RAM to multiple GB and OOM-killing the process
# whenever an exception is logged mid-search. Disable locals rendering.
exception_formatter=structlog.dev.RichTracebackFormatter(show_locals=False),
level_styles={
"critical": structlog.dev.RED,
"exception": structlog.dev.RED,
"error": structlog.dev.RED,
"warn": structlog.dev.YELLOW,
"warning": structlog.dev.YELLOW,
"info": structlog.dev.GREEN,
"debug": structlog.dev.BLUE,
}, # ty:ignore[invalid-argument-type]
),
)
# Setup handler with newlines for console output
class NewlineStreamHandler(logging.StreamHandler):
def emit(self, record) -> None:
try:
msg = self.format(record)
stream = self.stream
if hasattr(stream, "closed") and stream.closed:
return
stream.write("\n" + msg + self.terminator)
self.flush()
except Exception:
self.handleError(record)
# Use our custom handler for console output
stream_handler = NewlineStreamHandler(sys.stderr)
stream_handler.setFormatter(console_formatter)
stream_handler.setLevel(log_level)
root_logger = logging.getLogger()
if root_logger.hasHandlers():
root_logger.handlers.clear()
root_logger.addHandler(stream_handler)
# Note: root logger needs to be set at NOTSET to allow all messages through and specific stream and file handlers
# can define their own levels.
root_logger.setLevel(logging.NOTSET)
# --- File logging (opt-out via COGNEE_LOG_FILE=false) ---
# Set COGNEE_LOG_FILE=false to disable file logging entirely.
log_file_enabled = os.getenv("COGNEE_LOG_FILE", "true").lower() not in ("false", "0", "no")
log_file_path = None
if log_file_enabled:
# Resolve logs directory with env and safe fallbacks
logs_dir = resolve_logs_dir()
# Check if we already have a log file path from the environment
# NOTE: environment variable must be used here as it allows us to
# log to a single file with a name based on a timestamp in a multiprocess setting.
# Without it, we would have a separate log file for every process.
log_file_path = os.environ.get("LOG_FILE_NAME")
if not log_file_path and logs_dir is not None:
# Create a new log file name with the cognee start time
start_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_file_path = str((logs_dir / f"{start_time}.log").resolve())
os.environ["LOG_FILE_NAME"] = log_file_path
try:
# Rotating file handler: caps each file at LOG_MAX_BYTES,
# keeps LOG_BACKUP_COUNT old files (default 50 MB × 5 = 250 MB total).
file_handler = PlainFileHandler(
log_file_path, # ty:ignore[invalid-argument-type]
maxBytes=LOG_MAX_BYTES,
backupCount=LOG_BACKUP_COUNT,
encoding="utf-8",
)
file_handler.setLevel(log_level)
root_logger.addHandler(file_handler)
except Exception as e:
# Logging to file is not mandatory — warn on console and continue.
root_logger.warning(
f"Warning: Could not create log file handler at {log_file_path}: {e}"
)
if log_level > logging.DEBUG:
import warnings
from sqlalchemy.exc import SAWarning
warnings.filterwarnings(
"ignore", category=SAWarning, module="dlt.destinations.impl.sqlalchemy.merge_job"
)
warnings.filterwarnings(
"ignore", category=SAWarning, module="dlt.destinations.impl.sqlalchemy.load_jobs"
)
# Clean up old log files, keeping only the most recent ones
if log_file_enabled and logs_dir is not None:
cleanup_old_logs(logs_dir, MAX_LOG_FILES)
# Mark logging as configured
_is_structlog_configured = True
# Get a configured logger
logger = structlog.get_logger(name if name else __name__)
if log_file_path is not None:
logger.info(f"Log file created at: {log_file_path}", log_file=log_file_path)
# Defer heavy database config logging to first actual pipeline use.
# Importing graph/vector/relational configs triggers litellm (~900ms)
# and other heavy dependencies. Log basic info now, details later.
_log_deferred_info(logger)
return logger
def _log_deferred_info(logger) -> None:
"""Log lightweight startup info. Heavy DB config is logged on first pipeline call."""
logger.warning(
"Cognee 1.0 changes: "
"New API — remember/recall/forget/improve (V1 add/cognify/search still work). "
"Session memory enabled by default (CACHING=false to disable). "
"Multi-user access control on by default (ENABLE_BACKEND_ACCESS_CONTROL=false to disable). "
"Agents (@cognee.agent) auto-verified on registration. "
"See https://docs.cognee.ai/"
)
try:
from cognee.base_config import get_base_config
base_config = get_base_config()
databases_path = os.path.join(base_config.system_root_directory, "databases")
except Exception:
databases_path = "unknown"
logger.info(
"Logging initialized",
python_version=PYTHON_VERSION,
structlog_version=STRUCTLOG_VERSION,
cognee_version=COGNEE_VERSION,
os_info=OS_INFO,
database_path=databases_path,
)
logger.info(f"Database storage: {databases_path}")
def get_log_file_location() -> str | None:
"""Return the file path of the log file in use, if any."""
root_logger = logging.getLogger()
# Loop through handlers to find the FileHandler
for handler in root_logger.handlers:
if isinstance(handler, logging.FileHandler):
return handler.baseFilename
def get_timestamp_format() -> str:
# NOTE: Some users have complained that Cognee crashes when trying to get microsecond value
# Added handler to not use microseconds if users can't access it
logger = structlog.get_logger()
try:
# We call datetime.now() here to test if microseconds are supported.
# If they are not supported a ValueError will be raised
datetime.now().strftime("%Y-%m-%dT%H:%M:%S.%f")
return "%Y-%m-%dT%H:%M:%S.%f"
except Exception as e:
logger.debug(f"Exception caught: {e}")
logger.debug(
"Could not use microseconds for the logging timestamp, defaulting to use hours minutes and seconds only"
)
# We call datetime.now() here to test if won't break.
datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
# We return the timestamp format without microseconds as they are not supported
return "%Y-%m-%dT%H:%M:%S"