Conversation
Audit logs (CRUD events on keys, teams, users, models) were only stored in
the Prisma DB. This adds a pluggable callback system so audit logs can be
forwarded to external services like S3 for ingestion into security monitoring
tools.
New config key `audit_log_callbacks` under `litellm_settings` reuses the
existing callback infrastructure. Any CustomLogger subclass can opt in by
overriding `async_log_audit_log_event()`. S3Logger (s3_v2) is implemented
as the first handler, storing audit logs under `audit_logs/{date}/` prefix.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two fixes based on PR feedback: 1. Move callback dispatch before the prisma_client check so audit logs still reach S3/Datadog even if the DB is down. Also changed the prisma_client=None case from raising an exception to logging an error and returning gracefully. 2. Attach a done_callback to asyncio tasks created for audit log callbacks so exceptions are logged through verbose_proxy_logger instead of silently swallowed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add a CopyOutlined icon next to the truncated User ID that copies the full UUID to clipboard on click. Follows the existing pattern used in model_hub_table_columns.tsx.
Made-with: Cursor
[Feature] Add Audit Log Export to External Callbacks
The create key form used getPredefinedTags() which only extracted tags from existing keys' metadata. If no keys had tags, the dropdown was empty. Switch to the existing useTags() React Query hook that fetches from /tag/list, matching the edit key form behavior.
Made-with: Cursor
…opdown Litellm fix create key tags dropdown
…ict dumps Add key-name-based regex patterns (master_key, database_url, auth_token, etc.) to SecretRedactionFilter so secrets embedded in dict/config dumps are redacted by key name, regardless of value format. Fixes a leak where general_settings containing master_key and database_url was logged in full because the secret values didn't match any existing value-format regex pattern.
…lobal fix: global secret redaction via root logger + key-name-based pattern matching
polish: add click-to-copy icon on User ID in internal users table
Hide the red asterisk indicators on Username and Password fields while keeping the validation rules intact.
polish: remove required asterisks from v3 login form fields
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis is a semi-daily integration branch bundling several features and polish fixes: a new High Availability Control Plane architecture (docs + React diagram component), Audit Log Export to External Callbacks (dispatching audit events to Key changes:
Confidence Score: 3/5
|
| Filename | Overview |
|---|---|
| litellm/proxy/management_helpers/audit_logs.py | Core audit log callback dispatch logic added; callable-type callbacks in CALLBACK_TYPES are silently dropped without any warning log, and DB write now happens after callback dispatch (intentionally). |
| litellm/integrations/s3_v2.py | Adds async_log_audit_log_event to S3Logger, correctly batching audit logs using the existing log_queue and s3BatchLoggingElement infrastructure. |
| litellm/proxy/proxy_server.py | Wires audit_log_callbacks config key into the proxy config loader; correctly warns when store_audit_logs is not set. |
| litellm/_logging.py | Adds key-name-based secret redaction patterns for common sensitive config keys like master_key, database_url, access_token, etc. |
| ui/litellm-dashboard/src/components/organisms/create_key_button.tsx | Replaces stale predefinedTags state with live useTags hook; Object.values() usage is correct since TagListResponse is Record<string, Tag>. |
| ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx | Adds useTags mock and tags dropdown test, but the mock uses an array shape instead of the correct Record<string, Tag> shape defined by TagListResponse. |
Sequence Diagram
sequenceDiagram
participant PS as proxy_server.py
participant AL as audit_logs.py
participant CB as _dispatch_audit_log_to_callbacks
participant S3 as S3Logger
participant CL as CustomLogger (subclass)
participant DB as Prisma DB
PS->>AL: create_audit_log_for_update(request_data)
AL->>AL: check store_audit_logs & premium_user
AL->>AL: serialize updated_values/before_value to JSON
AL->>CB: await _dispatch_audit_log_to_callbacks(request_data)
CB->>CB: _build_audit_log_payload(request_data)
loop for each callback in audit_log_callbacks
alt callback is CustomLogger instance
CB-->>CL: asyncio.create_task(async_log_audit_log_event(payload))
else callback is str (e.g. "s3_v2")
CB->>CB: _resolve_audit_log_callback(name) [cached]
CB-->>S3: asyncio.create_task(async_log_audit_log_event(payload))
else callback is Callable
CB->>CB: silently skipped (no warning logged)
end
end
AL->>DB: prisma_client.db.litellm_auditlog.create(...)
S3-->>S3: append to log_queue, flush if batch_size reached
Comments Outside Diff (1)
-
ui/litellm-dashboard/src/components/organisms/create_key_button.test.tsx, line 1653-1661 (link)Test mock uses wrong shape for
TagListResponseTagListResponseis defined asRecord<string, Tag>(a keyed object), not an array. The mock below returns an array, which accidentally works at runtime becauseObject.values([...])on an array just returns the same elements — but the mock does not accurately represent the real data contract.If the component code changes to access the data by key (e.g.,
tagsData["production"]) rather than usingObject.values(), this test will silently pass with the wrong mock shape while production breaks.The mock should match the real
TagListResponseshape:
Last reviewed commit: "fix: resolve mypy ty..."
| _audit_log_callback_cache: Dict[str, CustomLogger] = {} | ||
|
|
||
|
|
||
| def _resolve_audit_log_callback(name: str) -> Optional[CustomLogger]: | ||
| """Resolve a string callback name to a CustomLogger instance, with caching.""" | ||
| if name in _audit_log_callback_cache: | ||
| return _audit_log_callback_cache[name] | ||
|
|
||
| from litellm.litellm_core_utils.litellm_logging import ( | ||
| _init_custom_logger_compatible_class, | ||
| ) | ||
|
|
||
| instance = _init_custom_logger_compatible_class( | ||
| logging_integration=name, # type: ignore | ||
| internal_usage_cache=None, | ||
| llm_router=None, | ||
| ) | ||
|
|
||
| if instance is not None: | ||
| _audit_log_callback_cache[name] = instance | ||
| return instance |
There was a problem hiding this comment.
Stale callback cache on config reload
_audit_log_callback_cache is a module-level dict that is never cleared. In proxy_server.py, when audit_log_callbacks is re-configured (e.g., on a hot-reload), litellm.audit_log_callbacks is reset to [] and rebuilt — but _audit_log_callback_cache is not invalidated. This means any string-named callback like "s3_v2" will continue resolving to the old cached instance (with stale bucket name, credentials, etc.) after a config reload.
To fix, add a cache-clearing step whenever litellm.audit_log_callbacks is reconfigured. One approach is to expose a _clear_audit_log_callback_cache() helper and call it from proxy_server.py:
def _clear_audit_log_callback_cache() -> None:
"""Invalidate the resolved-callback cache (call on config reload)."""
_audit_log_callback_cache.clear()And in proxy_server.py, before rebuilding audit_log_callbacks:
elif key == "audit_log_callbacks":
from litellm.proxy.management_helpers.audit_logs import _clear_audit_log_callback_cache
_clear_audit_log_callback_cache()
litellm.audit_log_callbacks = []
...| else: | ||
| verbose_proxy_logger.warning( | ||
| "'audit_log_callbacks' is configured but 'store_audit_logs' is not enabled. " | ||
| "Audit log callbacks will not fire until 'store_audit_logs: true' is added to litellm_settings." | ||
| ) |
There was a problem hiding this comment.
Warning message omits the
LITELLM_STORE_AUDIT_LOGS env var alternative
create_audit_log_for_update checks litellm.store_audit_logs or get_secret_bool("LITELLM_STORE_AUDIT_LOGS"), so users can enable audit logs via environment variable without touching litellm_settings. The current warning message only mentions store_audit_logs: true, which could cause confusion for users who rely on the env-var approach.
| else: | |
| verbose_proxy_logger.warning( | |
| "'audit_log_callbacks' is configured but 'store_audit_logs' is not enabled. " | |
| "Audit log callbacks will not fire until 'store_audit_logs: true' is added to litellm_settings." | |
| ) | |
| verbose_proxy_logger.warning( | |
| "'audit_log_callbacks' is configured but 'store_audit_logs' is not enabled. " | |
| "Audit log callbacks will not fire until 'store_audit_logs: true' is added to litellm_settings " | |
| "or the LITELLM_STORE_AUDIT_LOGS environment variable is set to 'true'." | |
| ) |
Extract multiline `or` chain from LiteLLM_AuditLogs constructor to fix pydantic mypy plugin field-type misattribution, and add explicit Optional[bool] annotation to avoid variable name shadowing conflict.
| r"(?:master_key|database_url|db_url|connection_string|" | ||
| r"private_key|signing_key|encryption_key|" | ||
| r"auth_token|access_token|refresh_token|" | ||
| r"slack_webhook_url|webhook_url|" | ||
| r"database_connection_string|" | ||
| r"huggingface_token|jwt_secret)" | ||
| r"""['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+""", |
There was a problem hiding this comment.
Missing word boundaries — key-name patterns can over-redact
The key-name patterns (e.g. access_token, refresh_token, webhook_url) are not anchored with word boundaries or negative lookbehinds. This means a key such as oauth_access_token, last_access_token, or my_webhook_url would produce a partial match, e.g.:
"last_access_token": "my-value"
# becomes → "last_REDACTED"
The regex matches access_token" (consuming the trailing " of the key name), then : ", then my-value — leaving a dangling "last_ prefix and a broken closing " in the log line.
Consider adding a word-boundary or negative lookbehind to anchor each key name:
r"(?<!['\"\w])(?:master_key|database_url|db_url|connection_string|"
r"private_key|signing_key|encryption_key|"
r"auth_token|access_token|refresh_token|"
r"slack_webhook_url|webhook_url|"
r"database_connection_string|"
r"huggingface_token|jwt_secret)"
r"""['\"]?\s*[:=]\s*['\"]?[^\s,'\"})\]{}>]+""",Note: the existing api[_-]?key pattern already sidesteps this by requiring {8,} characters for the value. The new patterns have no such minimum length guard either.
New docs page covering the HA control plane architecture where each worker instance has its own DB, Redis, and master key. Includes a React component diagram, setup configs, SSO notes, and local testing instructions.
| for callback in litellm.audit_log_callbacks: | ||
| try: | ||
| resolved: Optional[CustomLogger] = callback if isinstance(callback, CustomLogger) else None | ||
| if isinstance(callback, str): | ||
| resolved = _resolve_audit_log_callback(callback) | ||
| if resolved is None: | ||
| verbose_proxy_logger.warning( | ||
| "Could not resolve audit log callback: %s", callback | ||
| ) | ||
| continue | ||
|
|
||
| if isinstance(resolved, CustomLogger): | ||
| task = asyncio.create_task(resolved.async_log_audit_log_event(payload)) | ||
| task.add_done_callback(_audit_log_task_done_callback) | ||
| except Exception as e: | ||
| verbose_proxy_logger.error( | ||
| "Failed dispatching audit log to callback: %s", e | ||
| ) |
There was a problem hiding this comment.
Callable callbacks silently dropped without warning
CALLBACK_TYPES is defined as Union[str, Callable, "CustomLogger"] (see litellm/__init__.py), meaning a user can legally add a plain callable to audit_log_callbacks. However, _dispatch_audit_log_to_callbacks only handles CustomLogger instances and strings — if a callback is a Callable, resolved stays None and the entry is silently skipped with no log message whatsoever.
This is a silent failure: a user who registers a callable expecting it to be invoked will see no error and no invocation. At minimum, an else branch with a warning is needed:
for callback in litellm.audit_log_callbacks:
try:
resolved: Optional[CustomLogger] = callback if isinstance(callback, CustomLogger) else None
if isinstance(callback, str):
resolved = _resolve_audit_log_callback(callback)
if resolved is None:
verbose_proxy_logger.warning(
"Could not resolve audit log callback: %s", callback
)
continue
elif not isinstance(callback, CustomLogger):
verbose_proxy_logger.warning(
"Unsupported audit_log_callback type %s (%r) — only CustomLogger instances "
"and string names are supported.",
type(callback).__name__,
callback,
)
continue
if isinstance(resolved, CustomLogger):
task = asyncio.create_task(resolved.async_log_audit_log_event(payload))
task.add_done_callback(_audit_log_task_done_callback)
except Exception as e:
...
ryans branch
circle ci: https://app.circleci.com/pipelines/github/BerriAI/litellm?branch=litellm_ryan_march_20
feat: add control plane for multi-proxy worker management
feature Add Audit Log Export to External Callbacks
refactor: internal users page destructive action modal
fix: tags not displaying after creation
fix: followup secrets leaking in detailed debug
polish: add click-to-copy icon on User ID in internal users table
polish: remove required asterisks from v3 login form fields