Skip to content

Commit 357a841

Browse files
🎨 introduce logger filtering with ENV var (#6596)
1 parent 87a9b7e commit 357a841

File tree

41 files changed

+330
-37
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+330
-37
lines changed

.env-devel

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# unset $(grep -v '^#' .env | sed -E 's/(.*)=.*/\1/' | xargs)
1111
#
1212

13-
AGENT_LOGLEVEL=WARNING
13+
AGENT_LOGLEVEL=INFO
1414
AGENT_VOLUMES_CLEANUP_S3_ACCESS_KEY=12345678
1515
AGENT_VOLUMES_CLEANUP_S3_BUCKET=simcore-volume-backups
1616
AGENT_VOLUMES_CLEANUP_S3_ENDPOINT=http://172.17.0.1:9001
@@ -19,7 +19,7 @@ AGENT_VOLUMES_CLEANUP_S3_REGION=us-east-1
1919
AGENT_VOLUMES_CLEANUP_S3_SECRET_KEY=12345678
2020

2121
API_SERVER_DEV_FEATURES_ENABLED=0
22-
API_SERVER_LOGLEVEL=WARNING
22+
API_SERVER_LOGLEVEL=INFO
2323
API_SERVER_PROFILING=1
2424
TRAEFIK_API_SERVER_INFLIGHTREQ_AMOUNT=25
2525

@@ -29,7 +29,7 @@ AUTOSCALING_DOCKER_JOIN_DRAINED=True
2929
AUTOSCALING_WAIT_FOR_CLOUD_INIT_BEFORE_WARM_BUFFER_ACTIVATION=False
3030
AUTOSCALING_EC2_ACCESS=null
3131
AUTOSCALING_EC2_INSTANCES=null
32-
AUTOSCALING_LOGLEVEL=WARNING
32+
AUTOSCALING_LOGLEVEL=INFO
3333
AUTOSCALING_NODES_MONITORING=null
3434
AUTOSCALING_POLL_INTERVAL=10
3535
AUTOSCALING_SSM_ACCESS=null
@@ -39,7 +39,7 @@ AWS_S3_CLI_S3=null
3939
CATALOG_BACKGROUND_TASK_REST_TIME=60
4040
CATALOG_DEV_FEATURES_ENABLED=0
4141
CATALOG_HOST=catalog
42-
CATALOG_LOGLEVEL=WARNING
42+
CATALOG_LOGLEVEL=INFO
4343
CATALOG_PORT=8000
4444
CATALOG_PROFILING=1
4545
CATALOG_SERVICES_DEFAULT_RESOURCES='{"CPU": {"limit": 0.1, "reservation": 0.1}, "RAM": {"limit": 2147483648, "reservation": 2147483648}}'
@@ -52,15 +52,15 @@ CLUSTERS_KEEPER_DASK_WORKER_SATURATION=inf
5252
CLUSTERS_KEEPER_EC2_ACCESS=null
5353
CLUSTERS_KEEPER_SSM_ACCESS=null
5454
CLUSTERS_KEEPER_EC2_INSTANCES_PREFIX=""
55-
CLUSTERS_KEEPER_LOGLEVEL=WARNING
55+
CLUSTERS_KEEPER_LOGLEVEL=INFO
5656
CLUSTERS_KEEPER_MAX_MISSED_HEARTBEATS_BEFORE_CLUSTER_TERMINATION=5
5757
CLUSTERS_KEEPER_PRIMARY_EC2_INSTANCES=null
5858
CLUSTERS_KEEPER_TASK_INTERVAL=30
5959
CLUSTERS_KEEPER_WORKERS_EC2_INSTANCES=null
6060

6161
DASK_SCHEDULER_HOST=dask-scheduler
6262
DASK_SCHEDULER_PORT=8786
63-
DASK_SIDECAR_LOGLEVEL=WARNING
63+
DASK_SIDECAR_LOGLEVEL=INFO
6464
DASK_TLS_CA_FILE=/home/scu/.dask/dask-crt.pem
6565
DASK_TLS_CERT=/home/scu/.dask/dask-crt.pem
6666
DASK_TLS_KEY=/home/scu/.dask/dask-key.pem
@@ -91,7 +91,7 @@ DIRECTOR_V2_DYNAMIC_SCHEDULER_CLOSE_SERVICES_VIA_FRONTEND_WHEN_CREDITS_LIMIT_REA
9191
DIRECTOR_V2_DYNAMIC_SIDECAR_SLEEP_AFTER_CONTAINER_REMOVAL=0
9292
DIRECTOR_V2_GENERIC_RESOURCE_PLACEMENT_CONSTRAINTS_SUBSTITUTIONS='{}'
9393
DIRECTOR_V2_HOST=director-v2
94-
DIRECTOR_V2_LOGLEVEL=WARNING
94+
DIRECTOR_V2_LOGLEVEL=INFO
9595
DIRECTOR_V2_NODE_PORTS_STORAGE_AUTH=null
9696
DIRECTOR_V2_PORT=8000
9797
DIRECTOR_V2_PROFILING=1
@@ -115,7 +115,7 @@ FUNCTION_SERVICES_AUTHORS='{"UN": {"name": "Unknown", "email": "[email protected]
115115
# Can use 'docker run -it itisfoundation/invitations:latest simcore-service-invitations generate-dotenv --auto-password'
116116
INVITATIONS_DEFAULT_PRODUCT=osparc
117117
INVITATIONS_HOST=invitations
118-
INVITATIONS_LOGLEVEL=WARNING
118+
INVITATIONS_LOGLEVEL=INFO
119119
INVITATIONS_OSPARC_URL=http://127.0.0.1.nip.io:9081
120120
INVITATIONS_PASSWORD=adminadmin
121121
INVITATIONS_PORT=8000
@@ -124,6 +124,7 @@ INVITATIONS_SWAGGER_API_DOC_ENABLED=1
124124
INVITATIONS_USERNAME=admin
125125

126126
LOG_FORMAT_LOCAL_DEV_ENABLED=1
127+
LOG_FILTER_MAPPING='{}'
127128

128129
PAYMENTS_ACCESS_TOKEN_EXPIRE_MINUTES=30
129130
PAYMENTS_ACCESS_TOKEN_SECRET_KEY=2c0411810565e063309be1457009fb39ce023946f6a354e6935107b57676
@@ -138,7 +139,7 @@ PAYMENTS_FAKE_COMPLETION=0
138139
PAYMENTS_GATEWAY_API_SECRET=adminadmin
139140
PAYMENTS_GATEWAY_URL=http://127.0.0.1:32769
140141
PAYMENTS_HOST=payments
141-
PAYMENTS_LOGLEVEL=WARNING
142+
PAYMENTS_LOGLEVEL=INFO
142143
PAYMENTS_PASSWORD=adminadmin
143144
PAYMENTS_PORT=8000
144145
PAYMENTS_STRIPE_API_SECRET='REPLACE_ME_with_api_secret'
@@ -179,7 +180,7 @@ RESOURCE_MANAGER_RESOURCE_TTL_S=900
179180
RESOURCE_USAGE_TRACKER_HOST=resource-usage-tracker
180181
RESOURCE_USAGE_TRACKER_PORT=8000
181182
RESOURCE_USAGE_TRACKER_EXTERNAL_PORT=8000
182-
RESOURCE_USAGE_TRACKER_LOGLEVEL=WARNING
183+
RESOURCE_USAGE_TRACKER_LOGLEVEL=INFO
183184
RESOURCE_USAGE_TRACKER_MISSED_HEARTBEAT_CHECK_ENABLED=1
184185
RESOURCE_USAGE_TRACKER_MISSED_HEARTBEAT_COUNTER_FAIL=6
185186
RESOURCE_USAGE_TRACKER_MISSED_HEARTBEAT_INTERVAL_SEC=300
@@ -214,7 +215,7 @@ BF_API_KEY=none
214215
BF_API_SECRET=none
215216
STORAGE_ENDPOINT=storage:8080
216217
STORAGE_HOST=storage
217-
STORAGE_LOGLEVEL=WARNING
218+
STORAGE_LOGLEVEL=INFO
218219
STORAGE_PORT=8080
219220
STORAGE_PROFILING=1
220221
# STORAGE ----
@@ -245,7 +246,7 @@ WB_GC_GARBAGE_COLLECTOR='{"GARBAGE_COLLECTOR_INTERVAL_S": 30}'
245246
WB_GC_GROUPS=0
246247
WB_GC_INVITATIONS=null
247248
WB_GC_LOGIN=null
248-
WB_GC_LOGLEVEL=WARNING
249+
WB_GC_LOGLEVEL=INFO
249250
WB_GC_META_MODELING=0
250251
WB_GC_NOTIFICATIONS=0
251252
WB_GC_PAYMENTS=null
@@ -278,7 +279,7 @@ WB_DB_EL_GARBAGE_COLLECTOR=null
278279
WB_DB_EL_GROUPS=0
279280
WB_DB_EL_INVITATIONS=null
280281
WB_DB_EL_LOGIN=null
281-
WB_DB_EL_LOGLEVEL=WARNING
282+
WB_DB_EL_LOGLEVEL=INFO
282283
WB_DB_EL_META_MODELING=0
283284
WB_DB_EL_NOTIFICATIONS=0
284285
WB_DB_EL_PAYMENTS=null
@@ -348,7 +349,7 @@ WEBSERVER_GROUPS=1
348349
WEBSERVER_GUNICORN_CMD_ARGS=--timeout=180
349350
WEBSERVER_HOST=webserver
350351
WEBSERVER_LOGIN={}
351-
WEBSERVER_LOGLEVEL=WARNING
352+
WEBSERVER_LOGLEVEL=INFO
352353
WEBSERVER_META_MODELING=1
353354
WEBSERVER_NOTIFICATIONS=1
354355
WEBSERVER_PAYMENTS={}

packages/service-library/src/servicelib/logging_utils.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from pathlib import Path
1717
from typing import Any, NotRequired, TypeAlias, TypedDict, TypeVar
1818

19+
from .logging_utils_filtering import GeneralLogFilter, LoggerName, MessageSubstring
1920
from .utils_secrets import mask_sensitive_data
2021

2122
_logger = logging.getLogger(__name__)
@@ -86,7 +87,11 @@ def format(self, record) -> str:
8687
# log_level=%{WORD:log_level} \| log_timestamp=%{TIMESTAMP_ISO8601:log_timestamp} \| log_source=%{DATA:log_source} \| log_msg=%{GREEDYDATA:log_msg}
8788

8889

89-
def config_all_loggers(*, log_format_local_dev_enabled: bool) -> None:
90+
def config_all_loggers(
91+
*,
92+
log_format_local_dev_enabled: bool,
93+
logger_filter_mapping: dict[LoggerName, list[MessageSubstring]],
94+
) -> None:
9095
"""
9196
Applies common configuration to ALL registered loggers
9297
"""
@@ -102,12 +107,25 @@ def config_all_loggers(*, log_format_local_dev_enabled: bool) -> None:
102107
fmt = LOCAL_FORMATTING
103108

104109
for logger in loggers:
105-
set_logging_handler(
110+
_set_logging_handler(
106111
logger, fmt=fmt, log_format_local_dev_enabled=log_format_local_dev_enabled
107112
)
108113

114+
for logger_name, filtered_routes in logger_filter_mapping.items():
115+
logger = logging.getLogger(logger_name)
116+
# Check if the logger has any handlers or is in active use
117+
if not logger.hasHandlers():
118+
_logger.warning(
119+
"Logger %s does not have any handlers. Filter will not be added.",
120+
logger_name,
121+
)
122+
continue
123+
124+
log_filter = GeneralLogFilter(filtered_routes)
125+
logger.addFilter(log_filter)
126+
109127

110-
def set_logging_handler(
128+
def _set_logging_handler(
111129
logger: logging.Logger,
112130
*,
113131
fmt: str,
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""
2+
This codes originates from this article
3+
https://medium.com/swlh/add-log-decorators-to-your-python-project-84094f832181
4+
5+
SEE also https://github.com/Delgan/loguru for a future alternative
6+
"""
7+
8+
import logging
9+
from typing import TypeAlias
10+
11+
_logger = logging.getLogger(__name__)
12+
13+
LoggerName: TypeAlias = str
14+
MessageSubstring: TypeAlias = str
15+
16+
17+
class GeneralLogFilter(logging.Filter):
18+
def __init__(self, filtered_routes: list[str]) -> None:
19+
super().__init__()
20+
self.filtered_routes = filtered_routes
21+
22+
def filter(self, record: logging.LogRecord) -> bool:
23+
msg = record.getMessage()
24+
25+
# Check if the filtered routes exists in the message
26+
return not any(
27+
filter_criteria in msg for filter_criteria in self.filtered_routes
28+
)
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# pylint: disable=redefined-outer-name
2+
3+
import logging
4+
from typing import Generator
5+
6+
import pytest
7+
from servicelib.logging_utils_filtering import GeneralLogFilter
8+
9+
10+
@pytest.fixture
11+
def logger_with_filter() -> Generator[tuple[logging.Logger, list[str]], None, None]:
12+
# Set up a logger for testing
13+
logger = logging.getLogger("uvicorn.access")
14+
logger.setLevel(logging.DEBUG)
15+
16+
# Create a list to capture log outputs
17+
log_capture = []
18+
19+
# Create a handler that appends log messages to the log_capture list
20+
class ListHandler(logging.Handler):
21+
def emit(self, record):
22+
log_capture.append(self.format(record))
23+
24+
handler = ListHandler()
25+
logger.addHandler(handler)
26+
27+
# Set up the filter based on the new logic
28+
filtered_routes = [
29+
'"GET / HTTP/1.1" 200',
30+
'"GET /metrics HTTP/1.1" 200',
31+
]
32+
33+
# Add the GeneralLogFilter to the logger
34+
log_filter = GeneralLogFilter(filtered_routes)
35+
logger.addFilter(log_filter)
36+
37+
# Return logger and the log_capture for testing
38+
yield logger, log_capture
39+
40+
# Cleanup: remove handlers and filters after test
41+
logger.handlers.clear()
42+
logger.filters.clear()
43+
44+
45+
def test_log_filtered_out(logger_with_filter: tuple[logging.Logger, list[str]]):
46+
logger, log_capture = logger_with_filter
47+
48+
# Create a log record that should be filtered out (matches the filter criteria)
49+
record = logger.makeRecord(
50+
name="uvicorn.access",
51+
level=logging.INFO,
52+
fn="testfile",
53+
lno=10,
54+
msg='"GET / HTTP/1.1" 200 OK',
55+
args=(),
56+
exc_info=None,
57+
)
58+
logger.handle(record)
59+
60+
# Assert no log messages were captured (filtered out)
61+
assert len(log_capture) == 0
62+
63+
64+
def test_log_allowed(logger_with_filter):
65+
logger, log_capture = logger_with_filter
66+
67+
# Create a log record that should NOT be filtered out (doesn't match any filter criteria)
68+
record = logger.makeRecord(
69+
name="uvicorn.access",
70+
level=logging.INFO,
71+
fn="testfile",
72+
lno=10,
73+
msg='"GET /another HTTP/1.1" 200 OK',
74+
args=(),
75+
exc_info=None,
76+
)
77+
logger.handle(record)
78+
79+
# Assert the log message was captured (not filtered out)
80+
assert len(log_capture) == 1
81+
82+
83+
def test_log_with_different_status(logger_with_filter):
84+
logger, log_capture = logger_with_filter
85+
86+
# Create a log record that has the same route but a different status code (should pass through)
87+
record = logger.makeRecord(
88+
name="uvicorn.access",
89+
level=logging.INFO,
90+
fn="testfile",
91+
lno=10,
92+
msg='"GET / HTTP/1.1" 500 Internal Server Error',
93+
args=(),
94+
exc_info=None,
95+
)
96+
logger.handle(record)
97+
98+
# Assert the log message was captured (not filtered out due to different status code)
99+
assert len(log_capture) == 1

services/agent/src/simcore_service_agent/core/application.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,8 @@ def _setup_logger(settings: ApplicationSettings):
3030
logging.basicConfig(level=settings.LOGLEVEL.value) # NOSONAR
3131
logging.root.setLevel(settings.LOGLEVEL.value)
3232
config_all_loggers(
33-
log_format_local_dev_enabled=settings.AGENT_VOLUMES_LOG_FORMAT_LOCAL_DEV_ENABLED
33+
log_format_local_dev_enabled=settings.AGENT_VOLUMES_LOG_FORMAT_LOCAL_DEV_ENABLED,
34+
logger_filter_mapping=settings.AGENT_VOLUMES_LOG_FILTER_MAPPING,
3435
)
3536

3637

services/agent/src/simcore_service_agent/core/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from models_library.basic_types import BootModeEnum, LogLevel
44
from pydantic import AnyHttpUrl, Field, validator
5+
from servicelib.logging_utils_filtering import LoggerName, MessageSubstring
56
from settings_library.base import BaseCustomSettings
67
from settings_library.r_clone import S3Provider
78
from settings_library.rabbit import RabbitSettings
@@ -25,6 +26,11 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings):
2526
"disabled if you want to have structured logs!"
2627
),
2728
)
29+
AGENT_VOLUMES_LOG_FILTER_MAPPING: dict[LoggerName, list[MessageSubstring]] = Field(
30+
default_factory=dict,
31+
env=["AGENT_VOLUMES_LOG_FILTER_MAPPING", "LOG_FILTER_MAPPING"],
32+
description="is a dictionary that maps specific loggers (such as 'uvicorn.access' or 'gunicorn.access') to a list of log message patterns that should be filtered out.",
33+
)
2834
AGENT_VOLUMES_CLEANUP_TARGET_SWARM_STACK_NAME: str = Field(
2935
..., description="Exactly the same as director-v2's `SWARM_STACK_NAME` env var"
3036
)

services/api-server/src/simcore_service_api_server/core/application.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ def init_app(settings: ApplicationSettings | None = None) -> FastAPI:
5252
logging.basicConfig(level=settings.log_level)
5353
logging.root.setLevel(settings.log_level)
5454
config_all_loggers(
55-
log_format_local_dev_enabled=settings.API_SERVER_LOG_FORMAT_LOCAL_DEV_ENABLED
55+
log_format_local_dev_enabled=settings.API_SERVER_LOG_FORMAT_LOCAL_DEV_ENABLED,
56+
logger_filter_mapping=settings.API_SERVER_LOG_FILTER_MAPPING,
5657
)
5758
_logger.debug("App settings:\n%s", settings.json(indent=2))
5859

services/api-server/src/simcore_service_api_server/core/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from models_library.basic_types import BootModeEnum, LogLevel
44
from pydantic import Field, NonNegativeInt, PositiveInt, SecretStr
55
from pydantic.class_validators import validator
6+
from servicelib.logging_utils_filtering import LoggerName, MessageSubstring
67
from settings_library.base import BaseCustomSettings
78
from settings_library.catalog import CatalogSettings
89
from settings_library.director_v2 import DirectorV2Settings
@@ -55,6 +56,11 @@ class BasicSettings(BaseCustomSettings, MixinLoggingSettings):
5556
env=["API_SERVER_LOG_FORMAT_LOCAL_DEV_ENABLED", "LOG_FORMAT_LOCAL_DEV_ENABLED"],
5657
description="Enables local development log format. WARNING: make sure it is disabled if you want to have structured logs!",
5758
)
59+
API_SERVER_LOG_FILTER_MAPPING: dict[LoggerName, list[MessageSubstring]] = Field(
60+
default_factory=dict,
61+
env=["API_SERVER_LOG_FILTER_MAPPING", "LOG_FILTER_MAPPING"],
62+
description="is a dictionary that maps specific loggers (such as 'uvicorn.access' or 'gunicorn.access') to a list of log message patterns that should be filtered out.",
63+
)
5864

5965
@validator("LOG_LEVEL", pre=True)
6066
@classmethod

services/autoscaling/src/simcore_service_autoscaling/core/settings.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
root_validator,
2323
validator,
2424
)
25+
from servicelib.logging_utils_filtering import LoggerName, MessageSubstring
2526
from settings_library.base import BaseCustomSettings
2627
from settings_library.docker_registry import RegistrySettings
2728
from settings_library.ec2 import EC2Settings
@@ -240,6 +241,11 @@ class ApplicationSettings(BaseCustomSettings, MixinLoggingSettings):
240241
],
241242
description="Enables local development log format. WARNING: make sure it is disabled if you want to have structured logs!",
242243
)
244+
AUTOSCALING_LOG_FILTER_MAPPING: dict[LoggerName, list[MessageSubstring]] = Field(
245+
default_factory=dict,
246+
env=["AUTOSCALING_LOG_FILTER_MAPPING", "LOG_FILTER_MAPPING"],
247+
description="is a dictionary that maps specific loggers (such as 'uvicorn.access' or 'gunicorn.access') to a list of log message patterns that should be filtered out.",
248+
)
243249

244250
AUTOSCALING_EC2_ACCESS: AutoscalingEC2Settings | None = Field(
245251
auto_default_from_env=True

0 commit comments

Comments
 (0)