robotframework-tracer 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ """Robot Framework Tracer - OpenTelemetry distributed tracing for Robot Framework."""
2
+
3
+ from .listener import TracingListener
4
+ from .version import __version__
5
+
6
+ __all__ = ["TracingListener", "__version__"]
@@ -0,0 +1,108 @@
1
+ class RFAttributes:
2
+ """OpenTelemetry semantic conventions for Robot Framework."""
3
+
4
+ # Suite attributes
5
+ SUITE_NAME = "rf.suite.name"
6
+ SUITE_SOURCE = "rf.suite.source"
7
+ SUITE_ID = "rf.suite.id"
8
+ SUITE_METADATA = "rf.suite.metadata"
9
+
10
+ # Test attributes
11
+ TEST_NAME = "rf.test.name"
12
+ TEST_ID = "rf.test.id"
13
+ TEST_TAGS = "rf.test.tags"
14
+ TEST_TEMPLATE = "rf.test.template"
15
+ TEST_TIMEOUT = "rf.test.timeout"
16
+
17
+ # Keyword attributes
18
+ KEYWORD_NAME = "rf.keyword.name"
19
+ KEYWORD_TYPE = "rf.keyword.type"
20
+ KEYWORD_LIBRARY = "rf.keyword.library"
21
+ KEYWORD_ARGS = "rf.keyword.args"
22
+
23
+ # Result attributes
24
+ STATUS = "rf.status"
25
+ ELAPSED_TIME = "rf.elapsed_time"
26
+ START_TIME = "rf.start_time"
27
+ END_TIME = "rf.end_time"
28
+ MESSAGE = "rf.message"
29
+
30
+ # Framework attributes
31
+ RF_VERSION = "rf.version"
32
+
33
+
34
+ class AttributeExtractor:
35
+ """Extract attributes from Robot Framework objects."""
36
+
37
+ @staticmethod
38
+ def from_suite(data, result):
39
+ """Extract attributes from suite data and result."""
40
+ attrs = {
41
+ RFAttributes.SUITE_NAME: data.name,
42
+ RFAttributes.SUITE_ID: result.id,
43
+ }
44
+ if data.source:
45
+ attrs[RFAttributes.SUITE_SOURCE] = str(data.source)
46
+
47
+ # Extract suite metadata
48
+ if hasattr(data, 'metadata') and data.metadata:
49
+ for key, value in data.metadata.items():
50
+ attrs[f"{RFAttributes.SUITE_METADATA}.{key}"] = str(value)
51
+
52
+ # Add timing information
53
+ if hasattr(result, 'starttime') and result.starttime:
54
+ attrs[RFAttributes.START_TIME] = result.starttime
55
+ if hasattr(result, 'endtime') and result.endtime:
56
+ attrs[RFAttributes.END_TIME] = result.endtime
57
+
58
+ return attrs
59
+
60
+ @staticmethod
61
+ def from_test(data, result):
62
+ """Extract attributes from test data and result."""
63
+ attrs = {
64
+ RFAttributes.TEST_NAME: data.name,
65
+ RFAttributes.TEST_ID: result.id,
66
+ }
67
+ if data.tags:
68
+ attrs[RFAttributes.TEST_TAGS] = list(data.tags)
69
+
70
+ # Add test template if available
71
+ if hasattr(data, 'template') and data.template:
72
+ attrs[RFAttributes.TEST_TEMPLATE] = str(data.template)
73
+
74
+ # Add test timeout if available
75
+ if hasattr(data, 'timeout') and data.timeout:
76
+ attrs[RFAttributes.TEST_TIMEOUT] = str(data.timeout)
77
+
78
+ # Add timing information
79
+ if hasattr(result, 'starttime') and result.starttime:
80
+ attrs[RFAttributes.START_TIME] = result.starttime
81
+ if hasattr(result, 'endtime') and result.endtime:
82
+ attrs[RFAttributes.END_TIME] = result.endtime
83
+
84
+ # Add message if available
85
+ if hasattr(result, 'message') and result.message:
86
+ attrs[RFAttributes.MESSAGE] = result.message
87
+
88
+ return attrs
89
+
90
+ @staticmethod
91
+ def from_keyword(data, result, max_arg_length=200):
92
+ """Extract attributes from keyword data and result."""
93
+ attrs = {
94
+ RFAttributes.KEYWORD_NAME: data.name,
95
+ RFAttributes.KEYWORD_TYPE: data.type,
96
+ }
97
+ # Try to get library name (may not always be available)
98
+ if hasattr(data, 'libname') and data.libname:
99
+ attrs[RFAttributes.KEYWORD_LIBRARY] = data.libname
100
+ elif hasattr(data, 'owner') and data.owner:
101
+ attrs[RFAttributes.KEYWORD_LIBRARY] = data.owner.name if hasattr(data.owner, 'name') else str(data.owner)
102
+
103
+ if data.args:
104
+ args_str = ", ".join(str(arg)[:max_arg_length] for arg in data.args[:10])
105
+ if len(args_str) > max_arg_length:
106
+ args_str = args_str[:max_arg_length] + "..."
107
+ attrs[RFAttributes.KEYWORD_ARGS] = args_str
108
+ return attrs
@@ -0,0 +1,63 @@
1
+ import os
2
+
3
+
4
+ class TracerConfig:
5
+ """Configuration for the Robot Framework tracer."""
6
+
7
+ def __init__(self, **kwargs):
8
+ self.endpoint = self._get_config(
9
+ "endpoint", kwargs, "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces"
10
+ )
11
+ self.service_name = self._get_config(
12
+ "service_name", kwargs, "OTEL_SERVICE_NAME", "rf"
13
+ )
14
+ self.protocol = self._get_config("protocol", kwargs, "RF_TRACER_PROTOCOL", "http")
15
+ self.capture_arguments = self._get_bool_config(
16
+ "capture_arguments", kwargs, "RF_TRACER_CAPTURE_ARGUMENTS", True
17
+ )
18
+ self.max_arg_length = int(
19
+ self._get_config("max_arg_length", kwargs, "RF_TRACER_MAX_ARG_LENGTH", "200")
20
+ )
21
+ self.capture_logs = self._get_bool_config(
22
+ "capture_logs", kwargs, "RF_TRACER_CAPTURE_LOGS", False
23
+ )
24
+ self.log_level = self._get_config(
25
+ "log_level", kwargs, "RF_TRACER_LOG_LEVEL", "INFO"
26
+ ).upper()
27
+ self.max_log_length = int(
28
+ self._get_config("max_log_length", kwargs, "RF_TRACER_MAX_LOG_LENGTH", "500")
29
+ )
30
+ self.sample_rate = float(
31
+ self._get_config("sample_rate", kwargs, "RF_TRACER_SAMPLE_RATE", "1.0")
32
+ )
33
+ self.span_prefix_style = self._get_config(
34
+ "span_prefix_style", kwargs, "RF_TRACER_SPAN_PREFIX_STYLE", "none"
35
+ ).lower()
36
+
37
+ def _get_config(self, key, kwargs, env_var, default):
38
+ """Get configuration value with precedence: kwargs > env > default."""
39
+ return kwargs.get(key, os.environ.get(env_var, default))
40
+
41
+ def _get_bool_config(self, key, kwargs, env_var, default):
42
+ """Get boolean configuration value."""
43
+ value = self._get_config(key, kwargs, env_var, str(default))
44
+ if isinstance(value, bool):
45
+ return value
46
+ return value.lower() in ("true", "1", "yes")
47
+
48
+ @staticmethod
49
+ def from_listener_args(*args):
50
+ """Parse listener arguments from Robot Framework.
51
+
52
+ Args can be passed as:
53
+ - Single string: "endpoint=http://localhost:4318,service_name=my-service"
54
+ - Multiple strings: "endpoint=http://localhost:4318", "service_name=my-service"
55
+ """
56
+ kwargs = {}
57
+ for arg in args:
58
+ if "=" in arg:
59
+ for pair in arg.split(","):
60
+ if "=" in pair:
61
+ key, value = pair.split("=", 1)
62
+ kwargs[key.strip()] = value.strip()
63
+ return TracerConfig(**kwargs)
@@ -0,0 +1,236 @@
1
+ from opentelemetry import trace
2
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPExporter
3
+ from opentelemetry.sdk.resources import Resource, SERVICE_NAME, SERVICE_VERSION
4
+ from opentelemetry.sdk.trace import TracerProvider
5
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
6
+ from opentelemetry.sdk.trace.sampling import TraceIdRatioBased, ParentBased
7
+ from opentelemetry.semconv.resource import ResourceAttributes
8
+ import platform
9
+ import sys
10
+ import robot
11
+
12
+ from .config import TracerConfig
13
+ from .span_builder import SpanBuilder
14
+
15
+ # Try to import gRPC exporter (optional dependency)
16
+ try:
17
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GRPCExporter
18
+ GRPC_AVAILABLE = True
19
+ except ImportError:
20
+ GRPC_AVAILABLE = False
21
+
22
+
23
+ class TracingListener:
24
+ """Robot Framework Listener v3 for distributed tracing."""
25
+
26
+ ROBOT_LISTENER_API_VERSION = 3
27
+
28
+ def __init__(self, endpoint=None, service_name=None, protocol=None,
29
+ capture_arguments=None, max_arg_length=None, capture_logs=None,
30
+ sample_rate=None, span_prefix_style=None, log_level=None, max_log_length=None):
31
+ """Initialize the tracing listener.
32
+
33
+ Args:
34
+ endpoint: OTLP endpoint URL
35
+ service_name: Service name for traces
36
+ protocol: Protocol (http or grpc)
37
+ capture_arguments: Whether to capture keyword arguments
38
+ max_arg_length: Maximum length for arguments
39
+ capture_logs: Whether to capture log messages
40
+ sample_rate: Sampling rate (0.0-1.0)
41
+ span_prefix_style: Span prefix style (none, text, emoji)
42
+ log_level: Minimum log level to capture (DEBUG, INFO, WARN, ERROR)
43
+ max_log_length: Maximum length for log messages
44
+ """
45
+ # Build kwargs dict from provided arguments
46
+ kwargs = {}
47
+ if endpoint is not None:
48
+ kwargs['endpoint'] = endpoint
49
+ if service_name is not None:
50
+ kwargs['service_name'] = service_name
51
+ if protocol is not None:
52
+ kwargs['protocol'] = protocol
53
+ if capture_arguments is not None:
54
+ kwargs['capture_arguments'] = capture_arguments
55
+ if max_arg_length is not None:
56
+ kwargs['max_arg_length'] = max_arg_length
57
+ if capture_logs is not None:
58
+ kwargs['capture_logs'] = capture_logs
59
+ if sample_rate is not None:
60
+ kwargs['sample_rate'] = sample_rate
61
+ if span_prefix_style is not None:
62
+ kwargs['span_prefix_style'] = span_prefix_style
63
+ if log_level is not None:
64
+ kwargs['log_level'] = log_level
65
+ if max_log_length is not None:
66
+ kwargs['max_log_length'] = max_log_length
67
+
68
+ self.config = TracerConfig(**kwargs)
69
+
70
+ # Initialize OpenTelemetry with automatic resource detection
71
+ resource_attrs = {
72
+ SERVICE_NAME: self.config.service_name,
73
+ ResourceAttributes.TELEMETRY_SDK_NAME: "robotframework-tracer",
74
+ ResourceAttributes.TELEMETRY_SDK_LANGUAGE: "python",
75
+ ResourceAttributes.TELEMETRY_SDK_VERSION: "0.1.0",
76
+ "rf.version": robot.version.get_version(),
77
+ "python.version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
78
+ ResourceAttributes.HOST_NAME: platform.node(),
79
+ ResourceAttributes.OS_TYPE: platform.system(),
80
+ ResourceAttributes.OS_VERSION: platform.release(),
81
+ }
82
+ resource = Resource.create(resource_attrs)
83
+
84
+ # Configure sampling only if sample_rate < 1.0
85
+ if self.config.sample_rate < 1.0:
86
+ sampler = ParentBased(root=TraceIdRatioBased(self.config.sample_rate))
87
+ provider = TracerProvider(resource=resource, sampler=sampler)
88
+ else:
89
+ provider = TracerProvider(resource=resource)
90
+
91
+ # Select exporter based on protocol
92
+ if self.config.protocol == "grpc":
93
+ if not GRPC_AVAILABLE:
94
+ print("Warning: gRPC exporter not available. Install with: pip install opentelemetry-exporter-otlp-proto-grpc")
95
+ print("Falling back to HTTP exporter")
96
+ exporter = HTTPExporter(endpoint=self.config.endpoint)
97
+ else:
98
+ exporter = GRPCExporter(endpoint=self.config.endpoint)
99
+ else:
100
+ exporter = HTTPExporter(endpoint=self.config.endpoint)
101
+
102
+ processor = BatchSpanProcessor(exporter)
103
+ provider.add_span_processor(processor)
104
+ trace.set_tracer_provider(provider)
105
+
106
+ self.tracer = trace.get_tracer(__name__)
107
+ self.span_stack = []
108
+
109
+ def start_suite(self, data, result):
110
+ """Create root span for suite."""
111
+ try:
112
+ span = SpanBuilder.create_suite_span(self.tracer, data, result, self.config.span_prefix_style)
113
+ self.span_stack.append(span)
114
+ except Exception as e:
115
+ print(f"TracingListener error in start_suite: {e}")
116
+
117
+ def end_suite(self, data, result):
118
+ """Close suite span."""
119
+ try:
120
+ if self.span_stack:
121
+ span = self.span_stack.pop()
122
+ SpanBuilder.set_span_status(span, result)
123
+ span.end()
124
+ except Exception as e:
125
+ print(f"TracingListener error in end_suite: {e}")
126
+
127
+ def start_test(self, data, result):
128
+ """Create child span for test case."""
129
+ try:
130
+ parent_context = (
131
+ trace.set_span_in_context(self.span_stack[-1]) if self.span_stack else None
132
+ )
133
+ span = SpanBuilder.create_test_span(self.tracer, data, result, parent_context, self.config.span_prefix_style)
134
+ self.span_stack.append(span)
135
+ except Exception as e:
136
+ print(f"TracingListener error in start_test: {e}")
137
+
138
+ def end_test(self, data, result):
139
+ """Close test span with verdict."""
140
+ try:
141
+ if self.span_stack:
142
+ span = self.span_stack.pop()
143
+ SpanBuilder.set_span_status(span, result)
144
+ if result.status == "FAIL":
145
+ SpanBuilder.add_error_event(span, result)
146
+ span.end()
147
+ except Exception as e:
148
+ print(f"TracingListener error in end_test: {e}")
149
+
150
+ def start_keyword(self, data, result):
151
+ """Create child span for keyword/step."""
152
+ try:
153
+ if not self.config.capture_arguments and data.args:
154
+ return
155
+
156
+ parent_context = (
157
+ trace.set_span_in_context(self.span_stack[-1]) if self.span_stack else None
158
+ )
159
+ span = SpanBuilder.create_keyword_span(
160
+ self.tracer, data, result, parent_context, self.config.max_arg_length, self.config.span_prefix_style
161
+ )
162
+ self.span_stack.append(span)
163
+
164
+ # Add event for setup/teardown start
165
+ if data.type in ("SETUP", "TEARDOWN"):
166
+ span.add_event(f"{data.type.lower()}.start", {"keyword": data.name})
167
+ except Exception as e:
168
+ print(f"TracingListener error in start_keyword: {e}")
169
+
170
+ def end_keyword(self, data, result):
171
+ """Close keyword span."""
172
+ try:
173
+ if self.span_stack:
174
+ span = self.span_stack.pop()
175
+
176
+ # Add event for setup/teardown end
177
+ if data.type in ("SETUP", "TEARDOWN"):
178
+ span.add_event(f"{data.type.lower()}.end", {
179
+ "keyword": data.name,
180
+ "status": result.status
181
+ })
182
+
183
+ SpanBuilder.set_span_status(span, result)
184
+ if result.status == "FAIL":
185
+ SpanBuilder.add_error_event(span, result)
186
+ span.end()
187
+ except Exception as e:
188
+ print(f"TracingListener error in end_keyword: {e}")
189
+
190
+ def close(self):
191
+ """Cleanup on listener close."""
192
+ try:
193
+ while self.span_stack:
194
+ span = self.span_stack.pop()
195
+ span.end()
196
+ trace.get_tracer_provider().force_flush()
197
+ except Exception as e:
198
+ print(f"TracingListener error in close: {e}")
199
+
200
+ def log_message(self, message):
201
+ """Capture log messages as span events."""
202
+ try:
203
+ if not self.config.capture_logs or not self.span_stack:
204
+ return
205
+
206
+ # Filter by log level
207
+ log_levels = {"TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3, "ERROR": 4, "FAIL": 5}
208
+ min_level = log_levels.get(self.config.log_level, 2)
209
+ msg_level = log_levels.get(message.level, 2)
210
+
211
+ if msg_level < min_level:
212
+ return
213
+
214
+ # Get current span
215
+ current_span = self.span_stack[-1]
216
+
217
+ # Limit message length
218
+ log_text = message.message
219
+ if len(log_text) > self.config.max_log_length:
220
+ log_text = log_text[:self.config.max_log_length] + "..."
221
+
222
+ # Add log as span event (convert timestamp to string)
223
+ event_attrs = {
224
+ "message": log_text,
225
+ "level": message.level,
226
+ }
227
+ if hasattr(message, 'timestamp') and message.timestamp:
228
+ event_attrs["timestamp"] = str(message.timestamp)
229
+
230
+ current_span.add_event(f"log.{message.level.lower()}", event_attrs)
231
+ except RecursionError:
232
+ # Avoid infinite recursion if logging causes more logs
233
+ pass
234
+ except Exception:
235
+ # Silently ignore errors in log capture to avoid breaking tests
236
+ pass
@@ -0,0 +1,128 @@
1
+ from opentelemetry import trace, baggage
2
+ from opentelemetry.trace import Status, StatusCode
3
+ import robot
4
+
5
+ from .attributes import AttributeExtractor, RFAttributes
6
+
7
+
8
+ class SpanBuilder:
9
+ """Build OpenTelemetry spans from Robot Framework objects."""
10
+
11
+ # Prefix mappings
12
+ TEXT_PREFIXES = {
13
+ "SUITE": "[SUITE]",
14
+ "TEST": "[TEST CASE]",
15
+ "KEYWORD": "[TEST STEP]",
16
+ "SETUP": "[SETUP]",
17
+ "TEARDOWN": "[TEARDOWN]",
18
+ }
19
+
20
+ EMOJI_PREFIXES = {
21
+ "SUITE": "📦",
22
+ "TEST": "🧪",
23
+ "KEYWORD": "👟",
24
+ "SETUP": "🔧",
25
+ "TEARDOWN": "🧹",
26
+ }
27
+
28
+ @staticmethod
29
+ def _add_prefix(name, span_type, prefix_style):
30
+ """Add prefix to span name based on style."""
31
+ if prefix_style == "none" or not prefix_style:
32
+ return name
33
+ elif prefix_style == "text":
34
+ prefix = SpanBuilder.TEXT_PREFIXES.get(span_type, "")
35
+ return f"{prefix} {name}" if prefix else name
36
+ elif prefix_style == "emoji":
37
+ prefix = SpanBuilder.EMOJI_PREFIXES.get(span_type, "")
38
+ return f"{prefix} {name}" if prefix else name
39
+ return name
40
+
41
+ @staticmethod
42
+ def create_suite_span(tracer, data, result, prefix_style="none"):
43
+ """Create root span for test suite."""
44
+ attrs = AttributeExtractor.from_suite(data, result)
45
+ name = SpanBuilder._add_prefix(data.name, "SUITE", prefix_style)
46
+
47
+ # Create context with baggage
48
+ ctx = baggage.set_baggage("rf.suite.id", result.id)
49
+ ctx = baggage.set_baggage("rf.version", robot.version.get_version(), ctx)
50
+
51
+ # Add suite metadata to baggage (limit to avoid too much data)
52
+ if hasattr(data, 'metadata') and data.metadata:
53
+ for key, value in list(data.metadata.items())[:5]: # Limit to 5 metadata items
54
+ ctx = baggage.set_baggage(f"rf.suite.metadata.{key}", str(value), ctx)
55
+
56
+ span = tracer.start_span(name, context=ctx, kind=trace.SpanKind.INTERNAL, attributes=attrs)
57
+ return span
58
+
59
+ @staticmethod
60
+ def create_test_span(tracer, data, result, parent_context, prefix_style="none"):
61
+ """Create child span for test case."""
62
+ attrs = AttributeExtractor.from_test(data, result)
63
+ name = SpanBuilder._add_prefix(data.name, "TEST", prefix_style)
64
+ span = tracer.start_span(
65
+ name, context=parent_context, kind=trace.SpanKind.INTERNAL, attributes=attrs
66
+ )
67
+ return span
68
+
69
+ @staticmethod
70
+ def create_keyword_span(tracer, data, result, parent_context, max_arg_length=200, prefix_style="none"):
71
+ """Create child span for keyword."""
72
+ attrs = AttributeExtractor.from_keyword(data, result, max_arg_length)
73
+
74
+ # Build keyword name with arguments (like RF test step line)
75
+ kw_name = data.name
76
+ if data.args:
77
+ # Join arguments with comma-space
78
+ args_str = ", ".join(str(arg) for arg in data.args)
79
+ # Limit total length to avoid extremely long span names
80
+ if len(args_str) > 100:
81
+ args_str = args_str[:100] + "..."
82
+ kw_name = f"{data.name} {args_str}"
83
+
84
+ # Determine span type for prefix
85
+ if data.type in ("SETUP", "TEARDOWN"):
86
+ span_type = data.type
87
+ else:
88
+ span_type = "KEYWORD"
89
+
90
+ # Add prefix based on style
91
+ kw_name = SpanBuilder._add_prefix(kw_name, span_type, prefix_style)
92
+
93
+ span = tracer.start_span(
94
+ kw_name, context=parent_context, kind=trace.SpanKind.INTERNAL, attributes=attrs
95
+ )
96
+ return span
97
+
98
+ @staticmethod
99
+ def set_span_status(span, result):
100
+ """Set span status based on RF result."""
101
+ span.set_attribute(RFAttributes.STATUS, result.status)
102
+ span.set_attribute(RFAttributes.ELAPSED_TIME, result.elapsedtime / 1000.0)
103
+
104
+ if result.status == "FAIL":
105
+ span.set_status(Status(StatusCode.ERROR, result.message))
106
+ else:
107
+ span.set_status(Status(StatusCode.OK))
108
+
109
+ @staticmethod
110
+ def add_error_event(span, result):
111
+ """Add error event with exception details."""
112
+ if result.status == "FAIL" and result.message:
113
+ # Create detailed error event
114
+ event_attrs = {
115
+ "message": result.message,
116
+ "rf.status": "FAIL",
117
+ }
118
+
119
+ # Try to extract exception type if available
120
+ if hasattr(result, 'error') and result.error:
121
+ event_attrs["exception.type"] = type(result.error).__name__
122
+ event_attrs["exception.message"] = str(result.error)
123
+
124
+ # Add timestamp
125
+ if hasattr(result, 'endtime') and result.endtime:
126
+ event_attrs["timestamp"] = result.endtime
127
+
128
+ span.add_event("test.failed", event_attrs)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.1"
@@ -0,0 +1,242 @@
1
+ Metadata-Version: 2.1
2
+ Name: robotframework-tracer
3
+ Version: 0.1.0
4
+ Summary: OpenTelemetry distributed tracing for Robot Framework
5
+ Author: Robot Framework Tracer Contributors
6
+ License: Apache-2.0
7
+ Project-URL: Homepage, https://github.com/yourusername/robotframework-tracer
8
+ Project-URL: Documentation, https://github.com/yourusername/robotframework-tracer/blob/main/README.md
9
+ Project-URL: Repository, https://github.com/yourusername/robotframework-tracer
10
+ Project-URL: Issues, https://github.com/yourusername/robotframework-tracer/issues
11
+ Keywords: robotframework,testing,tracing,opentelemetry,observability
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Topic :: Software Development :: Testing
15
+ Classifier: License :: OSI Approved :: Apache Software License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.8
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Framework :: Robot Framework
23
+ Requires-Python: >=3.8
24
+ Description-Content-Type: text/markdown
25
+ Requires-Dist: robotframework>=6.0
26
+ Requires-Dist: opentelemetry-api>=1.20.0
27
+ Requires-Dist: opentelemetry-sdk>=1.20.0
28
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http>=1.20.0
29
+ Provides-Extra: dev
30
+ Requires-Dist: pytest>=7.0; extra == "dev"
31
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
32
+ Requires-Dist: black>=23.0; extra == "dev"
33
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
34
+ Requires-Dist: mypy>=1.0; extra == "dev"
35
+ Provides-Extra: grpc
36
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.20.0; extra == "grpc"
37
+
38
+ # Robot Framework Tracer
39
+
40
+ OpenTelemetry distributed tracing integration for Robot Framework test execution.
41
+
42
+ ## What is this?
43
+
44
+ `robotframework-tracer` is a Robot Framework listener plugin that automatically creates distributed traces for your test execution using OpenTelemetry. It captures the complete test hierarchy (suites → tests → keywords) as spans and exports them to any OpenTelemetry-compatible backend like Jaeger, Grafana Tempo, or Zipkin.
45
+
46
+ This enables you to:
47
+ - **Visualize test execution flow** with detailed timing information
48
+ - **Debug test failures** by examining the complete execution trace
49
+ - **Analyze performance** and identify slow keywords or tests
50
+ - **Correlate tests with application traces** in distributed systems
51
+ - **Monitor test execution** across CI/CD pipelines
52
+
53
+ ![Robot Framework Trace Visualization](docs/robotframework-trace.jpg)
54
+
55
+ ## How it works
56
+
57
+ The tracer implements the Robot Framework Listener v3 API and creates OpenTelemetry spans for each test execution phase:
58
+
59
+ ```
60
+ Suite Span (root)
61
+ ├── Test Case Span
62
+ │ ├── Keyword Span
63
+ │ │ └── Nested Keyword Span
64
+ │ └── Keyword Span
65
+ └── Test Case Span
66
+ └── Keyword Span
67
+ ```
68
+
69
+ Each span includes rich metadata: test names, tags, status (PASS/FAIL), timing, arguments, and error details.
70
+
71
+ ## Installation
72
+
73
+ ### From PyPI (when released)
74
+
75
+ ```bash
76
+ pip install robotframework-tracer
77
+ ```
78
+
79
+ ### From Source (Development)
80
+
81
+ ```bash
82
+ # Clone the repository
83
+ git clone <repository-url>
84
+ cd robotframework-tracer
85
+
86
+ # Create and activate virtual environment
87
+ python3 -m venv venv
88
+ source venv/bin/activate # On Windows: venv\Scripts\activate
89
+
90
+ # Install in development mode
91
+ pip install -e ".[dev]"
92
+ ```
93
+
94
+ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for detailed development setup instructions.
95
+
96
+ ## Quick Start
97
+
98
+ ### 1. Start a tracing backend (Jaeger example)
99
+
100
+ ```bash
101
+ docker run -d --name jaeger \
102
+ -p 16686:16686 \
103
+ -p 4318:4318 \
104
+ jaegertracing/all-in-one:latest
105
+ ```
106
+
107
+ ### 2. Run your tests with the listener
108
+
109
+ ```bash
110
+ robot --listener robotframework_tracer.TracingListener tests/
111
+ ```
112
+
113
+ ### 3. View traces
114
+
115
+ Open http://localhost:16686 in your browser to see your test traces in Jaeger UI.
116
+
117
+ ## Configuration
118
+
119
+ ### Basic usage
120
+
121
+ ```bash
122
+ robot --listener robotframework_tracer.TracingListener tests/
123
+ ```
124
+
125
+ ### Custom endpoint
126
+
127
+ ```bash
128
+ robot --listener robotframework_tracer.TracingListener:endpoint=http://jaeger:4318/v1/traces tests/
129
+ ```
130
+
131
+ ### Custom service name
132
+
133
+ ```bash
134
+ robot --listener "robotframework_tracer.TracingListener:endpoint=http://jaeger:4318/v1/traces,service_name=my-tests" tests/
135
+ ```
136
+
137
+ ### All configuration options
138
+
139
+ ```bash
140
+ robot --listener "robotframework_tracer.TracingListener:\
141
+ endpoint=http://localhost:4318/v1/traces,\
142
+ service_name=robot-tests,\
143
+ protocol=http,\
144
+ capture_arguments=true,\
145
+ max_arg_length=200" tests/
146
+ ```
147
+
148
+ ### Environment variables
149
+
150
+ ```bash
151
+ export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
152
+ export OTEL_SERVICE_NAME=robot-framework-tests
153
+ robot --listener robotframework_tracer.TracingListener tests/
154
+ ```
155
+
156
+ ## Configuration Options
157
+
158
+ | Option | Default | Description |
159
+ |--------|---------|-------------|
160
+ | `endpoint` | `http://localhost:4318/v1/traces` | OTLP endpoint URL |
161
+ | `service_name` | `rf` | Service name in traces |
162
+ | `protocol` | `http` | Protocol: `http` or `grpc` |
163
+ | `span_prefix_style` | `none` | Span prefix style: `none`, `text`, `emoji` |
164
+ | `capture_arguments` | `true` | Capture keyword arguments |
165
+ | `max_arg_length` | `200` | Max length for arguments |
166
+ | `capture_logs` | `false` | Capture log messages as events |
167
+ | `log_level` | `INFO` | Minimum log level (DEBUG, INFO, WARN, ERROR) |
168
+ | `max_log_length` | `500` | Max length for log messages |
169
+ | `sample_rate` | `1.0` | Sampling rate (0.0-1.0, 1.0 = no sampling) |
170
+
171
+ ## Span Attributes
172
+
173
+ Each span includes relevant Robot Framework metadata:
174
+
175
+ **Suite spans:**
176
+ - `rf.suite.name` - Suite name
177
+ - `rf.suite.source` - Suite file path
178
+ - `rf.suite.id` - Suite ID
179
+ - `rf.version` - Robot Framework version
180
+
181
+ **Test spans:**
182
+ - `rf.test.name` - Test case name
183
+ - `rf.test.id` - Test ID
184
+ - `rf.test.tags` - Test tags
185
+ - `rf.status` - PASS/FAIL/SKIP
186
+ - `rf.elapsed_time` - Execution time
187
+
188
+ **Keyword spans:**
189
+ - `rf.keyword.name` - Keyword name
190
+ - `rf.keyword.type` - SETUP/TEARDOWN/KEYWORD
191
+ - `rf.keyword.library` - Library name
192
+ - `rf.keyword.args` - Arguments (if enabled)
193
+ - `rf.status` - PASS/FAIL
194
+
195
+ ## Supported Backends
196
+
197
+ Works with any OpenTelemetry-compatible backend:
198
+ - **Jaeger** - Open source tracing platform
199
+ - **Grafana Tempo** - High-scale distributed tracing
200
+ - **Zipkin** - Distributed tracing system
201
+ - **AWS X-Ray** - AWS distributed tracing
202
+ - **Honeycomb** - Observability platform
203
+ - **Datadog** - Monitoring and analytics
204
+
205
+ See [docs/backends.md](docs/backends.md) for backend-specific setup guides.
206
+
207
+ ## Requirements
208
+
209
+ - Python 3.8+
210
+ - Robot Framework 6.0+
211
+ - OpenTelemetry SDK
212
+
213
+ ## Documentation
214
+
215
+ - [Architecture](docs/ARCHITECTURE.md) - Design and architecture details
216
+ - [Implementation Plan](docs/IMPLEMENTATION_PLAN.md) - Development roadmap
217
+ - [Configuration Guide](docs/configuration.md) - Detailed configuration reference
218
+ - [Attribute Reference](docs/attributes.md) - Complete attribute documentation
219
+ - [Backend Setup](docs/backends.md) - Backend-specific guides
220
+
221
+ ## Examples
222
+
223
+ See the [examples/](examples/) directory for complete examples:
224
+ - Basic usage with Jaeger
225
+ - Advanced configuration
226
+ - CI/CD integration
227
+ - Multiple backend setups
228
+
229
+ ## Contributing
230
+
231
+ Contributions are welcome! Please see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for guidelines.
232
+
233
+ ## License
234
+
235
+ Apache License 2.0 - See [docs/LICENSE](docs/LICENSE) for details.
236
+
237
+ ## Status
238
+
239
+ **Current Version:** v0.1.0
240
+ **Status:** Production-ready MVP
241
+
242
+ Core functionality is complete and tested. See [docs/CHANGELOG.md](docs/CHANGELOG.md) for version history and [docs/IMPLEMENTATION_PLAN.md](docs/IMPLEMENTATION_PLAN.md) for the development roadmap.
@@ -0,0 +1,10 @@
1
+ robotframework_tracer/__init__.py,sha256=cZh3xnNaYhRisrqqWH5Gdt4zsgpbh1mUALJiUtGNG8M,204
2
+ robotframework_tracer/attributes.py,sha256=7lpqy-7maQma01tfG3TzUNLIseQPzARCuYPFiy1K3ao,3854
3
+ robotframework_tracer/config.py,sha256=qCckfLNvwRUrJ8EolG47zJDmZG-9175V7_1gR4ypNZc,2552
4
+ robotframework_tracer/listener.py,sha256=8FMv2BTFo3Q471dFVCoS0YfLrHkeBSPsH6EKG75ccqQ,9795
5
+ robotframework_tracer/span_builder.py,sha256=NKo0AOS-ZxYY_Z6Oypfbg33CAt2EKt6016Vh8woDtfo,4914
6
+ robotframework_tracer/version.py,sha256=rnObPjuBcEStqSO0S6gsdS_ot8ITOQjVj_-P1LUUYpg,22
7
+ robotframework_tracer-0.1.0.dist-info/METADATA,sha256=C60xEMX1xrxLxefDlGggCCzbwd1Nbt_mJI5mXQdyLOY,7654
8
+ robotframework_tracer-0.1.0.dist-info/WHEEL,sha256=iAkIy5fosb7FzIOwONchHf19Qu7_1wCWyFNR5gu9nU0,91
9
+ robotframework_tracer-0.1.0.dist-info/top_level.txt,sha256=G1sMKH-8SM_CdJe0Wm6wa_rg1uo62jfhft_UfaxZ05I,22
10
+ robotframework_tracer-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.3.2)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ robotframework_tracer