Skip to main content

Small logged cachetools wrappers with cache managers, stats, and decorators.

Project description

logged-cache

logged-cache is a small Python package around cachetools for applications that want cache hit/miss logs without rewriting their caching code.

It provides:

  • LoggedCache, a mutable mapping wrapper that logs cache activity.
  • LRUCacheManager and TTLCacheManager, with a shared lock for cachetools.cached.
  • CacheStats, lightweight counters for hits, misses, writes, deletes, and clears.
  • cached and cachedmethod, convenience decorators for functions and methods.
  • Synchronous and asynchronous function caching with the same decorator.
  • Hooks and key formatters for metrics, redaction, and structured logging.

Installation

pip install logged-cache

For local development with uv:

uv sync --extra dev --extra docs
uv run pytest

If you do not use uv, the project is still standard PEP 621 Python packaging:

python -m venv .venv
. .venv/bin/activate
python -m pip install -e ".[dev,docs]"
pytest

Quick Start

import logging

from logged_cache import (
    LRUCacheManager,
    TTLCacheManager,
    cached,
    cachedmethod,
)

logger = logging.getLogger("myapp.cache")

# Use LRU when you want a bounded cache.
users = LRUCacheManager(maxsize=256, logger=logger, name="users")


@cached(users)
def load_user(user_id: int) -> dict[str, int | str]:
    return {"id": user_id, "name": "Ada"}


# Use TTL when values should expire.
tokens = TTLCacheManager(maxsize=1000, ttl=300, logger=logger, name="tokens")


@cached(tokens)
def fetch_token(account_id: str) -> str:
    return f"token-for-{account_id}"


# Use cachedmethod for instance or class methods.
class ProfileService:
    cache = LRUCacheManager(maxsize=256, logger=logger, name="profiles")

    @cachedmethod(cache)
    def load_profile(self, user_id: int) -> dict[str, int | str]:
        return {"id": user_id, "name": "Ada"}


# The same decorators work with async functions and methods.
@cached(users)
async def load_user_async(user_id: int) -> dict[str, int | str]:
    return {"id": user_id, "name": "Ada"}


load_user(1)  # miss, then set.
load_user(1)  # hit.
fetch_token("main")  # TTL miss, then set.
ProfileService().load_profile(1)  # method cache.

print(users.stats.hits, users.stats.misses, users.stats.hit_rate)

With a standard logging formatter, cache events look like this:

DEBUG:myapp.cache:[users] Cache miss: function=load_user key=(1,)
DEBUG:myapp.cache:[users] Cache set: function=load_user key=(1,)
DEBUG:myapp.cache:[users] Cache hit: function=load_user key=(1,)

If logger is not provided, logging.getLogger() is used. Each log record also contains cache_name, cache_function, cache_event, and cache_key attributes for structured logging. When a cache is used through cached, cachedmethod, async_cached, or async_cachedmethod, log messages also show the function or method name.

If name is omitted, decorators set a readable fallback name from the decorated callable, such as myapp.services.UserService.load_user.

Keys are formatted defensively before they are written to logs. Very large keys are truncated, and plain object instances are shown as readable class names instead of memory-address-heavy default representations:

DEBUG:myapp.cache:[users] Cache miss: key=<myapp.queries.UserQuery>
DEBUG:myapp.cache:[users] Cache miss: key=('xxxxxxxxxxxxxxxxxxxx...<truncated>)

LRU Cache

Use LRUCacheManager when you want a fixed-size cache where the least recently used entries are evicted first.

import logging

from logged_cache import LRUCacheManager, cached

logger = logging.getLogger("myapp.cache")
users = LRUCacheManager(maxsize=256, logger=logger, name="users")


@cached(users)
def load_user(user_id: int) -> dict[str, int | str]:
    return {"id": user_id, "name": "Ada"}


load_user(1)
load_user(1)

print(users.stats.hits, users.stats.misses, users.stats.hit_rate)

TTL Cache

from logged_cache import TTLCacheManager, cached

tokens = TTLCacheManager(maxsize=1000, ttl=300)


@cached(tokens)
def fetch_token(account_id: str) -> str:
    return f"token-for-{account_id}"

Async Functions

Use the same cached decorator for async def. The awaited result is cached, not the coroutine object.

from logged_cache import LRUCacheManager, cached

profiles = LRUCacheManager(maxsize=256)


@cached(profiles)
async def fetch_profile(user_id: int) -> dict[str, int | str]:
    return {"id": user_id, "name": "Ada"}


profile = await fetch_profile(1)

For async-only applications, use AsyncLRUCacheManager or AsyncTTLCacheManager with async_cached to protect cache operations with asyncio.Lock.

from logged_cache import AsyncLRUCacheManager, async_cached

profiles = AsyncLRUCacheManager(maxsize=256, name="profiles")


@async_cached(profiles)
async def fetch_profile(user_id: int) -> dict[str, int | str]:
    return {"id": user_id, "name": "Ada"}

Methods

Use cachedmethod for instance methods and class methods. By default, it uses cachetools.keys.methodkey, so the first method argument (self or cls) is not included in the cache key.

from logged_cache import LRUCacheManager, cachedmethod


class ProfileService:
    cache = LRUCacheManager(maxsize=256)

    @cachedmethod(cache)
    def load_profile(self, user_id: int) -> dict[str, int | str]:
        return {"id": user_id, "name": "Ada"}

Async methods work the same way:

from logged_cache import AsyncLRUCacheManager, async_cachedmethod


class AsyncProfileService:
    cache = AsyncLRUCacheManager(maxsize=256)

    @async_cachedmethod(cache)
    async def load_profile(self, user_id: int) -> dict[str, int | str]:
        return {"id": user_id, "name": "Ada"}

Direct Mapping Usage

from cachetools import LRUCache
from logged_cache import LoggedCache

cache = LoggedCache(LRUCache(maxsize=2))
cache["a"] = 1

if "a" in cache:
    print(cache["a"])

Direct mapping operations emit the same event names:

DEBUG:myapp.cache:[logged-cache] Cache set: key='a'
DEBUG:myapp.cache:[logged-cache] Cache hit: key='a'
DEBUG:myapp.cache:[logged-cache] Cache delete: key='a'
DEBUG:myapp.cache:[logged-cache] Cache cleared

Redacting Sensitive Keys

By default, keys are logged with a safe formatter that truncates large values and describes plain object instances by class. For user IDs, tokens, emails, or other sensitive values, pass a formatter:

from logged_cache import LRUCacheManager


def redact_key(key: object) -> str:
    return "<redacted>"


cache = LRUCacheManager(maxsize=128, key_formatter=redact_key)

To keep the default behavior but change the maximum key length, use format_cache_key:

from functools import partial
from logged_cache import LRUCacheManager, format_cache_key

cache = LRUCacheManager(
    maxsize=128,
    key_formatter=partial(format_cache_key, max_length=80),
)

Naming Caches

Pass name when the same logger receives events from several caches.

users = LRUCacheManager(maxsize=256, name="users")
tokens = TTLCacheManager(maxsize=1000, ttl=300, name="tokens")

If name is omitted and the manager is used with cached or cachedmethod, the package sets a fallback from the decorated callable's module and qualified name. Explicit names are never replaced by decorators.

Exporting Metrics

Use on_event to bridge cache events into your metrics stack.

from logged_cache import CacheEvent, LRUCacheManager


def record_metric(event: CacheEvent, key: object) -> None:
    print(f"cache.{event}")


cache = LRUCacheManager(maxsize=128, on_event=record_metric)

API Overview

LoggedCache(inner, logger=None, log_level=logging.DEBUG, stats=None, key_formatter=repr, on_event=None, name=None) wraps any mutable mapping, including cachetools caches. If logger is None, the root logger from logging.getLogger() is used.

LRUCacheManager(maxsize, ...) creates an LRUCache, a LoggedCache, an RLock, and a shared CacheStats object.

TTLCacheManager(maxsize, ttl, ...) does the same for TTLCache.

AsyncLRUCacheManager(maxsize, ...) and AsyncTTLCacheManager(maxsize, ttl, ...) use asyncio.Lock for async-only applications.

cached(manager, key=cachetools.keys.hashkey, info=False) returns a decorator compatible with regular and async functions.

cachedmethod(manager, key=cachetools.keys.methodkey, info=False) returns a decorator compatible with regular and async methods.

async_cached(...) and async_cachedmethod(...) are async-only variants for async cache managers.

Development

uv sync --extra dev --extra docs
uv run ruff check .
uv run pylint src/logged_cache tests
uv run mypy
uv run pytest
uv run python -m build
uv run twine check dist/*

Install pre-commit hooks with:

uv run pre-commit install

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

logged_cache-0.0.0.tar.gz (18.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

logged_cache-0.0.0-py3-none-any.whl (13.6 kB view details)

Uploaded Python 3

File details

Details for the file logged_cache-0.0.0.tar.gz.

File metadata

  • Download URL: logged_cache-0.0.0.tar.gz
  • Upload date:
  • Size: 18.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for logged_cache-0.0.0.tar.gz
Algorithm Hash digest
SHA256 6f81d23c1d1f85185ab585ff9a00dc52cf3f01d3277f52e1675efbd49ffdf20f
MD5 d6d701b2a3af2dd26573a8c31960eb55
BLAKE2b-256 998d16ab8a48032ed1a8660f6434779a159441f49e1161b3724b61555a12e025

See more details on using hashes here.

Provenance

The following attestation bundles were made for logged_cache-0.0.0.tar.gz:

Publisher: release.yml on 51n91n51nk1n/logged_cache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file logged_cache-0.0.0-py3-none-any.whl.

File metadata

  • Download URL: logged_cache-0.0.0-py3-none-any.whl
  • Upload date:
  • Size: 13.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for logged_cache-0.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 88daf31a735a2beaeb6150861e6460365eb7c45ccb03ba561934add98b87fcda
MD5 dc976f8823ce87a53ea91c98aef19921
BLAKE2b-256 a22d1d0211d03d3c83d553c15ce9577e6fc1fc7c9a7cd8158673800530e76ffb

See more details on using hashes here.

Provenance

The following attestation bundles were made for logged_cache-0.0.0-py3-none-any.whl:

Publisher: release.yml on 51n91n51nk1n/logged_cache

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page