Skip to content

Commit f54088c

Browse files
diningPhilosopher64rashedmyt
authored andcommitted
Introduce 'TRACE' level logging and move all repetitive, less useful logs from DEBUG level to TRACE level
1 parent 71452e5 commit f54088c

8 files changed

Lines changed: 122 additions & 38 deletions

File tree

Advanced-Usage.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ The following table describes all the environment variables that you can set to
1818
| **MWI_BASE_URL** | string | `"/matlab"` | Set to control the base URL of the app. MWI_BASE_URL should start with `/` or be `empty`. |
1919
| **MWI_APP_PORT** | integer | `8080` | Specify the port for the HTTP server to listen on. |
2020
| **MWI_APP_HOST** | string | `127.0.0.1` | Specify the host address to display the connection URL in the startup message.|
21-
| **MWI_LOG_LEVEL** | string | `"CRITICAL"` | Specify the Python log level to be one of the following `NOTSET`, `DEBUG`, `INFO`, `WARN`, `ERROR`, or `CRITICAL`. For more information on Python log levels, see [Logging Levels](https://docs.python.org/3/library/logging.html#logging-levels) .<br />The default value is `INFO`. |
21+
| **MWI_LOG_LEVEL** | string | `"CRITICAL"` | Specify the log level. Valid options are: `NOTSET`, `TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `CRITICAL`. In addition to the standard Python [Log Levels (Python Documentation)](https://docs.python.org/3/library/logging.html#logging-levels), `matlab-proxy` supports a custom `TRACE` level. Use `TRACE` when you require more granular diagnostic information than `DEBUG` mode provides.<br />The default value is `INFO`. |
2222
| **MWI_LOG_FILE** | string | `"/tmp/logs.txt"` | Specify the full path to the file where you want debug logs from this integration to be written. |
2323
| **MWI_ENABLE_WEB_LOGGING** | string | `"True"` | Set this value to `"True"` to see additional web server logs. |
2424
| **MWI_CUSTOM_HTTP_HEADERS** | string |`'{"Content-Security-Policy": "frame-ancestors *.example.com:*"}'`<br /> OR <br />`"/path/to/your/custom/http-headers.json"` |Specify valid HTTP headers as JSON data in a string format. <br /> Alternatively, specify the full path to the JSON file containing valid HTTP headers instead. These headers are injected into the HTTP response sent to the browser. </br> For more information, see the [Custom HTTP Headers](#custom-http-headers) section.|
@@ -182,6 +182,6 @@ Note: Restarting MATLAB from within `matlab-proxy` will run the specified code a
182182

183183
----
184184

185-
Copyright 2020-2025 The MathWorks, Inc.
185+
Copyright 2020-2026 The MathWorks, Inc.
186186

187187
----

matlab_proxy/app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,7 @@ async def matlab_view(req):
589589
# If we are trying to send request to matlab while the matlab_port is still not assigned
590590
# by embedded connector, return service not available and log a message
591591
if not matlab_port:
592-
logger.debug(
592+
logger.trace(
593593
"MATLAB hasn't fully started, please retry after embedded connector has started"
594594
)
595595
raise web.HTTPServiceUnavailable()
@@ -742,7 +742,7 @@ async def wsforward(ws_from, ws_to):
742742
client_exceptions.ServerDisconnectedError,
743743
client_exceptions.ClientConnectionError,
744744
):
745-
logger.debug(
745+
logger.trace(
746746
"Failed to forward HTTP request as MATLAB process may not be running."
747747
)
748748
raise web.HTTPServiceUnavailable()

matlab_proxy/app_state.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ async def __setup_env_for_matlab(self) -> dict:
995995
)
996996

997997
# Env setup related to logging
998-
# Very verbose logging in debug mode
998+
# Very verbose logging in debug or trace mode (trace is lower level than debug, so the below condition will also be true for trace level)
999999
if logger.isEnabledFor(logging.getLevelName("DEBUG")):
10001000
mwi_log_file = self.settings.get("mwi_log_file", None)
10011001
# If a log file is supplied to write matlab-proxy server logs,
@@ -1017,8 +1017,12 @@ async def __setup_env_for_matlab(self) -> dict:
10171017
logger.info(
10181018
f"Writing MATLAB process logs to: {matlab_env['MW_DIAGNOSTIC_DEST']}"
10191019
)
1020+
1021+
if logger.isEnabledFor(logging.getLevelName("TRACE")):
1022+
trace_spec = ".*=fatal,critical,error,warning;connector::worker.*=all;connector::container::http=all;connector::lifecycle=all;connector::http::server=all"
1023+
existing_spec = matlab_env.get("MW_DIAGNOSTIC_SPEC", "")
10201024
matlab_env["MW_DIAGNOSTIC_SPEC"] = (
1021-
"connector::http::server=all;connector::lifecycle=all"
1025+
f"{existing_spec};{trace_spec}" if existing_spec else trace_spec
10221026
)
10231027

10241028
# TODO Introduce a warmup flag to enable this?

matlab_proxy/util/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,14 +315,14 @@ async def acquire(self):
315315
await self._lock.acquire()
316316
# Store the current task or function information when the lock is acquired
317317
self._acquired_by = get_caller_name()
318-
logger.debug(f"Lock acquired by '{self.acquired_by}()'")
318+
logger.trace(f"Lock acquired by '{self.acquired_by}()'")
319319

320320
async def release(self):
321321
"""Releases the lock."""
322322
if self.locked():
323323
# Clear the owner information when the lock is released
324324
self._lock.release()
325-
logger.debug(f"Lock released by '{self.acquired_by}()'")
325+
logger.trace(f"Lock released by '{self.acquired_by}()'")
326326
self._acquired_by = None
327327

328328
else:

matlab_proxy/util/mwi/embedded_connector/request.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020-2024 The MathWorks, Inc.
1+
# Copyright 2020-2026 The MathWorks, Inc.
22

33
"""
44
This file contains the methods to communicate with the embedded connector.
@@ -47,14 +47,14 @@ async def send_request(url: str, data: dict, method: str, headers: dict = None)
4747

4848
try:
4949
async with aiohttp.ClientSession(trust_env=True) as session:
50-
logger.debug(
50+
logger.trace(
5151
f"sending request: method={method}, url={url}, data={data}, headers={headers}, "
5252
)
5353

5454
async with session.request(
5555
method=method, url=url, data=data, headers=headers, ssl=False
5656
) as resp:
57-
logger.debug(f"response from endpoint{url} and resp={resp}")
57+
logger.trace(f"response from endpoint{url} and resp={resp}")
5858
if not resp.ok:
5959
# Converting to dict and formatting for printing
6060
data = json.loads(data)

matlab_proxy/util/mwi/logger.py

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020-2025 The MathWorks, Inc.
1+
# Copyright 2020-2026 The MathWorks, Inc.
22
"""Functions to access & control the logging behavior of the app"""
33

44
import logging
@@ -12,15 +12,54 @@
1212

1313
from . import environment_variables as mwi_env
1414

15-
logging.getLogger("aiohttp_session").setLevel(logging.ERROR)
15+
# Define TRACE level (more detailed than DEBUG)
16+
TRACE = 5
17+
18+
19+
class MwLogger(logging.Logger):
20+
"""Custom logger that adds TRACE level support (more detailed than DEBUG).
21+
22+
This logger class extends the standard Python logging.Logger to provide
23+
an additional TRACE log level for ultra-detailed debugging that is more
24+
granular than DEBUG.
25+
26+
Usage:
27+
logger = mwi.logger.get()
28+
logger.trace("Ultra-detailed trace: %s", data)
29+
"""
30+
31+
def trace(self, msg, *args, **kwargs):
32+
"""
33+
Log a message at TRACE level.
34+
35+
TRACE level is more detailed than DEBUG and is intended for
36+
ultra-detailed tracing information that is typically only needed
37+
during deep debugging sessions.
38+
39+
Args:
40+
msg: The message format string
41+
*args: Arguments to merge into msg using string formatting
42+
**kwargs: Additional keyword arguments passed to Logger.log()
43+
"""
44+
self.log(TRACE, msg, *args, **kwargs)
45+
46+
47+
def _setup_logging_system():
48+
"""Initialize the custom logging system."""
49+
logging.addLevelName(TRACE, "TRACE")
50+
logging.setLoggerClass(MwLogger)
51+
logging.getLogger("aiohttp_session").setLevel(logging.ERROR)
1652

1753

1854
def get(init=False):
1955
"""Get the logger used by this application.
2056
Set init=True to initialize the logger
2157
Returns:
22-
Logger: The logger used by this application.
58+
MwLogger: The logger used by this application.
2359
"""
60+
# Always ensure logging system is set up first (registers MwLogger class)
61+
_setup_logging_system()
62+
2463
if init is True:
2564
return __set_logging_configuration()
2665

@@ -40,7 +79,7 @@ def __get_mw_logger():
4079
"""Returns logger for use in this app.
4180
4281
Returns:
43-
Logger: A logger object
82+
MwLogger: A logger object with TRACE level support.
4483
"""
4584
return logging.getLogger(__get_mw_logger_name())
4685

@@ -49,9 +88,12 @@ def __set_logging_configuration():
4988
"""Sets the logging environment for the app
5089
5190
Returns:
52-
Logger: Logger object with the set configuration.
91+
MwLogger: Logger object with the set configuration.
5392
"""
54-
# Create the Logger for MATLABProxy
93+
# Ensure the logging system is set up before creating the logger
94+
_setup_logging_system()
95+
96+
# Create the Logger for MATLABProxy (will be MwLogger due to setLoggerClass)
5597
logger = __get_mw_logger()
5698

5799
# log_level is either set by environment or is the default value.
@@ -142,7 +184,8 @@ def __is_invalid_log_level(log_level):
142184
Boolean: Whether log level exists
143185
"""
144186

145-
return not hasattr(logging, log_level)
187+
# Check standard Python levels OR our custom TRACE level
188+
return not (hasattr(logging, log_level) or log_level == "TRACE")
146189

147190

148191
def log_startup_info(title=None, matlab_urls=[]):
@@ -183,12 +226,15 @@ class _ColoredFormatter(logging.Formatter):
183226

184227
def format(self, record):
185228
# Example: Add 'color' and 'end_color' attributes based on log level
186-
if record.levelno == logging.INFO:
187-
record.color = "\033[32m" # Green
229+
if record.levelno == TRACE:
230+
record.color = "\033[36m" # Cyan (light blue)
188231
record.end_color = "\033[0m"
189232
elif record.levelno == logging.DEBUG:
190233
record.color = "\033[94m" # Blue
191234
record.end_color = "\033[0m"
235+
elif record.levelno == logging.INFO:
236+
record.color = "\033[32m" # Green
237+
record.end_color = "\033[0m"
192238
elif record.levelno == logging.WARNING:
193239
record.color = "\033[93m" # Yellow
194240
record.end_color = "\033[0m"

matlab_proxy/util/mwi/token_auth.py

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020-2025 The MathWorks, Inc.
1+
# Copyright 2020-2026 The MathWorks, Inc.
22

33
# This file contains functions required to enable token based authentication in the server.
44

@@ -79,17 +79,17 @@ async def authenticate_request(request):
7979
Returns True when authentication is disabled.
8080
"""
8181

82-
logger.debug(f"<======== Authenticate request: {request}")
82+
logger.trace(f"<======== Authenticate request: {request}")
8383

8484
if _is_mwi_token_auth_enabled(request):
85-
logger.debug("Authentication is Enabled.")
85+
logger.trace("Authentication is Enabled.")
8686
is_authenticated = (
8787
await _is_valid_token_in_session_cookie(request)
8888
or await _is_valid_token_in_headers(request)
8989
or await _is_valid_token_in_url_query(request)
9090
)
9191
if is_authenticated:
92-
logger.debug("Authentication successful. ========>")
92+
logger.trace("Authentication successful. ========>")
9393
else:
9494
logger.error("Token Authentication failed. ========>")
9595

@@ -184,7 +184,7 @@ async def _store_token_hash_into_session(request):
184184

185185
# Stash token hash in session for other endpoints
186186
session[await _get_token_name(request)] = await _get_token_hash(request)
187-
logger.debug(f"Created session and saved cookie.")
187+
logger.trace(f"Created session and saved cookie.")
188188

189189

190190
def _is_mwi_token_auth_enabled(request):
@@ -212,7 +212,7 @@ async def _is_valid_token(token, request):
212212
is_valid = compare_digest(token, await _get_token_hash(request)) or compare_digest(
213213
token, await _get_token(request)
214214
)
215-
logger.debug("Token validation " + ("successful." if is_valid else "failed."))
215+
logger.trace("Token validation " + ("successful." if is_valid else "failed."))
216216
return is_valid
217217

218218

@@ -225,16 +225,16 @@ async def _is_valid_token_in_session_cookie(request):
225225
Returns:
226226
Boolean : True if valid token is found
227227
"""
228-
logger.debug("Checking for token in session cookie...")
228+
logger.trace("Checking for token in session cookie...")
229229
session = await get_session(request)
230-
logger.debug(f"Got session cookie.")
230+
logger.trace(f"Got session cookie.")
231231
token_name = await _get_token_name(request)
232232
if token_name in session:
233233
stored_session_token = session[token_name]
234-
logger.debug(f"Found token in session cookie, validating...")
234+
logger.trace(f"Found token in session cookie, validating...")
235235
return await _is_valid_token(stored_session_token, request)
236236

237-
logger.debug("Token not found in session cookie.")
237+
logger.trace("Token not found in session cookie.")
238238
return False
239239

240240

@@ -247,18 +247,18 @@ async def _is_valid_token_in_url_query(request):
247247
Returns:
248248
Boolean : True if valid token is found
249249
"""
250-
logger.debug("Checking for token in url query...")
250+
logger.trace("Checking for token in url query...")
251251
query_string = request.query_string
252-
logger.debug(f"url query parameters found:{query_string}")
252+
logger.trace(f"url query parameters found:{query_string}")
253253
if query_string:
254254
token_name = _get_token_name_for_http(request)
255255
parsed_token = parse_qs(request.query_string).get(token_name)
256256
if parsed_token:
257257
parsed_token = parsed_token[0]
258-
logger.debug("parsed_token from url query string.")
258+
logger.trace("parsed_token from url query string.")
259259
return await _is_valid_token(parsed_token, request)
260260

261-
logger.debug("Token not found in url query.")
261+
logger.trace("Token not found in url query.")
262262
return False
263263

264264

@@ -273,17 +273,17 @@ async def _is_valid_token_in_headers(request):
273273
Returns:
274274
Boolean : True if valid token is found
275275
"""
276-
logger.debug("Checking for token in request headers...")
276+
logger.trace("Checking for token in request headers...")
277277
headers = request.headers
278278
token_name = _get_token_name_for_http(request)
279279
if token_name in headers:
280-
logger.debug(f"Token found in headers: {token_name}")
280+
logger.trace(f"Token found in headers: {token_name}")
281281
is_valid_token = await _is_valid_token(headers[token_name], request)
282282
if is_valid_token:
283283
await _store_token_hash_into_session(request)
284284
return is_valid_token
285285

286-
logger.debug("Token not found in request headers.")
286+
logger.trace("Token not found in request headers.")
287287
return False
288288

289289

tests/unit/util/mwi/test_logger.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2020-2025 The MathWorks, Inc.
1+
# Copyright 2020-2026 The MathWorks, Inc.
22
"""This file tests methods present in matlab_proxy/util/mwi_logger.py"""
33

44
import logging
@@ -19,6 +19,7 @@ def test_get():
1919
logger = mwi_logger.get()
2020
# Okay to use hidden API for testing only.
2121
assert logger.name == mwi_logger.__get_mw_logger_name()
22+
assert hasattr(logger, "trace"), "Logger returned by get() should support trace()"
2223

2324

2425
def test_get_mw_logger_name():
@@ -58,9 +59,11 @@ def test_get_with_environment_variables(monkeypatch, tmp_path, reset_logger_hand
5859
@pytest.mark.parametrize(
5960
"log_level, expected_level",
6061
[
62+
("TRACE", mwi_logger.TRACE),
6163
("DEBUG", logging.DEBUG),
6264
("INFO", logging.INFO),
6365
("WARNING", logging.WARNING),
66+
("trace", mwi_logger.TRACE),
6467
("debug", logging.DEBUG),
6568
("info", logging.INFO),
6669
("warning", logging.WARNING),
@@ -89,3 +92,34 @@ def test_set_logging_configuration_unknown_logging_levels(
8992
assert (
9093
logger.isEnabledFor(logging.INFO) == True
9194
), "Error in initialising the default logger"
95+
96+
97+
def test_logger_has_trace_method(reset_logger_handlers):
98+
"""This test checks that the logger returned by get() has a trace() method"""
99+
logger = mwi_logger.get(init=True)
100+
assert hasattr(logger, "trace"), "Logger should have trace() method"
101+
102+
103+
def test_get_returns_custom_logger_instance(reset_logger_handlers):
104+
"""This test checks that get() returns the custom logger implementation."""
105+
logger = mwi_logger.get(init=True)
106+
assert isinstance(logger, mwi_logger.MwLogger)
107+
108+
109+
def test_trace_level_name_is_registered(reset_logger_handlers):
110+
"""This test checks that TRACE level has a registered logging name."""
111+
mwi_logger.get()
112+
assert logging.getLevelName(mwi_logger.TRACE) == "TRACE"
113+
114+
115+
def test_trace_logging_at_trace_level(monkeypatch, reset_logger_handlers, caplog):
116+
"""This test checks that trace() method logs at TRACE level when enabled"""
117+
env_names_list = mwi_logger.get_environment_variable_names()
118+
monkeypatch.setenv(env_names_list[0], "TRACE")
119+
120+
with caplog.at_level(mwi_logger.TRACE):
121+
logger = mwi_logger.get(init=True)
122+
logger.trace("Test trace message")
123+
124+
assert "Test trace message" in caplog.text
125+
assert caplog.records[0].levelname == "TRACE"

0 commit comments

Comments
 (0)