Skip to content

Commit fef161f

Browse files
committed
Update app configuration
Why these changes are being introduced: In most CLI and lambda apps we have used a Config class approach for app configuration. Additionally, there has been kind of a fuzzy line for the lambda template specifically between "cold-start" (when Lambda function is initialized) and "hot" (when Lambda is fielding requests without fully shutting down) configurations. How this addresses that need: * Adds Config class * Updates main lambda entrypoint file to have cold-start configurations (e.g. logging and Sentry) and very little hot configuration * Tests are updated to reflect new central Config class Side effects of this change: * None Relevant ticket(s): * None
1 parent 44bfef4 commit fef161f

File tree

6 files changed

+352
-206
lines changed

6 files changed

+352
-206
lines changed

lambdas/config.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import logging
2+
import os
3+
4+
import sentry_sdk
5+
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
6+
7+
logger = logging.getLogger(__name__)
8+
logger.setLevel(logging.DEBUG)
9+
10+
11+
class Config:
12+
REQUIRED_ENV_VARS = (
13+
"WORKSPACE",
14+
"SENTRY_DSN",
15+
)
16+
OPTIONAL_ENV_VARS = ("WARNING_ONLY_LOGGERS",)
17+
18+
def check_required_env_vars(self) -> None:
19+
"""Method to raise exception if required env vars not set."""
20+
missing_vars = [var for var in self.REQUIRED_ENV_VARS if not os.getenv(var)]
21+
if missing_vars:
22+
message = f"Missing required environment variables: {', '.join(missing_vars)}"
23+
raise OSError(message)
24+
25+
@property
26+
def workspace(self) -> str | None:
27+
return os.getenv("WORKSPACE")
28+
29+
@property
30+
def sentry_dsn(self) -> str | None:
31+
dsn = os.getenv("SENTRY_DSN")
32+
if dsn and dsn.strip().lower() != "none":
33+
return dsn
34+
return None
35+
36+
37+
def configure_logger(
38+
root_logger: logging.Logger,
39+
*,
40+
verbose: bool = False,
41+
warning_only_loggers: str | None = None,
42+
) -> str:
43+
"""Configure application via passed application root logger.
44+
45+
If verbose=True, 3rd party libraries can be quite chatty. For convenience, they can
46+
be set to WARNING level by either passing a comma seperated list of logger names to
47+
'warning_only_loggers' or by setting the env var WARNING_ONLY_LOGGERS.
48+
"""
49+
if verbose:
50+
root_logger.setLevel(logging.DEBUG)
51+
logging_format = (
52+
"%(asctime)s %(levelname)s %(name)s.%(funcName)s() "
53+
"line %(lineno)d: %(message)s"
54+
)
55+
else:
56+
root_logger.setLevel(logging.INFO)
57+
logging_format = "%(asctime)s %(levelname)s %(name)s.%(funcName)s(): %(message)s"
58+
59+
warning_only_loggers = os.getenv("WARNING_ONLY_LOGGERS", warning_only_loggers)
60+
if warning_only_loggers:
61+
for name in warning_only_loggers.split(","):
62+
logging.getLogger(name).setLevel(logging.WARNING)
63+
64+
# Clear any existing handlers to prevent duplication in AWS Lambda environment
65+
# where container may be reused between invocations
66+
for handler in root_logger.handlers[:]:
67+
root_logger.removeHandler(handler)
68+
69+
handler = logging.StreamHandler()
70+
handler.setFormatter(logging.Formatter(logging_format))
71+
root_logger.addHandler(handler)
72+
73+
return (
74+
f"Logger '{root_logger.name}' configured with level="
75+
f"{logging.getLevelName(root_logger.getEffectiveLevel())}"
76+
)
77+
78+
79+
def configure_dev_logger(
80+
warning_only_loggers: str = ",".join( # noqa: FLY002
81+
["asyncio", "botocore", "urllib3", "boto3", "smart_open"]
82+
),
83+
) -> None:
84+
"""Invoke to setup DEBUG level console logging for development work."""
85+
os.environ["WARNING_ONLY_LOGGERS"] = warning_only_loggers
86+
root_logger = logging.getLogger()
87+
configure_logger(root_logger, verbose=True)
88+
89+
90+
def configure_sentry() -> None:
91+
CONFIG = Config() # noqa: N806
92+
env = CONFIG.workspace
93+
if CONFIG.sentry_dsn:
94+
sentry_sdk.init(
95+
dsn=CONFIG.sentry_dsn,
96+
environment=env,
97+
integrations=[
98+
AwsLambdaIntegration(),
99+
],
100+
traces_sample_rate=1.0,
101+
)
102+
logger.info(
103+
"Sentry DSN found, exceptions will be sent to Sentry with env=%s", env
104+
)
105+
else:
106+
logger.info("No Sentry DSN found, exceptions will not be sent to Sentry")

lambdas/my_function.py

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,24 @@
11
import json
22
import logging
3-
import os
43

5-
import sentry_sdk
6-
from sentry_sdk.integrations.aws_lambda import AwsLambdaIntegration
4+
from lambdas.config import Config, configure_logger, configure_sentry
75

6+
# ---------------------------------------
7+
# One-time, Lambda cold start setup
8+
# ---------------------------------------
9+
CONFIG = Config()
10+
11+
root_logger = logging.getLogger()
12+
log_config_message = configure_logger(root_logger)
813
logger = logging.getLogger(__name__)
9-
logger.setLevel(logging.DEBUG)
14+
logger.info(log_config_message)
1015

11-
env = os.getenv("WORKSPACE")
12-
if sentry_dsn := os.getenv("SENTRY_DSN"):
13-
sentry = sentry_sdk.init(
14-
dsn=sentry_dsn,
15-
environment=env,
16-
integrations=[
17-
AwsLambdaIntegration(),
18-
],
19-
traces_sample_rate=1.0,
20-
)
21-
logger.info("Sentry DSN found, exceptions will be sent to Sentry with env=%s", env)
22-
else:
23-
logger.info("No Sentry DSN found, exceptions will not be sent to Sentry")
16+
configure_sentry()
2417

2518

19+
# ---------------------------------------
20+
# Lambda handler entrypoint
21+
# ---------------------------------------
2622
def lambda_handler(event: dict) -> str:
27-
if not os.getenv("WORKSPACE"):
28-
unset_workspace_error_message = "Required env variable WORKSPACE is not set"
29-
raise RuntimeError(unset_workspace_error_message)
30-
3123
logger.debug(json.dumps(event))
32-
3324
return "You have successfully called this lambda!"

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ ignore = [
5555
"PLR0912",
5656
"PLR0913",
5757
"PLR0915",
58-
"S320",
5958
"S321",
6059
]
6160

tests/test_config.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# ruff: noqa: N806
2+
from unittest.mock import patch
3+
4+
import pytest
5+
6+
from lambdas.config import Config, configure_sentry
7+
8+
9+
def test_config_configures_sentry_if_dsn_present(caplog, monkeypatch):
10+
monkeypatch.setenv("SENTRY_DSN", "https://[email protected]/123456")
11+
with patch("sentry_sdk.init") as mock_init:
12+
configure_sentry()
13+
mock_init.assert_called_once()
14+
assert (
15+
"Sentry DSN found, exceptions will be sent to Sentry with env=test"
16+
in caplog.text
17+
)
18+
19+
20+
def test_config_doesnt_configure_sentry_if_dsn_not_present(caplog, monkeypatch):
21+
monkeypatch.delenv("SENTRY_DSN", raising=False)
22+
configure_sentry()
23+
assert "No Sentry DSN found, exceptions will not be sent to Sentry" in caplog.text
24+
25+
26+
def test_config_missing_required_env_vars(monkeypatch):
27+
monkeypatch.delenv("WORKSPACE")
28+
with pytest.raises(
29+
OSError, match="Missing required environment variables: WORKSPACE"
30+
):
31+
Config().check_required_env_vars()
32+
33+
34+
def test_config_env_var_dot_notation(monkeypatch):
35+
CONFIG = Config()
36+
assert CONFIG.workspace == "test"

tests/test_my_function.py

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,5 @@
1-
from importlib import reload
2-
3-
import pytest
4-
51
from lambdas import my_function
62

73

8-
def test_my_function_configures_sentry_if_dsn_present(caplog, monkeypatch):
9-
monkeypatch.setenv("SENTRY_DSN", "https://[email protected]/123456")
10-
reload(my_function)
11-
assert (
12-
"Sentry DSN found, exceptions will be sent to Sentry with env=test" in caplog.text
13-
)
14-
15-
16-
def test_my_function_doesnt_configure_sentry_if_dsn_not_present(caplog, monkeypatch):
17-
monkeypatch.delenv("SENTRY_DSN", raising=False)
18-
reload(my_function)
19-
assert "No Sentry DSN found, exceptions will not be sent to Sentry" in caplog.text
20-
21-
22-
def test_lambda_handler_missing_workspace_env_raises_error(monkeypatch):
23-
monkeypatch.delenv("WORKSPACE", raising=False)
24-
with pytest.raises(RuntimeError) as error:
25-
my_function.lambda_handler({})
26-
assert "Required env variable WORKSPACE is not set" in str(error)
27-
28-
294
def test_my_function():
305
assert my_function.lambda_handler({}) == "You have successfully called this lambda!"

0 commit comments

Comments
 (0)