Skip to content

feat: add back the capturing of logs from logging#418

Open
bepri wants to merge 1 commit intofeature/oxidationfrom
work/rust-log-capture/CRAFT-4986
Open

feat: add back the capturing of logs from logging#418
bepri wants to merge 1 commit intofeature/oxidationfrom
work/rust-log-capture/CRAFT-4986

Conversation

@bepri
Copy link
Member

@bepri bepri commented Feb 13, 2026

  • Have you followed the guidelines for contributing?
  • Have you signed the CLA?
  • Have you successfully run make lint && make test?

This PR brings back Craft CLI's integration with the logging library. To do this, a shim class is defined in craft_cli/_logs.py in order to inherit from logging.Handler. This class wraps a LogListener class defined in Rust that actually parses logging events and sends them to the printer.

Here's a quick script for testing the feature:

#!/usr/bin/env python3
"""e."""
import logging
import subprocess

from craft_cli import EmitterMode
from craft_cli._rs.emitter import Emitter

logger = logging.getLogger(__name__)

e = Emitter("blah.log", EmitterMode.VERBOSE, "example.com", "Hello")

logger.info("Testing")
with e.open_stream() as pipe:
    for i in range(5):
        logger.debug("Printing %d", i)
        subprocess.run(["echo", str(i)], check=True, stdout=pipe)

e.message("Done")

I discovered a bug while testing where BRIEF verbosity will often clear one too many lines of your terminal, but it isn't related to these changes.

@bepri bepri requested a review from mr-cal February 13, 2026 20:31
@bepri bepri self-assigned this Feb 13, 2026
@bepri bepri requested a review from tigarmo as a code owner February 13, 2026 20:31
greeting: String,

/// A handle on log handling.
_log_handle: Option<Py<PyAny>>,
Copy link
Member Author

Choose a reason for hiding this comment

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

I keep this so Python doesn't deallocate it.

py: Python<'_>,
verbosity: Verbosity,
) -> PyResult<Option<Py<PyAny>>> {
let log_handler = match LogListener::new(py, verbosity)? {
Copy link
Member Author

Choose a reason for hiding this comment

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

If this returns None, it's because we decided that log capture wasn't necessary for this run's verbosity level.

#[pymethods]
impl LogListener {
fn emit(&mut self, py: Python<'_>, record: &Bound<'_, PyAny>) -> PyResult<()> {
let levelno = record.getattr(intern!(py, "levelno"))?;
Copy link
Member Author

Choose a reason for hiding this comment

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

This intern! macro is just a cheap optimization trick PyO3 provides. Instead of having to coerce the Rust "levelno" string into Python for every logging event, intern! keeps a copy of this string on Python's heap after the first call.

pub fn new(py: Python<'_>, verbosity: Verbosity) -> PyResult<Option<Self>> {
let res = match verbosity {
Verbosity::Quiet => None,
Verbosity::Brief => None,
Copy link
Member Author

Choose a reason for hiding this comment

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

This is separated out from Quiet because Brief mode can require log handling if streaming_brief is enabled, which will be the next PR.

@bepri bepri mentioned this pull request Feb 18, 2026
3 tasks
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.

1 participant