Logging Configuration#

Overview#

pyxcp uses Python’s standard logging module with a NullHandler by default to avoid interfering with your application’s logging configuration.

This follows Python’s logging best practices for libraries: libraries should never configure logging, only applications should.

Default Behavior#

By default, pyxcp produces no log output:

from pyxcp.cmdline import ArgumentParser

ap = ArgumentParser(description="Silent by default")
with ap.run() as xcp:
    xcp.connect()
    # No logging output unless you configure it
    xcp.disconnect()

This ensures pyxcp doesn’t interfere with your own logging setup.

Logger Hierarchy#

pyxcp uses a hierarchical logger structure:

pyxcp                         # Root logger (NullHandler)
├── pyxcp.master              # Master class logs
│   └── pyxcp.master.errorhandler  # Error handling logs
├── pyxcp.transport           # Transport layer logs
├── pyxcp.daq_stim            # DAQ/STIM logs
└── pyxcp.recorder.converter  # Recorder logs

All loggers inherit from the pyxcp root logger, so configuring the root affects all child loggers.

Enabling Logging#

Method 2: Standard Python Logging#

Configure using Python’s standard logging:

import logging

# Configure root logger for your application
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(name)s] %(levelname)s: %(message)s"
)

# pyxcp logs will now appear with 'pyxcp.*' names
from pyxcp.cmdline import ArgumentParser
ap = ArgumentParser(description="Standard logging")
with ap.run() as xcp:
    xcp.connect()
    xcp.disconnect()

Method 3: File Logging#

Log to file instead of console:

import logging

# Create file handler
handler = logging.FileHandler("pyxcp_session.log", mode="w")
formatter = logging.Formatter(
    "[%(asctime)s] [%(name)s] %(levelname)s: %(message)s"
)
handler.setFormatter(formatter)

# Configure pyxcp logger
pyxcp_logger = logging.getLogger("pyxcp")
pyxcp_logger.addHandler(handler)
pyxcp_logger.setLevel(logging.DEBUG)

# All pyxcp activity logged to file
from pyxcp.cmdline import ArgumentParser
ap = ArgumentParser(description="File logging")
with ap.run() as xcp:
    xcp.connect()
    xcp.disconnect()

Method 4: Selective Module Logging#

Enable logging for specific pyxcp modules only:

import logging

logging.basicConfig(level=logging.WARNING)  # App default: warnings only

# But enable DEBUG for transport layer only
transport_logger = logging.getLogger("pyxcp.transport")
transport_logger.setLevel(logging.DEBUG)

# Now only transport logs at DEBUG, rest at WARNING
from pyxcp.cmdline import ArgumentParser
ap = ArgumentParser(description="Selective logging")
with ap.run() as xcp:
    xcp.connect()  # Transport DEBUG logs appear
    xcp.disconnect()

Advanced Configuration#

Multiple Handlers#

Send logs to multiple destinations:

import logging

pyxcp_logger = logging.getLogger("pyxcp")
pyxcp_logger.setLevel(logging.DEBUG)

# Console handler (INFO and above)
console = logging.StreamHandler()
console.setLevel(logging.INFO)
console.setFormatter(logging.Formatter("%(levelname)s: %(message)s"))
pyxcp_logger.addHandler(console)

# File handler (DEBUG and above)
file_handler = logging.FileHandler("pyxcp_debug.log")
file_handler.setLevel(logging.DEBUG)
file_handler.setFormatter(logging.Formatter(
    "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
))
pyxcp_logger.addHandler(file_handler)

# INFO+ to console, DEBUG+ to file

Custom Formatting#

Use custom log format for pyxcp:

import logging

# Custom format with milliseconds and function name
formatter = logging.Formatter(
    "%(asctime)s.%(msecs)03d [%(name)s:%(funcName)s] %(levelname)s: %(message)s",
    datefmt="%H:%M:%S"
)

handler = logging.StreamHandler()
handler.setFormatter(formatter)

pyxcp_logger = logging.getLogger("pyxcp")
pyxcp_logger.addHandler(handler)
pyxcp_logger.setLevel(logging.DEBUG)

Integration with Existing Loggers#

If your application already has logging configured, pyxcp will inherit it:

import logging

# Your application's logging setup
logging.basicConfig(
    level=logging.INFO,
    format="[%(levelname)s] %(name)s: %(message)s",
    handlers=[
        logging.FileHandler("app.log"),
        logging.StreamHandler()
    ]
)

# pyxcp logs will automatically use your configuration
from pyxcp.cmdline import ArgumentParser
ap = ArgumentParser(description="Integrated logging")
with ap.run() as xcp:
    xcp.connect()  # Logs to app.log and console
    xcp.disconnect()

Logging Levels#

pyxcp uses standard Python logging levels:

Level

Usage

DEBUG

Detailed diagnostic information (frame contents, state transitions)

INFO

General informational messages (connection established, DAQ started)

WARNING

Something unexpected but not critical (timeout, retry)

ERROR

Error occurred but operation continues (command failed)

CRITICAL

Serious error, operation cannot continue

Recommended levels:

  • Production: WARNING or ERROR only

  • Development: INFO for general visibility

  • Debugging: DEBUG for detailed protocol traces

Troubleshooting#

“No logs appearing”#

  1. Check handler is configured:

    import logging
    pyxcp_logger = logging.getLogger("pyxcp")
    print(f"Handlers: {pyxcp_logger.handlers}")  # Should not be empty
    
  2. Check log level:

    import logging
    pyxcp_logger = logging.getLogger("pyxcp")
    print(f"Level: {logging.getLevelName(pyxcp_logger.level)}")
    
  3. Check propagation:

    import logging
    pyxcp_logger = logging.getLogger("pyxcp")
    print(f"Propagate: {pyxcp_logger.propagate}")  # Should be True
    

“ValueError: I/O operation on closed file” (Issue #176)#

Cause: Your application’s file handler was closed while pyxcp tried to log.

Solution: Ensure proper shutdown order:

import logging

# Your logging setup
file_handler = logging.FileHandler("myapp.log")
logger = logging.getLogger("myapp")
logger.addHandler(file_handler)

# Use pyxcp
from pyxcp.cmdline import ArgumentParser
with ap.run() as xcp:
    xcp.connect()
    xcp.disconnect()

# Close file handler AFTER pyxcp is done
file_handler.close()
logger.removeHandler(file_handler)

“Logs duplicated”#

Cause: Multiple handlers registered or propagation issues.

Solution: Clear existing handlers before reconfiguring:

import logging

pyxcp_logger = logging.getLogger("pyxcp")

# Clear existing handlers
for handler in pyxcp_logger.handlers[:]:
    pyxcp_logger.removeHandler(handler)

# Add your handler
new_handler = logging.StreamHandler()
pyxcp_logger.addHandler(new_handler)

Examples#

Minimal Debug Session#

from pyxcp.logger import setup_logging
from pyxcp.cmdline import ArgumentParser
import logging

# Quick debug setup
setup_logging(level=logging.DEBUG)

ap = ArgumentParser(description="Debug session")
with ap.run() as xcp:
    xcp.connect()
    result = xcp.fetch(0x1000, 4)
    print(f"Data: {result.hex()}")
    xcp.disconnect()

Production with File Rotation#

import logging
from logging.handlers import RotatingFileHandler

# Rotating file handler (10 MB max, 5 backups)
handler = RotatingFileHandler(
    "pyxcp.log",
    maxBytes=10*1024*1024,
    backupCount=5
)
formatter = logging.Formatter(
    "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
)
handler.setFormatter(formatter)

pyxcp_logger = logging.getLogger("pyxcp")
pyxcp_logger.addHandler(handler)
pyxcp_logger.setLevel(logging.WARNING)  # Production: warnings only

Multi-Application Integration#

import logging

# Application logger
app_logger = logging.getLogger("myapp")
app_logger.setLevel(logging.INFO)

# pyxcp logger (child of root)
pyxcp_logger = logging.getLogger("pyxcp")
pyxcp_logger.setLevel(logging.WARNING)  # Less verbose than app

# Shared handler
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter(
    "%(asctime)s [%(name)s] %(levelname)s: %(message)s"
))

# Add to both
app_logger.addHandler(handler)
# pyxcp inherits from root, so configure root too
logging.root.addHandler(handler)
logging.root.setLevel(logging.INFO)

FAQ#

Q: Why doesn’t pyxcp log anything by default?

A: Following Python best practices, libraries should not configure logging. Only applications should. This prevents pyxcp from interfering with your logging setup.

Q: How do I enable verbose logging for debugging?

A: Use setup_logging(level=logging.DEBUG) from pyxcp.logger.

Q: Can I use different log levels for different pyxcp modules?

A: Yes! Configure loggers like logging.getLogger("pyxcp.transport").setLevel(logging.DEBUG).

Q: Does pyxcp use print() statements?

A: No. All output uses proper logging (except for CLI tools which use rich.console).

Q: Can I disable logging completely?

A: It’s already disabled by default (NullHandler). If you see logs, it’s because you or another library configured logging.

Q: How do I log to syslog/network/database?

A: Use Python’s standard logging handlers (SysLogHandler, SocketHandler, etc.) with the pyxcp logger.

References#

Issue #176: User logging configuration conflict

Python logging best practices for libraries:

  • Use NullHandler by default

  • Never call logging.basicConfig() in library code

  • Use hierarchical logger names

  • Let applications configure logging