项目文件夹

文件
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

728 行
30 KiB
Python

import json
import uuid
from contextlib import contextmanager
from datetime import datetime
import redis
import redis.asyncio as aioredis
from pydantic import BaseModel, ValidationError
from cognee.infrastructure.databases.cache.cache_db_interface import CacheDBInterface
from cognee.infrastructure.databases.cache.models import SessionAgentTraceEntry, SessionQAEntry
from cognee.infrastructure.databases.exceptions import (
CacheConnectionError,
SessionQAEntryValidationError,
)
from cognee.shared.logging_utils import get_logger
logger = get_logger("RedisAdapter")
class RedisAdapter(CacheDBInterface):
"""Redis-backed cache adapter for session QA, trace storage, and coordination."""
def __init__(
self,
host,
port,
lock_name="default_lock",
log_key="usage_logs",
username=None,
password=None,
timeout=240,
blocking_timeout=300,
connection_timeout=30,
session_ttl_seconds: int | None = 604800,
):
"""Initialize sync/async Redis clients and validate connectivity up front."""
super().__init__(host, port, lock_name, log_key)
self.host = host
self.port = port
self.connection_timeout = connection_timeout
self.session_ttl_seconds = session_ttl_seconds
try:
self.sync_redis = redis.Redis(
host=host,
port=port,
username=username,
password=password,
socket_connect_timeout=connection_timeout,
socket_timeout=connection_timeout,
)
self.async_redis = aioredis.Redis(
host=host,
port=port,
username=username,
password=password,
decode_responses=True,
socket_connect_timeout=connection_timeout,
)
self.timeout = timeout
self.blocking_timeout = blocking_timeout
# Validate connection on initialization
self._validate_connection()
logger.info(f"Successfully connected to Redis at {host}:{port}")
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Failed to connect to Redis at {host}:{port}: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error initializing Redis adapter: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
def _validate_connection(self):
"""Validate Redis connection is available."""
try:
self.sync_redis.ping()
except (redis.ConnectionError, redis.TimeoutError) as e:
raise CacheConnectionError(
f"Cannot connect to Redis at {self.host}:{self.port}: {str(e)}"
) from e
@staticmethod
def _session_key(user_id: str, session_id: str) -> str:
"""Build the Redis key for QA session entries."""
return f"agent_sessions:{user_id}:{session_id}"
@staticmethod
def _agent_trace_key(user_id: str, session_id: str) -> str:
"""Build the Redis key for agent trace entries."""
return f"agent_traces:{user_id}:{session_id}"
@staticmethod
def _session_context_key(user_id: str, session_id: str) -> str:
"""Build the Redis key for session-context entries."""
return f"session_context:{user_id}:{session_id}"
@staticmethod
def _build_qa_entry_dump(
question: str,
context: str,
answer: str,
qa_id: str | None = None,
feedback_text: str | None = None,
feedback_score: int | None = None,
used_graph_element_ids: dict | None = None,
memify_metadata: dict | None = None,
used_session_context_ids: list | None = None,
) -> dict:
"""Serialize one QA entry into the normalized Redis payload shape."""
entry = SessionQAEntry(
time=datetime.utcnow().isoformat(),
question=question,
context=context,
answer=answer,
qa_id=qa_id or str(uuid.uuid4()),
feedback_text=feedback_text,
feedback_score=feedback_score,
used_graph_element_ids=used_graph_element_ids,
memify_metadata=memify_metadata,
used_session_context_ids=used_session_context_ids,
)
return entry.model_dump()
@staticmethod
def _build_agent_trace_entry_dump(
trace_id: str,
origin_function: str,
status: str,
memory_query: str = "",
memory_context: str = "",
method_params: dict | None = None,
method_return_value=None,
error_message: str = "",
session_feedback: str = "",
) -> dict:
"""Serialize one agent-trace step into the normalized Redis payload shape."""
entry = SessionAgentTraceEntry(
trace_id=trace_id,
origin_function=origin_function,
status=status,
memory_query=memory_query,
memory_context=memory_context,
method_params=method_params or {},
method_return_value=method_return_value,
error_message=error_message,
session_feedback=session_feedback,
)
return entry.model_dump()
async def _load_entries(self, session_key: str, start: int = 0, end: int = -1) -> list[dict]:
"""Load and deserialize a Redis list slice for the given key."""
raw = await self.async_redis.lrange(session_key, start, end)
return [json.loads(e) for e in raw] if raw else []
async def _write_entry_at(self, session_key: str, index: int, entry_dump: dict) -> None:
"""Overwrite a single serialized entry in-place within a Redis list."""
await self.async_redis.lset(session_key, index, json.dumps(entry_dump))
async def _rewrite_entries(self, session_key: str, entries: list) -> None:
"""Replace the full Redis list contents for a session key."""
await self.async_redis.delete(session_key)
for entry in entries:
await self.async_redis.rpush(session_key, json.dumps(entry))
async def _apply_session_ttl(self, session_key: str) -> None:
"""Refresh the configured TTL for a session-scoped Redis key."""
if self.session_ttl_seconds and self.session_ttl_seconds > 0:
await self.async_redis.expire(session_key, self.session_ttl_seconds)
@staticmethod
def _merge_entry_update(
entry: dict,
question: str | None = None,
context: str | None = None,
answer: str | None = None,
feedback_text: str | None = None,
feedback_score: int | None = None,
used_graph_element_ids: dict | None = None,
memify_metadata: dict | None = None,
used_session_context_ids: list | None = None,
) -> dict:
"""Merge partial QA updates into an existing serialized entry."""
merged = {**entry}
if question is not None:
merged["question"] = question
if context is not None:
merged["context"] = context
if answer is not None:
merged["answer"] = answer
if feedback_text is not None:
merged["feedback_text"] = feedback_text
if feedback_score is not None:
merged["feedback_score"] = feedback_score
if used_graph_element_ids is not None:
merged["used_graph_element_ids"] = used_graph_element_ids
if used_session_context_ids is not None:
merged["used_session_context_ids"] = used_session_context_ids
if memify_metadata is not None:
existing_metadata = merged.get("memify_metadata")
if isinstance(existing_metadata, dict):
merged["memify_metadata"] = {**existing_metadata, **memify_metadata}
else:
merged["memify_metadata"] = memify_metadata
return merged
@staticmethod
def _merge_entry_clear_feedback(entry: dict) -> dict:
"""Return a copy of the entry with feedback fields cleared."""
return {**entry, "feedback_text": None, "feedback_score": None}
@staticmethod
def _validate_entry_dict(entry_dict: dict) -> dict:
"""Validate one serialized QA entry and return its normalized dump."""
try:
return SessionQAEntry.model_validate(entry_dict).model_dump()
except ValidationError as e:
raise SessionQAEntryValidationError(
message=f"Session QA entry validation failed: {e!s}"
) from e
@staticmethod
def _find_index_by_qa_id(entries: list, qa_id: str) -> int | None:
"""Return the list index for a QA entry id, or None when absent."""
for i, entry in enumerate(entries):
if entry.get("qa_id") == qa_id:
return i
return None
def acquire_lock(self):
"""
Acquire the Redis lock manually. Raises if acquisition fails. (Sync because of Ladybug)
"""
lock = self.sync_redis.lock(
name=self.lock_key,
timeout=self.timeout,
blocking_timeout=self.blocking_timeout,
thread_local=False,
)
acquired = lock.acquire()
if not acquired:
raise RuntimeError(f"Could not acquire Redis lock: {self.lock_key}")
self.lock = lock
return lock
def release_lock(self, lock=None):
"""
Release the Redis lock manually, if held. (Sync because of Ladybug)
"""
lock = lock or self.lock
if lock:
try:
lock.release()
except redis.exceptions.LockError:
pass
finally:
if lock is self.lock:
self.lock = None
@contextmanager
def hold_lock(self):
"""
Context manager for acquiring and releasing the Redis lock automatically. (Sync because of Ladybug)
"""
lock = self.acquire_lock()
try:
yield
finally:
self.release_lock(lock)
async def create_qa_entry(
self,
user_id: str,
session_id: str,
question: str,
context: str,
answer: str,
qa_id: str | None = None,
feedback_text: str | None = None,
feedback_score: int | None = None,
used_graph_element_ids: dict | None = None,
memify_metadata: dict | None = None,
used_session_context_ids: list | None = None,
) -> None:
"""
Add a Q/A/context triplet to a Redis list for this session.
Same QA fields as update_qa_entry. Creates the session if it doesn't exist.
"""
try:
session_key = self._session_key(user_id, session_id)
qa_entry = self._build_qa_entry_dump(
question,
context,
answer,
qa_id,
feedback_text,
feedback_score,
used_graph_element_ids=used_graph_element_ids,
memify_metadata=memify_metadata,
used_session_context_ids=used_session_context_ids,
)
await self.async_redis.rpush(session_key, json.dumps(qa_entry))
await self._apply_session_ttl(session_key)
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while adding Q&A: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while adding Q&A to Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def get_latest_qa_entries(
self, user_id: str, session_id: str, last_n: int = 5
) -> list[SessionQAEntry]:
"""
Retrieve the most recent Q/A/context triplet(s) for the given session.
"""
session_key = self._session_key(user_id, session_id)
if last_n == 1:
data = await self.async_redis.lindex(session_key, -1)
return [SessionQAEntry.model_validate_json(data)] if data else None
data = await self.async_redis.lrange(session_key, -last_n, -1)
return [SessionQAEntry.model_validate_json(d) for d in data] if data else []
async def get_all_qa_entries(self, user_id: str, session_id: str) -> list[SessionQAEntry]:
"""
Retrieve all Q/A/context triplets for the given session.
"""
session_key = self._session_key(user_id, session_id)
return [SessionQAEntry(**entry) for entry in await self._load_entries(session_key)]
async def get_qa_entries_by_ids(
self,
user_id: str,
session_id: str,
qa_ids: list[str],
) -> list[SessionQAEntry]:
"""Return matching QA entries for the given session, oldest first."""
wanted_ids = set(qa_ids)
if not wanted_ids:
return []
session_key = self._session_key(user_id, session_id)
return [
SessionQAEntry(**entry)
for entry in await self._load_entries(session_key)
if entry.get("qa_id") in wanted_ids
]
async def update_qa_entry(
self,
user_id: str,
session_id: str,
qa_id: str,
question: str | None = None,
context: str | None = None,
answer: str | None = None,
feedback_text: str | None = None,
feedback_score: int | None = None,
used_graph_element_ids: dict | None = None,
memify_metadata: dict | None = None,
used_session_context_ids: list | None = None,
) -> bool:
"""
Update a QA entry by qa_id. Same QA fields as create_qa_entry.
question/context/answer=None preserve existing values.
Returns True if updated, False if qa_id not found.
"""
try:
session_key = self._session_key(user_id, session_id)
entries = await self._load_entries(session_key)
idx = self._find_index_by_qa_id(entries, qa_id)
if idx is None:
return False
merged = self._merge_entry_update(
entries[idx],
question,
context,
answer,
feedback_text,
feedback_score,
used_graph_element_ids=used_graph_element_ids,
memify_metadata=memify_metadata,
used_session_context_ids=used_session_context_ids,
)
entries[idx] = self._validate_entry_dict(merged)
await self._write_entry_at(session_key, idx, entries[idx])
await self._apply_session_ttl(session_key)
return True
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while updating Q&A: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except SessionQAEntryValidationError:
raise
except Exception as e:
error_msg = f"Unexpected error while updating Q&A in Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def delete_feedback(self, user_id: str, session_id: str, qa_id: str) -> bool:
"""
Set feedback_text and feedback_score to None for a QA entry.
"""
try:
session_key = self._session_key(user_id, session_id)
entries = await self._load_entries(session_key)
idx = self._find_index_by_qa_id(entries, qa_id)
if idx is None:
return False
merged = self._merge_entry_clear_feedback(entries[idx])
entries[idx] = self._validate_entry_dict(merged)
await self._write_entry_at(session_key, idx, entries[idx])
await self._apply_session_ttl(session_key)
return True
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while clearing feedback: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except SessionQAEntryValidationError:
raise
except Exception as e:
error_msg = f"Unexpected error while clearing feedback: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def delete_qa_entry(self, user_id: str, session_id: str, qa_id: str) -> bool:
"""
Delete a single QA entry by qa_id.
Returns True if deleted, False if qa_id not found.
"""
try:
session_key = self._session_key(user_id, session_id)
entries = await self._load_entries(session_key)
idx = self._find_index_by_qa_id(entries, qa_id)
if idx is None:
return False
entries.pop(idx)
await self._rewrite_entries(session_key, entries)
if entries:
await self._apply_session_ttl(session_key)
return True
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while deleting Q&A: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while deleting Q&A from Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def delete_session(self, user_id: str, session_id: str) -> bool:
"""
Delete the entire session and all its session-scoped artifacts.
Returns True if any session data existed, False otherwise.
"""
try:
session_key = self._session_key(user_id, session_id)
trace_key = self._agent_trace_key(user_id, session_id)
context_key = self._session_context_key(user_id, session_id)
deleted_sessions = await self.async_redis.delete(session_key)
deleted_traces = await self.async_redis.delete(trace_key)
deleted_context = await self.async_redis.delete(context_key)
return (deleted_sessions + deleted_traces + deleted_context) > 0
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while deleting session: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while deleting session from Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def get_value(self, key: str) -> str | None:
"""Retrieve a raw string value stored under the given key, or None if absent."""
try:
value = await self.async_redis.get(key)
if isinstance(value, bytes):
return value.decode("utf-8")
return value
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while getting value: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while getting value from Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def set_value(self, key: str, value: str, ttl: int | None = None) -> None:
"""Store a raw string value under the given key, optionally expiring after ttl seconds."""
try:
await self.async_redis.set(key, value)
if ttl:
await self.async_redis.expire(key, ttl)
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while setting value: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while setting value in Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def delete_value(self, key: str) -> None:
"""Delete the value stored under the given key, if present."""
try:
await self.async_redis.delete(key)
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while deleting value: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while deleting value from Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def append_agent_trace_step(
self,
user_id: str,
session_id: str,
trace_id: str,
origin_function: str,
status: str,
memory_query: str = "",
memory_context: str = "",
method_params: dict | None = None,
method_return_value=None,
error_message: str = "",
session_feedback: str = "",
) -> None:
"""Append one trace step to the Redis list for this trace session."""
try:
trace_key = self._agent_trace_key(user_id, session_id)
trace_entry = self._build_agent_trace_entry_dump(
trace_id=trace_id,
origin_function=origin_function,
status=status,
memory_query=memory_query,
memory_context=memory_context,
method_params=method_params,
method_return_value=method_return_value,
error_message=error_message,
session_feedback=session_feedback,
)
await self.async_redis.rpush(trace_key, json.dumps(trace_entry))
await self._apply_session_ttl(trace_key)
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while appending agent trace step: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while appending agent trace step to Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def get_agent_trace_session(
self, user_id: str, session_id: str, last_n: int | None = None
) -> list[SessionAgentTraceEntry]:
"""Retrieve stored trace steps for the given session."""
trace_key = self._agent_trace_key(user_id, session_id)
if last_n is not None:
return [
SessionAgentTraceEntry(**entry)
for entry in await self._load_entries(trace_key, -last_n, -1)
]
return [SessionAgentTraceEntry(**entry) for entry in await self._load_entries(trace_key)]
async def get_agent_trace_feedback(
self, user_id: str, session_id: str, last_n: int | None = None
) -> list[str]:
"""Retrieve ordered per-step feedback for the given trace session."""
entries = await self.get_agent_trace_session(user_id, session_id, last_n=last_n)
return [entry.session_feedback for entry in entries]
async def get_agent_trace_count(self, user_id: str, session_id: str) -> int:
"""Return the number of stored trace steps for the given session."""
trace_key = self._agent_trace_key(user_id, session_id)
return await self.async_redis.llen(trace_key)
async def create_session_context_entry(
self, user_id: str, session_id: str, entry_dump: dict
) -> None:
"""Append one session-context entry to the Redis list for this session."""
try:
context_key = self._session_context_key(user_id, session_id)
await self.async_redis.rpush(context_key, json.dumps(entry_dump))
await self._apply_session_ttl(context_key)
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while adding session context: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while adding session context to Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def get_session_context_entries(self, user_id: str, session_id: str) -> list[dict]:
"""Retrieve all stored session-context entries for the given session."""
context_key = self._session_context_key(user_id, session_id)
return await self._load_entries(context_key)
async def update_session_context_entry(
self, user_id: str, session_id: str, entry_id: str, merge: dict
) -> bool:
"""Shallow-merge updates into the session-context entry matching entry["id"]."""
try:
context_key = self._session_context_key(user_id, session_id)
entries = await self._load_entries(context_key)
for i, entry in enumerate(entries):
if entry.get("id") == entry_id:
entries[i] = {**entry, **merge}
await self._write_entry_at(context_key, i, entries[i])
await self._apply_session_ttl(context_key)
return True
return False
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while updating session context: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while updating session context in Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def delete_session_context(self, user_id: str, session_id: str) -> bool:
"""Delete the entire session-context list for the given session."""
try:
context_key = self._session_context_key(user_id, session_id)
return (await self.async_redis.delete(context_key)) > 0
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while deleting session context: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while deleting session context from Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def prune(self) -> None:
"""
Flush the Redis database. In Cognee, prune means deleting the whole cache.
"""
try:
await self.async_redis.flushdb()
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while pruning: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while pruning Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def log_usage(
self,
user_id: str,
log_entry: dict,
ttl: int | None = 604800,
):
"""
Log usage information (API endpoint calls, MCP tool invocations) to Redis.
Args:
user_id: The user ID.
log_entry: Dictionary containing usage log information.
ttl: Optional time-to-live (seconds). If provided, the log list expires after this time.
Raises:
CacheConnectionError: If Redis connection fails or times out.
"""
try:
usage_logs_key = f"{self.log_key}:{user_id}"
await self.async_redis.rpush(usage_logs_key, json.dumps(log_entry))
if ttl is not None:
await self.async_redis.expire(usage_logs_key, ttl)
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while logging usage: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while logging usage to Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def get_usage_logs(self, user_id: str, limit: int = 100):
"""
Retrieve usage logs for a given user.
Args:
user_id: The user ID.
limit: Maximum number of logs to retrieve (default: 100).
Returns:
List of usage log entries, most recent first.
"""
try:
usage_logs_key = f"{self.log_key}:{user_id}"
entries = await self.async_redis.lrange(usage_logs_key, -limit, -1)
return [json.loads(e) for e in reversed(entries)] if entries else []
except (redis.ConnectionError, redis.TimeoutError) as e:
error_msg = f"Redis connection error while retrieving usage logs: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
except Exception as e:
error_msg = f"Unexpected error while retrieving usage logs from Redis: {str(e)}"
logger.error(error_msg)
raise CacheConnectionError(error_msg) from e
async def close(self):
"""Close Redis connections."""
try:
await self.async_redis.aclose()
except Exception as e:
logger.debug("Error closing Redis async connection: %s", e)