sloria / environs

simplified environment variable parsing

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Log values as they are returned

jrouly opened this issue · comments

In production environments it's frequently handy for environment variables to get logged as they're read in or defaulted. This can help significantly with debugging e.g. when trying to reproduce the environment locally or figure out why a configuration isn't getting read in correctly when there's a mismatch in environment variable keys.

Masking credentials read in from environment variables may be a concern, but should probably be out of scope of a general purpose library like environs. I could see either:

  • requiring the user to opt-in by explicitly enabling logging on an Env object (env.with_logging() or something)
  • requiring the user to opt-out by explicitly disabling logging on each lookup (env.str("FOO", "bar", log=False))

could you meet the use case by logging the serialized values?

logger.debug(f"environment: {env.dump()}")

env.dump() only returns values that have already been retrieved (AFAIK). It would be difficult to know when to log that.

For example, if it's logged on application start, that may be too early and other environment variables could be read later in the application lifecycle.

If it's logged every time a value is looked up, then we may as well just log the retrieved value.

are you reading envvars during app runtime? if so, i'll warn that that's not the intended use case. we recommend parsing all the environment variables at one time in a settings module (like in the flask example). that ensures that they all get validated before the app starts, and it would also make it easier to log the values.

Generally speaking that's what we're doing (loading from Env on startup), but they're not loaded centrally. During dependency injection, different services and modules are wiring in their own from a shared Env object that gets passed around.

I guess we could add a log of env.dump() at the end of dependency injection if that's your recommendation, but it just feels riskier to separate that information out from the points where Env is actually accessed.

Hello @jrouly , currently this is not implemented but you can overwrite the call and other methods with the decorated logged method to achieve the result you want. It's a bit hacky, here is a working example. It was fun to make, but I have to say it's not a good practice and I don't encourage this to be done in production code.

import functools
import logging
from environs import Env


def log_env_result(func, /, key_index):
    logger = logging.getLogger("environs")

    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        result = func(*args, **kwargs)
        key = kwargs.get("name") or args[key_index]
        logger.debug(f"Getting value for key {key}: '{result}'")
        return result
    return wrapper


class CustomLoggingEnv(Env):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        # replace every single method with the decorated version
        for field in [
            "int", "bool", "str", "float", "decimal", "list", "dict", "json", "datetime", "date", "time",
            "path", "log_level", "timedelta", "uuid", "url", "enum", "dj_db_url", "dj_email_url", "dj_cache_url"
        ]:
            self.__setattr__(field, log_env_result(self.__getattribute__(field), key_index=0))

    __call__ = log_env_result(Env.__call__, key_index=1)  # since self is passed the key index is 1


env = CustomLoggingEnv()
env.read_env()  # read .env file, if it exists

# nothing should print
env("GITHUB_USER", "hello world")
env.int("NUMBER_OF_RETRIES", "10")

# set logging on module to debug
logging.basicConfig()
logging.getLogger("environs").setLevel("DEBUG")
env("GITHUB_USER", "hello world")
env.int("NUMBER_OF_RETRIES", "10")

That produces:

DEBUG:environs:Getting value for key GITHUB_USER: 'hello world'
DEBUG:environs:Getting value for key NUMBER_OF_RETRIES: '10'

Process finished with exit code 0