Logging
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.
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
- CDK v0.13.0 or higher
- A Connector, check out our Installation Guide to create a new Connector project with our
connector-factorycookiecutter and the Connector Walkthrough to get started writing your own Connector.
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.
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)