UniteLabs
Guides

Logging

How to emit logs from your connector code using the CDK and structlog.
This guide focuses specifically on logging usage within the CDK. It should be enough for most people to get started. For a base-level introduction to Python logging, read the Python standard library logging basic tutorial.

Logging is essential for understanding a connector's behavior while debugging and in production. The CDK provides a structured logging system built on top of structlog and Python's standard logging module. Out of the box a connector emits human-readable logs to the terminal, and in production it additionally writes JSON-structured logs to a rotating file.

This guide covers how to emit logs from your own connector code. For what a connector logs by default and how to customize its log output (levels, files, rotation), see Connect a device — From source.

To scaffold a config file pre-populated with the default logging presets, run config create with the --explicit-logging/-x flag (defaults to prod), e.g. uv run config create -x dev. See Customizing the logging configuration for the copyable available presets.

Prerequisites

Logging from your connector code

Wherever logging is required in a module of a connector, a logger can be created using:

from unitelabs.cdk import get_logger

logger = get_logger(__name__)

get_logger returns a named logger instance. __name__ is recommended to keep the logger names standard and clearly assigned to a module hierarchically by dotted path. Note that we call get_logger imported from the CDK and not logging.getLogger from Python's standard logging module.

Using Python's standard logging still works without any additional setup and will be merged automatically with the CDK's internal structlog logs. We recommend the CDK's get_logger to get access to all structlog features (more details below)

Whenever a module or class requires a lot of logging, consider exposing the logger as a property to make the code more readable. Consider for example a protocol class:

from unitelabs.bus import Protocol
from unitelabs.cdk import get_logger, BoundLogger


class DeviceProtocol(Protocol):
    def __init__(self, ...):
        ...

    @property
    def logger(self) -> BoundLogger:
        """A structured logger for this protocol."""
        return get_logger(__name__)

    async def execute(self, command: str) -> None:
        ...

    async def make_device_do_something(self) -> str:
        """Make the device do science."""
        try:
            await self.execute("start_science")
        except ValueError as exc:
            self.logger.error("Failed to run `start_science`.", exc_info=exc)
            raise

        self.logger.debug("Successfully ran `start_science`.")

Structlog features

Using the get_logger method from the CDK exposes a structlog bound logger. This means the following features are available:

Structured key-value context. Pass keywords alongside the message; they become first-class fields in the JSON output instead of being baked into a string:

logger.info("Command finished", command="start_science", duration_ms=42)
# JSON: {"event": "Command finished", "command": "start_science", "duration_ms": 42, ...}

Bound loggers. Pre-bind context that should appear on every subsequent log line from that logger:

log = logger.bind(device_id="A1B2")
log.info("connected")        # includes device_id
log.warning("timeout")       # includes device_id

Exceptions. Pass the exception via exc_info= to attach a formatted traceback:

try:
    ...
except ValueError as exc:
    logger.error("Validation failed", exc_info=exc)

Standard-library-style formatting also still works, so existing code needs no changes:

logger.info("Detected file change: %s", path)