Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Supports structured logging. #444

Open
wants to merge 11 commits into
base: master
Choose a base branch
from

Conversation

TlexCypher
Copy link
Contributor

@TlexCypher TlexCypher commented Feb 22, 2025

Source issue.

#124 (Thank you for @hirosassa )

Summary

This PR implements SlogConfig, which supports switch functionalities structured logging and plain text logging.
Of course, I confirmed end-to-end workflow with samples on provided issue, then structured logging and plain text logging works well.

Changes

Added SlogConfig and its tests.
If user export ${GOKART_LOGGING_FORMAT}=json, all logs are dumped as json, structured logging for machine.
On the other hand, the environment variables, ${GOKART_LOGGING_FORMAT}=text, all logs are dumped as plain text, for human. And also, no supported value is set, raise exception.
Also, we need user friendly interface, so implemented decorator function. So user import gokart.getLoggert instead of logging.getLogger.

Potential Discussion Points

The interface of logging might be the important discussion point.

This is my first OSS contribution, so please let me know how my code will be improved!

@TlexCypher TlexCypher changed the title Support structured logging. Supports structured logging. Feb 22, 2025
@hirosassa
Copy link
Collaborator

@TlexCypher Thanks for your contribution! Could you check the CI errors?

@TlexCypher
Copy link
Contributor Author

TlexCypher commented Feb 24, 2025

@hirosassa Thank you for reply.
By the way, of cource, I should work on fixing CI errors as you said, but I think current implementation is not following Python standard when library provides logging configration, because of adding handlers. So, I will convert this PR to draft.
I work on both fixing CI errors and new implementations, so wait for a new code, please.

@TlexCypher TlexCypher marked this pull request as draft February 24, 2025 05:28
@TlexCypher
Copy link
Contributor Author

TlexCypher commented Feb 24, 2025

Current work

I'm working on fix CI errors.
Failure is ocurred on TestTestFrameworkForPandasDataFrame::test_run_with_error and TestTestFrameworkForPandasDataFrame.test_run_with_namespace when execute tox -e py39, but when I execure tox -e py312, all tests have passed.
And also, I create Python3.9 virtual env with uv, and run target test, tests failed on tox with python 3.9 has passed.
Do you know how to reproduce tox environment? I wanna fix these CI errors on the tox env, with py3.9.

Error Content

[gw12] darwin -- Python 3.9.6 /Users/araki/personal/gokart/.tox/py39/bin/python

self = <test.testing.test_run_with_empty_data_frame.TestTestFrameworkForPandasDataFrame testMethod=test_run_with_namespace>

    def test_run_with_namespace(self):
        argv = [
            f'{__name__}.DummyWorkFlowWithoutError',
            '--local-scheduler',
            '--test-run-pandas',
            f'--test-run-namespace={__name__}',
            '--log-level=CRITICAL',
            '--no-lock',
        ]
        logger = logging.getLogger('gokart.testing.check_if_run_with_empty_data_frame')
        with patch.object(logger, 'info') as mock_debug:
            with self.assertRaises(SystemExit) as exit_code:
                gokart.run(argv)
>       log_str = mock_debug.call_args[0][0]
E       TypeError: 'NoneType' object is not subscriptable

test/testing/test_run_with_empty_data_frame.py:102: TypeError




```python
_______________________________________ TestTestFrameworkForPandasDataFrame.test_run_with_error _______________________________________
[gw12] darwin -- Python 3.9.6 /Users/araki/personal/gokart/.tox/py39/bin/python

self = <test.testing.test_run_with_empty_data_frame.TestTestFrameworkForPandasDataFrame testMethod=test_run_with_error>

    def test_run_with_error(self):
        argv = [f'{__name__}.DummyWorkFlowWithError', '--local-scheduler', '--test-run-pandas', '--log-level=CRITICAL', '--no-lock']
        logger = logging.getLogger('gokart.testing.check_if_run_with_empty_data_frame')
        with patch.object(logger, 'info') as mock_debug:
            with self.assertRaises(SystemExit) as exit_code:
                gokart.run(argv)
>       log_str = mock_debug.call_args[0][0]
E       TypeError: 'NoneType' object is not subscriptable

test/testing/test_run_with_empty_data_frame.py:85: TypeError

This contribution is related logging, also from error content, in both cases, the source of problem is log_str is None.

How to reproduce

Executre following command on this branch(support-slog)
$tox -e py39 # [Expected] Exact two cases might fail.
$tox -e py312 # [Expected] All testcases might pass.

If you are familiar with this kind of errors, way to reproduce on tox env, please tell me.
I'm still working on fixing CI errors.

@hirosassa
Copy link
Collaborator

In my local environment, all of the tox tests run successfully 🤔
I'll take a look this deeply.

@TlexCypher
Copy link
Contributor Author

TlexCypher commented Mar 2, 2025

Hi, @hirosassa .
I found the reason why CI errors are not fixed, and success to fix it.
So, I'll re-open this PR.
Please review it.
Sorry for late response.
Below is the description, how I found the way to fix CI errors.

@TlexCypher TlexCypher marked this pull request as ready for review March 2, 2025 12:36
@TlexCypher
Copy link
Contributor Author

The reason why CI errors were occured.

By disabling parallel test execution, I found that the test failures became consistent, with the same test failing every time.
Additionally, as expected, there were no test failures on the master branch.

Based on these two points, I suspected that my newly added implementation and test might have somehow broken the Logger configuration.
Upon inspecting the source file of the failing test, test/testing/test_run_with_empty_data_frame.py, I realized that it was reusing the test Logger configuration defined in gokart/testing/check_if_run_with_empty_data_frame.py.

In the implementation I added in test/test_slog_config.py, I had removed the existing Logger configuration. This caused the failure because gokart/testing/check_if_run_with_empty_data_frame.py could no longer reuse the predefined Logger configuration.

The reason the test initially appeared flaky was that, when running tests in parallel, the slog test would sometimes run after test/testing/test_run_with_empty_data_frame.py, in which case the removal of the existing Logger configuration did not cause a failure. This made it seem flaky.

After applying the fix, I confirmed that the tests now pass consistently, regardless of whether the parallel execution option is enabled or not.

Copy link
Collaborator

@hirosassa hirosassa left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the detailed investigation.


class SlogConfig(object):
"""
LoggerConfig is for logging configuration, Utility-class.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
LoggerConfig is for logging configuration, Utility-class.
SlogConfig is for logging configuration, Utility-class.

On the other hand, set logging configuration as plain text.
"""
logger_mode = os.environ.get('GOKART_LOGGER_FORMAT')
if not logger_mode or logger_mode.lower() == 'json':
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To keep default behavior (especially for current users), if the GOKART_LOGGER_FORMAT is not set, I prefer the log format should be text.
What do you think about this idea?

Copy link
Contributor Author

@TlexCypher TlexCypher Mar 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my opinion, structured logging is more useful than text simple logging as metrics, so default logging format should be structured logging.
If user wanna change log format, user should change GOKART_LOGGER_FORMAT, or write logging.ini, logging configuration file.
But you bet, I can understand your opinion, for current user, it might be breaking changes, so avoid to make slog configuration default.
As I checked, if user write their original logging configuration, that configuration is prioritized.
In conclusion, both are acceptable, and I follow maintainers opinion.

pyproject.toml Outdated
@@ -26,6 +26,10 @@ dependencies = [
"dill",
"backoff",
"typing-extensions>=4.11.0; python_version<'3.13'",
"python-json-logger>=3.2.1",
"notebook>=7.3.2",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is unnecessary dependency. Could you please remove this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

python-json-logger is necessary for adding JsonFormatter. But notebook is not, so removed.

pyproject.toml Outdated
Comment on lines 31 to 32
"tox>=4.24.1",
"tox-uv>=1.25.0",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These dependencies are unnecessary for production use (only for tests). Could you move these items to test dependecy-group?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this dependency is not necessary even for test.
I made a mistake, add tox and tox-uv...
So, remove this.

default_date_format = '%Y/%m/%d %H:%M:%S'

@staticmethod
def apply_slog_format(logger):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMO] This function selects log format and returns logger with the format. So there's the situation that the logger format is not "structured".
We should change the name of this function to such as apply_log_format, switch_log_format, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. I adopt swich_log_format from your suggestion. thx.

Comment on lines 52 to 54
last_resort_handler = logging.lastResort
if not last_resort_handler:
last_resort_handler = logging.StreamHandler()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
last_resort_handler = logging.lastResort
if not last_resort_handler:
last_resort_handler = logging.StreamHandler()
last_resort_handler = logging.lastResort if logging.lastResort is not None else logging.StreamHandler()

Comment on lines 71 to 75
# plain text mode, so nothing is applied.
elif logger_mode.lower() == 'text':
return logger
else:
raise Exception(f'Unknown logger format: {logger_mode}')
Copy link
Collaborator

@hirosassa hirosassa Mar 2, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These elif and else block is short. Let's use "early return" on the start part of this function to improve readability.

@hiro-o918
Copy link
Contributor

[IMO]

It seems that providing built-in logging functionality for first-party use may not fall within the scope of Gokart, as users are typically expected to choose their own logging libraries and formats.

However, allowing Gokart (or Luigi) to switch log formats could be quite helpful, as configuring this can be a bit challenging for users. It might be worth considering an option that simplifies this customization without enforcing a specific logging solution.

What do you think?

@TlexCypher
Copy link
Contributor Author

TlexCypher commented Mar 2, 2025

[IMO]

It seems that providing built-in logging functionality for first-party use may not fall within the scope of Gokart, as users are typically expected to choose their own logging libraries and formats.

However, allowing Gokart (or Luigi) to switch log formats could be quite helpful, as configuring this can be a bit challenging for users. It might be worth considering an option that simplifies this customization without enforcing a specific logging solution.

What do you think?

Thank you for thoughtful and stimulating comments.
Hmm, as u said, built-in structured logging functionalities is not scope for gokart, because gokart is just a ML pipeline library.
So, my implementation is following this thought.
If user writes logging.ini as usual, that configuration is prioritized.

And also, I think gokart should support structured logging as default.
Many programming languages and libraries support structured logging feature recent years.
For example golang, at first, standard organization did't support structured logging, so zap, zerolog, many structured logging libraries were developed by community.
As you seen in golang history, structured logging demand is growing, and especially for ML, we need the way to handle and gather bunch of parameters and logs easily.
So in my opinion, some kind of specific logging solution is blazingly important especially for ML libraries, but at the same time, flexibility is also important, so my implementation is following that.

@TlexCypher TlexCypher requested a review from hirosassa March 2, 2025 16:17
@hiro-o918
Copy link
Contributor

@TlexCypher
I understand.
You mean user have a choices to use gokart.getlooger or anther one, don't you?

By the way, python-json-logger is archived.... 😢
So we should omit this dependencies.

https://github.com/madzak/python-json-logger

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants