Structured logging with Robot Framework and structlog

#robot framework#python

27 June 2025

By default, Robot Framework captures all console output (stdout) and includes it in its log file. This means that simple logging statements or even print() calls will appear in the Robot Framework logs automatically.

Structured logging

But I like to use more advanced structured logging via a library called structlog. It allows storing custom key-value pairs and additional metadata next to the basic log event message.

I had some problems setting this up. So here are some notes in you case you plan to do something similar:

My solution involves a custom structlog processor which send log events directly to Robot Frameworks. Structlog itself will log to the stderr console output so logs do not get captured twice. This gives you full control over which details Robot Framework receives, while the console output remains independently configurable.

You can find the complete code in my git repository python-ideas (links to fixed commit). Below I will explain the relevant parts.

Configure structlog

In the past I created a custom processor for structlog which adds information about file and line number to the console output. This way you can click on the log output and jump to the exact location in your code editor.

Now I extended this configuration with a new processor for Robot Framework:

python-ideas/robotframework-tips/robotframework_tips/utils/structlog_utils.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
# https://github.com/hynek/structlog/issues/546#issuecomment-1741258252
import logging
import structlog
from pprint import pprint

import sys
import json

from structlog.typing import EventDict, WrappedLogger


class RobotLog:

    def __init__(self):

        try:
            # only import robotframework dependencies in case robot logger gets used
            from robot.api import logger

            self.robot_logger = logger
        except:
            print("you must install robotframework to use this feature")
            raise

    def __call__(
        self, logger: WrappedLogger, name: str, event_dict: EventDict
    ) -> EventDict:

        copied_event_dict = dict(event_dict)
        level = copied_event_dict.pop("level", None)
        event = copied_event_dict.pop("event", None)

        logger_func = self.robot_logger.debug

        match level:
            case "debug":
                logger_func = self.robot_logger.debug
            case "info":
                logger_func = self.robot_logger.info
            case "warning":
                logger_func = self.robot_logger.warn
            case "error":
                logger_func = self.robot_logger.error
            case _:
                print("unexpected log level", level)
                logger_func = self.robot_logger.debug

        msg = event

        if copied_event_dict:
            try:
                attributes_string = json.dumps(copied_event_dict, ensure_ascii=False)
            except TypeError:
                attributes_string = repr(copied_event_dict)

            msg += f" | {attributes_string}"

        logger_func(msg=msg, html=False)

        return event_dict


class LogJump:
    def __init__(
        self,
        full_path: bool = False,
    ) -> None:
        self.full_path = full_path

    def __call__(
        self, logger: WrappedLogger, name: str, event_dict: EventDict
    ) -> EventDict:
        if self.full_path:
            file_part = event_dict.pop("pathname")
        else:
            file_part = event_dict.pop("filename")

        # leading space improves link recognition in vscode (but spaces in paths still do not work always)
        event_dict["_l"] = f' {file_part}:{event_dict.pop("lineno")}'

        return event_dict


def configure_structlog(
    colors: bool = True, full_path: bool = False, robotframework_logger: bool = False
):
    """configure structlog with useful defaults and additional features

    Args:
        colors (bool, optional): create colorful logs. Defaults to True.
        full_path (bool, optional): location entry contains full path instead of only file name. Defaults to False.
        robotframework_logger (bool, optional): send logs to robotframework log output (requires robotframework to be installed and used). Defaults to False.
    """

    processors = [
        structlog.contextvars.merge_contextvars,
        structlog.processors.add_log_level,
        structlog.processors.StackInfoRenderer(),
        structlog.dev.set_exc_info,
    ]
    if robotframework_logger:
        processors.append(RobotLog())

    call_site_parameters = [structlog.processors.CallsiteParameter.LINENO]
    # add either pathname or filename and then set full_path to True or False in LogJump below
    if full_path:
        call_site_parameters.append(structlog.processors.CallsiteParameter.PATHNAME)
    else:
        call_site_parameters.append(structlog.processors.CallsiteParameter.FILENAME)

    processors.extend(
        [
            structlog.processors.CallsiteParameterAdder(call_site_parameters),
            LogJump(full_path=full_path),
            structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S", utc=False),
            structlog.dev.ConsoleRenderer(colors=colors),
        ]
    )

    if robotframework_logger:
        # use stderr in case of robot framework so logs do not get captured twice
        logger_factory = structlog.PrintLoggerFactory(file=sys.stderr)
    else:
        # use stdout (default in case structlog gets use without robot framework)
        logger_factory = structlog.PrintLoggerFactory()
    structlog.configure(
        processors=processors,
        wrapper_class=structlog.make_filtering_bound_logger(logging.NOTSET),
        context_class=dict,
        logger_factory=logger_factory,
        cache_logger_on_first_use=False,
    )

As you can see structlog will invoke the robot framework logger. Additional key-value pairs of the structured log will be attached as json encoded string. At the end structlog will write its own output to stderr.

Structlog listener for Robot Framework

A listener can add new custom functionality to Robot Framework. The following listener runs the our previous configuration code for structlog on its initialization. It sets the argument robotframework_logger=True. That’s all! 😁

python-ideas/robotframework-tips/robotframework_tips/utils/robot_listeners/StructlogListener.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import structlog
from robotframework_tips.utils.structlog_utils import configure_structlog

log = structlog.stdlib.get_logger()


class StructlogListener:

    ROBOT_LIBRARY_SCOPE = "GLOBAL"
    ROBOT_LISTENER_API_VERSION = 3

    def __init__(self, colors: bool = True, full_path: bool = False):
        log.info("Configuring structlog for Robot Framework...")
        configure_structlog(
            colors=colors, full_path=full_path, robotframework_logger=True
        )

        log.info(
            "structlog is configure now. New logs will be sent to Robot Framework too."
        )

Use listener on test execution

Now you have to execute a test case while the StructlogListener is enabled. There are several ways to add a listener. It depends on how you run robot framework test cases.

robot cli command with listener argument

Example 1: with default arguments for structlog listener:

robot \
	--console "dotted" \
	--outputdir "output" \
	--listener "robotframework_tips.utils.robot_listeners.StructlogListener" \
	"robotframework_tips/examples/failing_test_with_exception/Failing Test.robot"

Example 2: manual arguments for structlog listener and an additional Exception Traceback Listener:

robot \
	--console "dotted" \
	--outputdir "output" \
	--listener "robotframework_tips.utils.robot_listeners.ExceptionTracebackListener;ERROR" \
	--listener "robotframework_tips.utils.robot_listeners.StructlogListener;true;false" \
	"robotframework_tips/examples/failing_test_with_exception/Failing Test.robot"

RobotCode (vscode extension)

robot.toml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# https://robotcode.io/02_get_started/configuration
# Basic settings
output-dir = "output"

# log-level = "NONE"
log-level = "INFO"
# log-level DEBUG (or TRACE) would show full traceback
# as debug level by default
# log-level = "DEBUG"
# log-level = "TRACE"

languages = ["en", "de"]
# console = "verbose"
# using dotted console output avoids strange line breaks
# between stdout and stderr output of the verbose version
console = "dotted"

# add your custom listeners here:
[listeners]
'robotframework_tips.utils.robot_listeners.ExceptionTracebackListener' = [
  'INFO', # exception detail log level
]
'robotframework_tips.utils.robot_listeners.StructlogListener' = [
  "true",  # colors
  "false", # full path (structlog)
]

Open Robot Framework logs

Check the Robot Framework log.html file.
It will contain your structlog logs with correct log levels and json encoded key-value pairs.

Related content