Skip to content

Fix the log levels mapping #597

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

Open
wants to merge 3 commits into
base: devel
Choose a base branch
from
Open

Fix the log levels mapping #597

wants to merge 3 commits into from

Conversation

Jakuje
Copy link
Contributor

@Jakuje Jakuje commented May 23, 2024

SUMMARY

The libssh provides the most verbose logging with SSH_LOG_TRACE, which was mapped to logging.CRITICAL, causing the users being unable to get full debug logs. These are critical for libssh developers to be able to investigate issues.

ISSUE TYPE
  • Bugfix Pull Request

Jakuje added a commit to Jakuje/ansible.netcommon that referenced this pull request May 23, 2024
The `INFO` log level is usually not enough to get useful information from the libssh regarding the low-level issues that we would like to be able to resolve. This can be made even more fine grained, but I do not think less verbose logs would be much useful anyway.

Related is the issue in pylibssh, which maps the DEBUG verbosity to something that is not debug in libssh itself:

ansible/pylibssh#597
@webknjaz
Copy link
Member

It'd be useful to have tests in such PRs. I'm unable to verify what this patch affects without them.

Copy link

Congratulations! One of the builds has completed. 🍾

You can install the built RPMs by following these steps:

  • sudo yum install -y dnf-plugins-core on RHEL 8
  • sudo dnf install -y dnf-plugins-core on Fedora
  • dnf copr enable packit/ansible-pylibssh-597
  • And now you can install the packages.

Please note that the RPMs should be used only in a testing environment.

@psf-chronographer psf-chronographer bot added the bot:chronographer:provided There is a change note present in this PR label Jun 21, 2024
@Jakuje
Copy link
Contributor Author

Jakuje commented Aug 29, 2024

@webknjaz the current version should be working version with tests of what, I hope, you were trying to describe. I think it will need some polishing so I would be happy for your inputs.

Copy link

Copy link

Copy link
Member

@webknjaz webknjaz left a comment

Choose a reason for hiding this comment

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

I wrote most of the review months ago. Submitting just now. This might need a few more iterations. Hopefully, a few things are actionable immediately.

const char *function,
const char *buffer,
void *userdata) noexcept nogil:
with gil:
Copy link
Member

Choose a reason for hiding this comment

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

Will this be problematic in free-threading builds of CPython?

@Jakuje
Copy link
Contributor Author

Jakuje commented Apr 11, 2025

Turns out that also the logging.NOTSET is misplaced. In python it means "ask above"/"log everything" but in libssh the currently mapped value libssh.SSH_LOG_NONE means do not log anything, which is quite the opposite so my suggestion would be to move this to logging.WARNING which is probably the closest ...

@Jakuje Jakuje force-pushed the patch-1 branch 3 times, most recently from 808a4aa to 2c034b8 Compare April 11, 2025 09:13
Copy link

codecov bot commented Apr 11, 2025

Welcome to Codecov 🎉

Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.

Thanks for integrating Codecov - We've got you covered ☂️

@webknjaz
Copy link
Member

webknjaz commented Apr 24, 2025

Turns out that also the logging.NOTSET is misplaced. In python it means "ask above"/"log everything" but in libssh the currently mapped value libssh.SSH_LOG_NONE means do not log anything, which is quite the opposite so my suggestion would be to move this to logging.WARNING which is probably the closest ...

If you want a "log nothing", I'd probably set the threshold high in the mapping. Something like 55 or 60, or even 99:

$ python
Python 3.13.2 (main, Feb 28 2025, 20:34:46) [GCC 14.2.1 20241221] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import logging
>>> logging.getLevelNamesMapping()
{'CRITICAL': 50, 'FATAL': 50, 'ERROR': 40, 'WARN': 30, 'WARNING': 30, 'INFO': 20, 'DEBUG': 10, 'NOTSET': 0}
>>> logging.fatal
<function fatal at 0x7fa77ea62fc0>
>>> logging.CRITICAL
50
>>> logging.FATAL
50
>>> logging.ERROR
40

P.S. I wonder if float('inf') would be allowed since infinity is semantically what you're after...

if level not in LOG_MAP:
raise LibsshSessionException(f'Invalid log level [{level:d}]')

rc = libssh.ssh_set_log_level(LOG_MAP[level])
Copy link
Member

Choose a reason for hiding this comment

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

Should this special-case NOTSET and just skip calling 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.

It is not clear to me as the libssh mapping and python is that different.

I understand the NOTSET value that we should log everything and let the underlying code handle that. If we would skip this call on NOTSET, we would just keep the previously set log level, which might be TRACE, NONE, or anything in between, which does not sound right to me.

Copy link
Member

Choose a reason for hiding this comment

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

@Jakuje so the way the logging infrastructure works in python is that the loggers are called throughout the application code with explicit levels. And then, there's a level of logging set on the logger, which allows disregarding messages below the set level (this is simple integer comparison). But then, there's also handlers of which there can be many. And you can have a handler for stdout, stderr, some file and some network logging service. So a logged message that cleared the first gate (the logger log level) goes into all of the handlers which would have their own logging levels as well. And the handlers can be set up to output a debug log file and a normal log file + normal stderr + critical-only messages going to a network server (as an example).

logging.NOTSET is 0. Meaning that if a logger has logging.NOTSET, no messages are going to be disregarded “on entry”. This is a semantic “log everything”.

>>> import logging
>>> logging.NOTSET
0

I should note, though, that loggers have hierarchy. And so there's a "root logger" and "child" loggers. When a logger has a level of logging.NOTSET, the actual level is being computed by traversing the parent relationships. And only if the root logger has it as logging.NOTSET, this value of 0 is used (otherwise, some other value may end up being considered as the current logging level). logging.NOTSET is not the default of the root logger, logging.WARNING is.

So I'd say logging.NOTSET may be considered a special-cased sentinel in some contexts.

},
}

logger = logging.getLogger("libssh")
Copy link
Member

Choose a reason for hiding this comment

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

Should this be called libssh? or perhaps

Suggested change
logger = logging.getLogger("libssh")
_logger = logging.getLogger('ansible-pylibssh')

?
Should we be exposing this _logger object to the end-users or just have it internal?

Copy link
Member

Choose a reason for hiding this comment

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

What if we initialize the logger object in session.pyx (module-level or as an instance on Session) and then pass it to set_level() as an argument instead of having a global thing here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed, it should be internal. I think the problem was accessing the logger from the callback _pylibssh_log_wrapper(), which gets called by the libssh. I probably did not want to mess with passing it as a userdata, but it should be an option. Let me have a look into that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, the thing is that the logging is really not tied to the session so if we allocate the logger in session and then free it we might crash, which is why I went with the global.
The other option would be to unset the callback + userdata when session is freed, but it would still be mess if more sessions would get allocated in one thread.

So the only viable option I see could creating some singleton Logging object, encapsulating the logging context, if you prefer.

Copy link
Member

Choose a reason for hiding this comment

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

I think it's okay to have it global, just have it underscored so that it doesn't look like public API.

@Jakuje Jakuje force-pushed the patch-1 branch 3 times, most recently from 6c229ee to 7841ba9 Compare April 28, 2025 15:54
@Jakuje
Copy link
Contributor Author

Jakuje commented Apr 28, 2025

Ugh. Is the ubi/fedora RPM builds still/again broken? This does not look like an issue in this PR if I see right:

ImportError while loading conftest '/github/home/rpmbuild/BUILD/ansible-pylibssh-1.2.3.dev189+g6432716/tests/conftest.py'.
tests/conftest.py:16: in <module>
    from pylibsshext.logging import ANSIBLE_PYLIBSSH_TRACE
E   ImportError: /github/home/rpmbuild/BUILDROOT/python-ansible-pylibssh-1.2.3.dev189+g6432716-1.el9.x86_64/usr/lib64/python3.9/site-packages/pylibsshext/logging.cpython-39-x86_64-linux-gnu.so: undefined symbol: ssh_set_log_level

@webknjaz
Copy link
Member

Not sure about the RPMs but the test job is slow on Python 3.8: https://github.com/ansible/pylibssh/actions/runs/14712178586/job/41287439642?pr=597#step:16:696

@webknjaz
Copy link
Member

Building things under QEMU was slow and timed out too https://github.com/ansible/pylibssh/actions/runs/14712178586/job/41287253557?pr=597

@webknjaz
Copy link
Member

Ugh. Is the ubi/fedora RPM builds still/again broken? This does not look like an issue in this PR if I see right:

ImportError while loading conftest '/github/home/rpmbuild/BUILD/ansible-pylibssh-1.2.3.dev189+g6432716/tests/conftest.py'.
tests/conftest.py:16: in <module>
    from pylibsshext.logging import ANSIBLE_PYLIBSSH_TRACE
E   ImportError: /github/home/rpmbuild/BUILDROOT/python-ansible-pylibssh-1.2.3.dev189+g6432716-1.el9.x86_64/usr/lib64/python3.9/site-packages/pylibsshext/logging.cpython-39-x86_64-linux-gnu.so: undefined symbol: ssh_set_log_level

They weren't broken on the last commit to devel 4 days ago.
Back then, F40 used libssh-devel 0.10.6-5.fc40: https://github.com/ansible/pylibssh/actions/runs/14667486098/job/41165904433#step:16:22.
And UBI 9.0.0 used libssh-devel 0.10.4-13.el9: https://github.com/ansible/pylibssh/actions/runs/14667486098/job/41165904860#step:16:33

The UBI 9 job in this PR uses libssh-devel 0.10.4-13.el9 as well. So there's no difference there.

It's a mystery, so far. Can it be that CFLAGS / LDFLAGS is being broken in the upstream images? I'm not sure.

@webknjaz
Copy link
Member

And the container images are the same too.

F40: sha256:3c86d25fef9d2001712bc3d9b091fc40cf04be4767e48f1aa3b785bf58d300ed
UBI9: sha256:9db17fd5d0bbf8797d65d42c039f3a20ce9621d989cc173e3c616fa5e8588115

@webknjaz
Copy link
Member

@Jakuje The RPM jobs are not failing in other PRs, so this is related to something you changed here.

@Jakuje
Copy link
Contributor Author

Jakuje commented May 1, 2025

@Jakuje The RPM jobs are not failing in other PRs, so this is related to something you changed here.

Interesting. Locally it works, in other jobs it works too but we got import error on RPM build. This is not even a new symbol being used, the ssh_set_log_level() is here at least since libssh 0.8.0. Could it be that this build tries to import some older version of the pylibsshext in this case?

@Jakuje
Copy link
Contributor Author

Jakuje commented May 1, 2025

So it looks like the issue is the pyx file without the related pxd one. I removed the empty pxd in some of the previous iterations, which is probably the time when this started failing.

Adding just an empty file logging.pxd solves the issue (but raises warnings obviously). Given how few information is now left in the logging module, I would even consider merging it into the session or moving the constants to the pxd. What would you suggest to be the cleanest way?

@webknjaz
Copy link
Member

I wonder why it works for _libssh_version.pyx, then… Anyway, _logging should probably remain separate.

@Jakuje
Copy link
Contributor Author

Jakuje commented May 13, 2025

I wonder why it works for _libssh_version.pyx, then… Anyway, _logging should probably remain separate.

AFAIK the difference is in how the libssh_version module is not used by external users/tests, but the constant is loaded only from _version.pyx as from ._libssh_version import ..., while we want to keep the logging public and therefore loadable by users, which requires adjacent pxd file. I found some notes describing this but forgot to save them.

Jakuje added 3 commits May 13, 2025 22:51
The libssh provides the most verbose logging with SSH_LOG_TRACE, which
was not mapped to any of the standard values so the users are unable
to get full debug logs. These are critical for libssh developers to be
able to investigate issues.

Signed-off-by: Jakub Jelen <[email protected]>
Signed-off-by: Jakub Jelen <[email protected]>
Signed-off-by: Jakub Jelen <[email protected]>
Copy link

@Jakuje
Copy link
Contributor Author

Jakuje commented May 13, 2025

Sounds like now we have only the python 3.12 tests failing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bot:chronographer:provided There is a change note present in this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants