litellm - 💡(How to fix) Fix [Feature]: allow for better log filters

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

import logging import os import re import json from urllib.parse import urlparse from typing import Optional, Dict, Any

debug_filter = os.getenv("LITELLM_DEBUG_LOG_FILTER", "false").lower() == "true"

class EndpointFilter(logging.Filter): """ Generic filter to exclude logs based on request path. """ def init(self, excluded_paths=None): super().init() self.excluded_paths = set(excluded_paths or [])

def filter(self, record):
    if debug_filter:
        print(f"DEBUG_ENDPOINT_FILTER: Filtering log record with args: {getattr(record, 'args', None)}")
    # We only care about filtering Uvicorn access logs
    if record.name != "uvicorn.access":
        return True
    
    # Access log record.args structure: (addr, method, path, version, status)
    if record.args and len(record.args) >= 3:
        path = urlparse(record.args[2]).path
        # Check if the path is in our excluded list
        if path in self.excluded_paths:
            return False
    return True

class JSONFilter(logging.Filter): def init(self, filters=None): super().init() # filters is a list of dicts: # [ # { # "field": "component", # "value": "LiteLLM Proxy", # # matches are optional # "matches": [ # { # "field": "message", # "pattern": "Synced \d+ production policies .*" # } # ] # }, # ] self.filters = filters or [] for f in self.filters: for match in f.get("matches", []): match["pattern_compiled"] = re.compile(match["pattern"], re.DOTALL)

def filter(self, record):
    for f in self.filters:
        # 1. Resolve the field value. LiteLLM uses 'name' on the record,
        # but the JSON formatter maps this to 'component' in the output.
        # We check both to be safe.
        field_name = f.get("field")
        record_val = getattr(record, field_name, None)
        if field_name == "component" and record_val is None:
            record_val = record.name

        if debug_filter:
            print(f"DEBUG_JSON_FILTER: Checking {record_val} against {f.get('value')}")

        # 2. Check if the resolved value matches the primary filter criteria
        if record_val == f.get("value"):
            if debug_filter:
                print(f"DEBUG_JSON_FILTER: Primary match for {field_name}={record_val}")
            # 3. If there are no extra matches, filter out the log
            if not f.get("matches"):
                if debug_filter:
                    print("DEBUG_JSON_FILTER: No secondary matches specified, filtering out log.")
                return False
            
            # Check all extra matches (AND logic)
            all_match = True
            for match in f.get("matches", []):
                field_to_match = match.get("field")
                # Get the field value from the record (e.g., record.message)
                field_val = getattr(record, field_to_match, "")
                if field_to_match == "message":
                    field_val = record.getMessage()

                if debug_filter:
                    print(f"DEBUG_JSON_FILTER: Attempting to match secondary field '{field_to_match}' with value '{field_val}' against pattern '{match.get('pattern')}'")
                # Get the field value from the record (e.g., record.message)
                # Ensure it's a string for regex
                if not match["pattern_compiled"].search(str(field_val)):
                    all_match = False
                    break
                if debug_filter:
                    print(f"DEBUG_JSON_FILTER: Secondary match for '{field_to_match}' with value '{field_val}' against pattern '{match.get('pattern')}' SUCCEEDED.")

            if all_match:
                if debug_filter:
                    print("DEBUG_JSON_FILTER: All secondary matches passed, filtering out log.")
                return False
    return True

============================================================================

Auto-register filters at logger level to survive dictConfig reapplies

============================================================================

When json_logs: true, LiteLLM calls logging.config.dictConfig() which can

replace handler-level filters. Logger-level filters persist across reapplies.

def _load_filter_config() -> Dict[str, Any]: """Load filter definitions from log_config.yaml or log_config.json""" config_paths = [ os.getenv("LITELLM_LOG_CONFIG", "/etc/litellm/log_config.json"), "./log_config.json", ]

for config_path in config_paths:
    if not os.path.exists(config_path):
        if debug_filter:
            print(f"DEBUG: Log config not found at {config_path}")
        continue        
    try:
        if config_path.endswith(".json"):
            with open(config_path, "r") as f:
                config = json.load(f)
                return config.get("filters", {})
    except Exception as e:
        if debug_filter:
            print(f"DEBUG: Failed to load {config_path}: {e}")
        continue

if debug_filter:
    print("DEBUG: No log config found, using empty filter config")
return {}

_filter_config = _load_filter_config()

def _extract_json_filter_config() -> Optional[Dict[str, Any]]: """Extract JSONFilter config from filters section""" default_filter = _filter_config.get("default_json_filter", {}) if "filters" in default_filter: return default_filter return None

Auto-register JSONFilter

_json_config = _extract_json_filter_config() if _json_config: _json_filter = JSONFilter(filters=_json_config.get("filters", []))

# LiteLLM subsystem loggers disable propagation, so we must add the filter
# directly to each logger rather than relying on the root logger.
target_loggers = ["LiteLLM Proxy", "LiteLLM Router", "LiteLLM", "uvicorn", "uvicorn.access"]
for name in target_loggers:
    logging.getLogger(name).addFilter(_json_filter)

if debug_filter:
    print(f"DEBUG: Registered JSONFilter with {len(_json_config.get('filters', []))} filter rules")
    print(f"DEBUG: JSONFilter rules: {_json_config.get('filters', [])}")

else: if debug_filter: print("DEBUG: No JSONFilter config found")

Fix Action

Fix / Workaround

Better control about logs. Info is currently pretty noisy, the solution is to upgrade to warnings but that removes a lot of telemetry.

Code Example

# ============================================================================
# Auto-register filters at logger level to survive dictConfig reapplies
# ============================================================================

---

import logging
import os
import re
import json
from urllib.parse import urlparse
from typing import Optional, Dict, Any

debug_filter = os.getenv("LITELLM_DEBUG_LOG_FILTER", "false").lower() == "true"

class EndpointFilter(logging.Filter):
    """
    Generic filter to exclude logs based on request path.
    """
    def __init__(self, excluded_paths=None):
        super().__init__()
        self.excluded_paths = set(excluded_paths or [])

    def filter(self, record):
        if debug_filter:
            print(f"DEBUG_ENDPOINT_FILTER: Filtering log record with args: {getattr(record, 'args', None)}")
        # We only care about filtering Uvicorn access logs
        if record.name != "uvicorn.access":
            return True
        
        # Access log record.args structure: (addr, method, path, version, status)
        if record.args and len(record.args) >= 3:
            path = urlparse(record.args[2]).path
            # Check if the path is in our excluded list
            if path in self.excluded_paths:
                return False
        return True

class JSONFilter(logging.Filter):
    def __init__(self, filters=None):
        super().__init__()
        # filters is a list of dicts: 
        # [
        #   {
        #     "field": "component", 
        #     "value": "LiteLLM Proxy", 
        #     # matches are optional
        #     "matches": [
        #         {
        #            "field": "message",
        #            "pattern": "Synced \\d+ production policies .*"
        #         }
        #      ]
        #   }, 
        # ]
        self.filters = filters or []
        for f in self.filters:
            for match in f.get("matches", []):
                match["pattern_compiled"] = re.compile(match["pattern"], re.DOTALL)


    def filter(self, record):
        for f in self.filters:
            # 1. Resolve the field value. LiteLLM uses 'name' on the record,
            # but the JSON formatter maps this to 'component' in the output.
            # We check both to be safe.
            field_name = f.get("field")
            record_val = getattr(record, field_name, None)
            if field_name == "component" and record_val is None:
                record_val = record.name

            if debug_filter:
                print(f"DEBUG_JSON_FILTER: Checking {record_val} against {f.get('value')}")

            # 2. Check if the resolved value matches the primary filter criteria
            if record_val == f.get("value"):
                if debug_filter:
                    print(f"DEBUG_JSON_FILTER: Primary match for {field_name}={record_val}")
                # 3. If there are no extra matches, filter out the log
                if not f.get("matches"):
                    if debug_filter:
                        print("DEBUG_JSON_FILTER: No secondary matches specified, filtering out log.")
                    return False
                
                # Check all extra matches (AND logic)
                all_match = True
                for match in f.get("matches", []):
                    field_to_match = match.get("field")
                    # Get the field value from the record (e.g., record.message)
                    field_val = getattr(record, field_to_match, "")
                    if field_to_match == "message":
                        field_val = record.getMessage()

                    if debug_filter:
                        print(f"DEBUG_JSON_FILTER: Attempting to match secondary field '{field_to_match}' with value '{field_val}' against pattern '{match.get('pattern')}'")
                    # Get the field value from the record (e.g., record.message)
                    # Ensure it's a string for regex
                    if not match["pattern_compiled"].search(str(field_val)):
                        all_match = False
                        break
                    if debug_filter:
                        print(f"DEBUG_JSON_FILTER: Secondary match for '{field_to_match}' with value '{field_val}' against pattern '{match.get('pattern')}' SUCCEEDED.")

                if all_match:
                    if debug_filter:
                        print("DEBUG_JSON_FILTER: All secondary matches passed, filtering out log.")
                    return False
        return True


# ============================================================================
# Auto-register filters at logger level to survive dictConfig reapplies
# ============================================================================
# When json_logs: true, LiteLLM calls logging.config.dictConfig() which can
# replace handler-level filters. Logger-level filters persist across reapplies.

def _load_filter_config() -> Dict[str, Any]:
    """Load filter definitions from log_config.yaml or log_config.json"""
    config_paths = [
        os.getenv("LITELLM_LOG_CONFIG", "/etc/litellm/log_config.json"),
        "./log_config.json",
    ]
    
    for config_path in config_paths:
        if not os.path.exists(config_path):
            if debug_filter:
                print(f"DEBUG: Log config not found at {config_path}")
            continue        
        try:
            if config_path.endswith(".json"):
                with open(config_path, "r") as f:
                    config = json.load(f)
                    return config.get("filters", {})
        except Exception as e:
            if debug_filter:
                print(f"DEBUG: Failed to load {config_path}: {e}")
            continue
    
    if debug_filter:
        print("DEBUG: No log config found, using empty filter config")
    return {}


_filter_config = _load_filter_config()

def _extract_json_filter_config() -> Optional[Dict[str, Any]]:
    """Extract JSONFilter config from filters section"""
    default_filter = _filter_config.get("default_json_filter", {})
    if "filters" in default_filter:
        return default_filter
    return None

# Auto-register JSONFilter
_json_config = _extract_json_filter_config()
if _json_config:
    _json_filter = JSONFilter(filters=_json_config.get("filters", []))
    
    # LiteLLM subsystem loggers disable propagation, so we must add the filter
    # directly to each logger rather than relying on the root logger.
    target_loggers = ["LiteLLM Proxy", "LiteLLM Router", "LiteLLM", "uvicorn", "uvicorn.access"]
    for name in target_loggers:
        logging.getLogger(name).addFilter(_json_filter)

    if debug_filter:
        print(f"DEBUG: Registered JSONFilter with {len(_json_config.get('filters', []))} filter rules")
        print(f"DEBUG: JSONFilter rules: {_json_config.get('filters', [])}")
else:
    if debug_filter:
        print("DEBUG: No JSONFilter config found")

---

{
    "version": 1,
    "disable_existing_loggers": false,
    "filters": {
        "default_json_filter": {
            "()": "custom_log_filters.JSONFilter",
            "filters": [
                {
                    "field": "component",
                    "value": "LiteLLM"
                },
                {
                    "field": "component",
                    "value": "LiteLLM Proxy",
                    "matches": [
                        {
                            "field": "message",
                            "pattern": "Synced \\d+ production policies .*"
                        }
                    ]
                }
            ]
        },
        "access_filter": {
            "()": "custom_log_filters.EndpointFilter",
            "excluded_paths": [
                "/health/readiness",
                "/health/liveliness"
            ]
        }
    },
    "formatters": {
        "json": {
            "()": "litellm._logging.JsonFormatter"
        },
        "default": {
            "()": "litellm._logging.JsonFormatter"
        },
        "access": {
            "()": "litellm._logging.JsonFormatter"
        }
    },
    "handlers": {
        "default": {
            "formatter": "json",
            "class": "logging.StreamHandler",
            "filters": [
                "default_json_filter"
            ],
            "stream": "ext://sys.stdout"
        },
        "access": {
            "formatter": "access",
            "class": "logging.StreamHandler",
            "filters": [
                "access_filter"
            ],
            "stream": "ext://sys.stdout"
        }
    },
    "loggers": {
        "uvicorn": {
            "handlers": [
                "default"
            ],
            "level": "INFO",
            "propagate": false
        },
        "uvicorn.error": {
            "handlers": [
                "default"
            ],
            "level": "INFO",
            "propagate": false
        },
        "uvicorn.access": {
            "handlers": [
                "access"
            ],
            "level": "INFO",
            "propagate": false
        }
    }
}
RAW_BUFFERClick to expand / collapse

Check for existing issues

  • I have searched the existing issues and checked that my issue is not a duplicate.

The Feature

referencing #18992

Allow to filter out endpoints (noisy health checks) and various json logs (info logs are pretty noisy).

Here are 2 logging filters that can be thrown into /app/custom_log_filters.py in the docker container. They use the standard logging config format.

Would be nice to make them first class citizens in the proxy.

Since the JSON logger deletes existing loggers the filter below need to re-inject themselves.

If that would not be the case everything below following comment can be removed.

# ============================================================================
# Auto-register filters at logger level to survive dictConfig reapplies
# ============================================================================

I suspect the issue lies within: https://github.com/BerriAI/litellm/blob/e59e34bed3670a6894d43129c2af16af28057d03/litellm/_logging.py#L327

So curenrtly i load it via --log_config and an additional env value LITELLM_LOG_CONFIG, for the re-injection to find the config.

Could be unified if that the filters are embedded.

I had to add a extensive amount of debugging via LITELLM_DEBUG_LOG_FILTER, can probably also be dialed down if the aggressive injection is not necessary.

import logging
import os
import re
import json
from urllib.parse import urlparse
from typing import Optional, Dict, Any

debug_filter = os.getenv("LITELLM_DEBUG_LOG_FILTER", "false").lower() == "true"

class EndpointFilter(logging.Filter):
    """
    Generic filter to exclude logs based on request path.
    """
    def __init__(self, excluded_paths=None):
        super().__init__()
        self.excluded_paths = set(excluded_paths or [])

    def filter(self, record):
        if debug_filter:
            print(f"DEBUG_ENDPOINT_FILTER: Filtering log record with args: {getattr(record, 'args', None)}")
        # We only care about filtering Uvicorn access logs
        if record.name != "uvicorn.access":
            return True
        
        # Access log record.args structure: (addr, method, path, version, status)
        if record.args and len(record.args) >= 3:
            path = urlparse(record.args[2]).path
            # Check if the path is in our excluded list
            if path in self.excluded_paths:
                return False
        return True

class JSONFilter(logging.Filter):
    def __init__(self, filters=None):
        super().__init__()
        # filters is a list of dicts: 
        # [
        #   {
        #     "field": "component", 
        #     "value": "LiteLLM Proxy", 
        #     # matches are optional
        #     "matches": [
        #         {
        #            "field": "message",
        #            "pattern": "Synced \\d+ production policies .*"
        #         }
        #      ]
        #   }, 
        # ]
        self.filters = filters or []
        for f in self.filters:
            for match in f.get("matches", []):
                match["pattern_compiled"] = re.compile(match["pattern"], re.DOTALL)


    def filter(self, record):
        for f in self.filters:
            # 1. Resolve the field value. LiteLLM uses 'name' on the record,
            # but the JSON formatter maps this to 'component' in the output.
            # We check both to be safe.
            field_name = f.get("field")
            record_val = getattr(record, field_name, None)
            if field_name == "component" and record_val is None:
                record_val = record.name

            if debug_filter:
                print(f"DEBUG_JSON_FILTER: Checking {record_val} against {f.get('value')}")

            # 2. Check if the resolved value matches the primary filter criteria
            if record_val == f.get("value"):
                if debug_filter:
                    print(f"DEBUG_JSON_FILTER: Primary match for {field_name}={record_val}")
                # 3. If there are no extra matches, filter out the log
                if not f.get("matches"):
                    if debug_filter:
                        print("DEBUG_JSON_FILTER: No secondary matches specified, filtering out log.")
                    return False
                
                # Check all extra matches (AND logic)
                all_match = True
                for match in f.get("matches", []):
                    field_to_match = match.get("field")
                    # Get the field value from the record (e.g., record.message)
                    field_val = getattr(record, field_to_match, "")
                    if field_to_match == "message":
                        field_val = record.getMessage()

                    if debug_filter:
                        print(f"DEBUG_JSON_FILTER: Attempting to match secondary field '{field_to_match}' with value '{field_val}' against pattern '{match.get('pattern')}'")
                    # Get the field value from the record (e.g., record.message)
                    # Ensure it's a string for regex
                    if not match["pattern_compiled"].search(str(field_val)):
                        all_match = False
                        break
                    if debug_filter:
                        print(f"DEBUG_JSON_FILTER: Secondary match for '{field_to_match}' with value '{field_val}' against pattern '{match.get('pattern')}' SUCCEEDED.")

                if all_match:
                    if debug_filter:
                        print("DEBUG_JSON_FILTER: All secondary matches passed, filtering out log.")
                    return False
        return True


# ============================================================================
# Auto-register filters at logger level to survive dictConfig reapplies
# ============================================================================
# When json_logs: true, LiteLLM calls logging.config.dictConfig() which can
# replace handler-level filters. Logger-level filters persist across reapplies.

def _load_filter_config() -> Dict[str, Any]:
    """Load filter definitions from log_config.yaml or log_config.json"""
    config_paths = [
        os.getenv("LITELLM_LOG_CONFIG", "/etc/litellm/log_config.json"),
        "./log_config.json",
    ]
    
    for config_path in config_paths:
        if not os.path.exists(config_path):
            if debug_filter:
                print(f"DEBUG: Log config not found at {config_path}")
            continue        
        try:
            if config_path.endswith(".json"):
                with open(config_path, "r") as f:
                    config = json.load(f)
                    return config.get("filters", {})
        except Exception as e:
            if debug_filter:
                print(f"DEBUG: Failed to load {config_path}: {e}")
            continue
    
    if debug_filter:
        print("DEBUG: No log config found, using empty filter config")
    return {}


_filter_config = _load_filter_config()

def _extract_json_filter_config() -> Optional[Dict[str, Any]]:
    """Extract JSONFilter config from filters section"""
    default_filter = _filter_config.get("default_json_filter", {})
    if "filters" in default_filter:
        return default_filter
    return None

# Auto-register JSONFilter
_json_config = _extract_json_filter_config()
if _json_config:
    _json_filter = JSONFilter(filters=_json_config.get("filters", []))
    
    # LiteLLM subsystem loggers disable propagation, so we must add the filter
    # directly to each logger rather than relying on the root logger.
    target_loggers = ["LiteLLM Proxy", "LiteLLM Router", "LiteLLM", "uvicorn", "uvicorn.access"]
    for name in target_loggers:
        logging.getLogger(name).addFilter(_json_filter)

    if debug_filter:
        print(f"DEBUG: Registered JSONFilter with {len(_json_config.get('filters', []))} filter rules")
        print(f"DEBUG: JSONFilter rules: {_json_config.get('filters', [])}")
else:
    if debug_filter:
        print("DEBUG: No JSONFilter config found")

example config:

{
    "version": 1,
    "disable_existing_loggers": false,
    "filters": {
        "default_json_filter": {
            "()": "custom_log_filters.JSONFilter",
            "filters": [
                {
                    "field": "component",
                    "value": "LiteLLM"
                },
                {
                    "field": "component",
                    "value": "LiteLLM Proxy",
                    "matches": [
                        {
                            "field": "message",
                            "pattern": "Synced \\d+ production policies .*"
                        }
                    ]
                }
            ]
        },
        "access_filter": {
            "()": "custom_log_filters.EndpointFilter",
            "excluded_paths": [
                "/health/readiness",
                "/health/liveliness"
            ]
        }
    },
    "formatters": {
        "json": {
            "()": "litellm._logging.JsonFormatter"
        },
        "default": {
            "()": "litellm._logging.JsonFormatter"
        },
        "access": {
            "()": "litellm._logging.JsonFormatter"
        }
    },
    "handlers": {
        "default": {
            "formatter": "json",
            "class": "logging.StreamHandler",
            "filters": [
                "default_json_filter"
            ],
            "stream": "ext://sys.stdout"
        },
        "access": {
            "formatter": "access",
            "class": "logging.StreamHandler",
            "filters": [
                "access_filter"
            ],
            "stream": "ext://sys.stdout"
        }
    },
    "loggers": {
        "uvicorn": {
            "handlers": [
                "default"
            ],
            "level": "INFO",
            "propagate": false
        },
        "uvicorn.error": {
            "handlers": [
                "default"
            ],
            "level": "INFO",
            "propagate": false
        },
        "uvicorn.access": {
            "handlers": [
                "access"
            ],
            "level": "INFO",
            "propagate": false
        }
    }
}

Motivation, pitch

Better control about logs. Info is currently pretty noisy, the solution is to upgrade to warnings but that removes a lot of telemetry.

It is really helpful to be able to just match noisy log events that have no additional value on demand.

What part of LiteLLM is this about?

Proxy

LiteLLM is hiring a founding backend engineer, are you interested in joining us and shipping to all our users?

No

Twitter / LinkedIn details

No response

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

litellm - 💡(How to fix) Fix [Feature]: allow for better log filters