robotframework-tracer 0.1.0__tar.gz → 0.2.0__tar.gz

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.
Files changed (17) hide show
  1. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/PKG-INFO +31 -3
  2. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/README.md +30 -2
  3. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/pyproject.toml +1 -1
  4. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer/attributes.py +22 -20
  5. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer/config.py +1 -3
  6. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer/listener.py +118 -37
  7. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer/span_builder.py +16 -14
  8. robotframework_tracer-0.2.0/src/robotframework_tracer/version.py +1 -0
  9. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer.egg-info/PKG-INFO +31 -3
  10. robotframework_tracer-0.1.0/src/robotframework_tracer/version.py +0 -1
  11. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/setup.cfg +0 -0
  12. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/setup.py +0 -0
  13. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer/__init__.py +0 -0
  14. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer.egg-info/SOURCES.txt +0 -0
  15. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer.egg-info/dependency_links.txt +0 -0
  16. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer.egg-info/requires.txt +0 -0
  17. {robotframework_tracer-0.1.0 → robotframework_tracer-0.2.0}/src/robotframework_tracer.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: robotframework-tracer
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: OpenTelemetry distributed tracing for Robot Framework
5
5
  Author: Robot Framework Tracer Contributors
6
6
  License: Apache-2.0
@@ -49,6 +49,7 @@ This enables you to:
49
49
  - **Analyze performance** and identify slow keywords or tests
50
50
  - **Correlate tests with application traces** in distributed systems
51
51
  - **Monitor test execution** across CI/CD pipelines
52
+ - **Propagate trace context** to your System Under Test (SUT)
52
53
 
53
54
  ![Robot Framework Trace Visualization](docs/robotframework-trace.jpg)
54
55
 
@@ -114,6 +115,33 @@ robot --listener robotframework_tracer.TracingListener tests/
114
115
 
115
116
  Open http://localhost:16686 in your browser to see your test traces in Jaeger UI.
116
117
 
118
+ ## Trace Context Propagation
119
+
120
+ The tracer automatically makes trace context available as Robot Framework variables for propagating to your System Under Test:
121
+
122
+ ```robot
123
+ *** Test Cases ***
124
+ Test API With Distributed Tracing
125
+ # HTTP headers automatically include trace context
126
+ ${response}= POST http://my-sut/api
127
+ ... json={"data": "test"}
128
+ ... headers=${TRACE_HEADERS}
129
+
130
+ # For custom protocols, use individual components
131
+ ${diameter_msg}= Create Diameter Request
132
+ ... trace_id=${TRACE_ID}
133
+ ... span_id=${SPAN_ID}
134
+ ```
135
+
136
+ **Available variables:**
137
+ - `${TRACE_HEADERS}` - HTTP headers dictionary
138
+ - `${TRACE_ID}` - 32-character hex trace ID
139
+ - `${SPAN_ID}` - 16-character hex span ID
140
+ - `${TRACEPARENT}` - W3C traceparent header
141
+ - `${TRACESTATE}` - W3C tracestate header
142
+
143
+ See [docs/trace-propagation.md](docs/trace-propagation.md) for complete examples.
144
+
117
145
  ## Configuration
118
146
 
119
147
  ### Basic usage
@@ -236,7 +264,7 @@ Apache License 2.0 - See [docs/LICENSE](docs/LICENSE) for details.
236
264
 
237
265
  ## Status
238
266
 
239
- **Current Version:** v0.1.0
240
- **Status:** Production-ready MVP
267
+ **Current Version:** v0.2.0
268
+ **Status:** Production-ready with trace propagation
241
269
 
242
270
  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.
@@ -12,6 +12,7 @@ This enables you to:
12
12
  - **Analyze performance** and identify slow keywords or tests
13
13
  - **Correlate tests with application traces** in distributed systems
14
14
  - **Monitor test execution** across CI/CD pipelines
15
+ - **Propagate trace context** to your System Under Test (SUT)
15
16
 
16
17
  ![Robot Framework Trace Visualization](docs/robotframework-trace.jpg)
17
18
 
@@ -77,6 +78,33 @@ robot --listener robotframework_tracer.TracingListener tests/
77
78
 
78
79
  Open http://localhost:16686 in your browser to see your test traces in Jaeger UI.
79
80
 
81
+ ## Trace Context Propagation
82
+
83
+ The tracer automatically makes trace context available as Robot Framework variables for propagating to your System Under Test:
84
+
85
+ ```robot
86
+ *** Test Cases ***
87
+ Test API With Distributed Tracing
88
+ # HTTP headers automatically include trace context
89
+ ${response}= POST http://my-sut/api
90
+ ... json={"data": "test"}
91
+ ... headers=${TRACE_HEADERS}
92
+
93
+ # For custom protocols, use individual components
94
+ ${diameter_msg}= Create Diameter Request
95
+ ... trace_id=${TRACE_ID}
96
+ ... span_id=${SPAN_ID}
97
+ ```
98
+
99
+ **Available variables:**
100
+ - `${TRACE_HEADERS}` - HTTP headers dictionary
101
+ - `${TRACE_ID}` - 32-character hex trace ID
102
+ - `${SPAN_ID}` - 16-character hex span ID
103
+ - `${TRACEPARENT}` - W3C traceparent header
104
+ - `${TRACESTATE}` - W3C tracestate header
105
+
106
+ See [docs/trace-propagation.md](docs/trace-propagation.md) for complete examples.
107
+
80
108
  ## Configuration
81
109
 
82
110
  ### Basic usage
@@ -199,7 +227,7 @@ Apache License 2.0 - See [docs/LICENSE](docs/LICENSE) for details.
199
227
 
200
228
  ## Status
201
229
 
202
- **Current Version:** v0.1.0
203
- **Status:** Production-ready MVP
230
+ **Current Version:** v0.2.0
231
+ **Status:** Production-ready with trace propagation
204
232
 
205
233
  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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "robotframework-tracer"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "OpenTelemetry distributed tracing for Robot Framework"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -43,18 +43,18 @@ class AttributeExtractor:
43
43
  }
44
44
  if data.source:
45
45
  attrs[RFAttributes.SUITE_SOURCE] = str(data.source)
46
-
46
+
47
47
  # Extract suite metadata
48
- if hasattr(data, 'metadata') and data.metadata:
48
+ if hasattr(data, "metadata") and data.metadata:
49
49
  for key, value in data.metadata.items():
50
50
  attrs[f"{RFAttributes.SUITE_METADATA}.{key}"] = str(value)
51
-
51
+
52
52
  # Add timing information
53
- if hasattr(result, 'starttime') and result.starttime:
53
+ if hasattr(result, "starttime") and result.starttime:
54
54
  attrs[RFAttributes.START_TIME] = result.starttime
55
- if hasattr(result, 'endtime') and result.endtime:
55
+ if hasattr(result, "endtime") and result.endtime:
56
56
  attrs[RFAttributes.END_TIME] = result.endtime
57
-
57
+
58
58
  return attrs
59
59
 
60
60
  @staticmethod
@@ -66,25 +66,25 @@ class AttributeExtractor:
66
66
  }
67
67
  if data.tags:
68
68
  attrs[RFAttributes.TEST_TAGS] = list(data.tags)
69
-
69
+
70
70
  # Add test template if available
71
- if hasattr(data, 'template') and data.template:
71
+ if hasattr(data, "template") and data.template:
72
72
  attrs[RFAttributes.TEST_TEMPLATE] = str(data.template)
73
-
73
+
74
74
  # Add test timeout if available
75
- if hasattr(data, 'timeout') and data.timeout:
75
+ if hasattr(data, "timeout") and data.timeout:
76
76
  attrs[RFAttributes.TEST_TIMEOUT] = str(data.timeout)
77
-
77
+
78
78
  # Add timing information
79
- if hasattr(result, 'starttime') and result.starttime:
79
+ if hasattr(result, "starttime") and result.starttime:
80
80
  attrs[RFAttributes.START_TIME] = result.starttime
81
- if hasattr(result, 'endtime') and result.endtime:
81
+ if hasattr(result, "endtime") and result.endtime:
82
82
  attrs[RFAttributes.END_TIME] = result.endtime
83
-
83
+
84
84
  # Add message if available
85
- if hasattr(result, 'message') and result.message:
85
+ if hasattr(result, "message") and result.message:
86
86
  attrs[RFAttributes.MESSAGE] = result.message
87
-
87
+
88
88
  return attrs
89
89
 
90
90
  @staticmethod
@@ -95,11 +95,13 @@ class AttributeExtractor:
95
95
  RFAttributes.KEYWORD_TYPE: data.type,
96
96
  }
97
97
  # Try to get library name (may not always be available)
98
- if hasattr(data, 'libname') and data.libname:
98
+ if hasattr(data, "libname") and data.libname:
99
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
-
100
+ elif hasattr(data, "owner") and data.owner:
101
+ attrs[RFAttributes.KEYWORD_LIBRARY] = (
102
+ data.owner.name if hasattr(data.owner, "name") else str(data.owner)
103
+ )
104
+
103
105
  if data.args:
104
106
  args_str = ", ".join(str(arg)[:max_arg_length] for arg in data.args[:10])
105
107
  if len(args_str) > max_arg_length:
@@ -8,9 +8,7 @@ class TracerConfig:
8
8
  self.endpoint = self._get_config(
9
9
  "endpoint", kwargs, "OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318/v1/traces"
10
10
  )
11
- self.service_name = self._get_config(
12
- "service_name", kwargs, "OTEL_SERVICE_NAME", "rf"
13
- )
11
+ self.service_name = self._get_config("service_name", kwargs, "OTEL_SERVICE_NAME", "rf")
14
12
  self.protocol = self._get_config("protocol", kwargs, "RF_TRACER_PROTOCOL", "http")
15
13
  self.capture_arguments = self._get_bool_config(
16
14
  "capture_arguments", kwargs, "RF_TRACER_CAPTURE_ARGUMENTS", True
@@ -5,6 +5,7 @@ from opentelemetry.sdk.trace import TracerProvider
5
5
  from opentelemetry.sdk.trace.export import BatchSpanProcessor
6
6
  from opentelemetry.sdk.trace.sampling import TraceIdRatioBased, ParentBased
7
7
  from opentelemetry.semconv.resource import ResourceAttributes
8
+ from opentelemetry.propagate import inject
8
9
  import platform
9
10
  import sys
10
11
  import robot
@@ -12,9 +13,20 @@ import robot
12
13
  from .config import TracerConfig
13
14
  from .span_builder import SpanBuilder
14
15
 
16
+ # Try to import Robot Framework BuiltIn library for variable setting
17
+ try:
18
+ from robot.libraries.BuiltIn import BuiltIn
19
+
20
+ BUILTIN_AVAILABLE = True
21
+ except ImportError:
22
+ BUILTIN_AVAILABLE = False
23
+
15
24
  # Try to import gRPC exporter (optional dependency)
16
25
  try:
17
- from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GRPCExporter
26
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
27
+ OTLPSpanExporter as GRPCExporter,
28
+ )
29
+
18
30
  GRPC_AVAILABLE = True
19
31
  except ImportError:
20
32
  GRPC_AVAILABLE = False
@@ -25,9 +37,19 @@ class TracingListener:
25
37
 
26
38
  ROBOT_LISTENER_API_VERSION = 3
27
39
 
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):
40
+ def __init__(
41
+ self,
42
+ endpoint=None,
43
+ service_name=None,
44
+ protocol=None,
45
+ capture_arguments=None,
46
+ max_arg_length=None,
47
+ capture_logs=None,
48
+ sample_rate=None,
49
+ span_prefix_style=None,
50
+ log_level=None,
51
+ max_log_length=None,
52
+ ):
31
53
  """Initialize the tracing listener.
32
54
 
33
55
  Args:
@@ -45,26 +67,26 @@ class TracingListener:
45
67
  # Build kwargs dict from provided arguments
46
68
  kwargs = {}
47
69
  if endpoint is not None:
48
- kwargs['endpoint'] = endpoint
70
+ kwargs["endpoint"] = endpoint
49
71
  if service_name is not None:
50
- kwargs['service_name'] = service_name
72
+ kwargs["service_name"] = service_name
51
73
  if protocol is not None:
52
- kwargs['protocol'] = protocol
74
+ kwargs["protocol"] = protocol
53
75
  if capture_arguments is not None:
54
- kwargs['capture_arguments'] = capture_arguments
76
+ kwargs["capture_arguments"] = capture_arguments
55
77
  if max_arg_length is not None:
56
- kwargs['max_arg_length'] = max_arg_length
78
+ kwargs["max_arg_length"] = max_arg_length
57
79
  if capture_logs is not None:
58
- kwargs['capture_logs'] = capture_logs
80
+ kwargs["capture_logs"] = capture_logs
59
81
  if sample_rate is not None:
60
- kwargs['sample_rate'] = sample_rate
82
+ kwargs["sample_rate"] = sample_rate
61
83
  if span_prefix_style is not None:
62
- kwargs['span_prefix_style'] = span_prefix_style
84
+ kwargs["span_prefix_style"] = span_prefix_style
63
85
  if log_level is not None:
64
- kwargs['log_level'] = log_level
86
+ kwargs["log_level"] = log_level
65
87
  if max_log_length is not None:
66
- kwargs['max_log_length'] = max_log_length
67
-
88
+ kwargs["max_log_length"] = max_log_length
89
+
68
90
  self.config = TracerConfig(**kwargs)
69
91
 
70
92
  # Initialize OpenTelemetry with automatic resource detection
@@ -80,25 +102,27 @@ class TracingListener:
80
102
  ResourceAttributes.OS_VERSION: platform.release(),
81
103
  }
82
104
  resource = Resource.create(resource_attrs)
83
-
105
+
84
106
  # Configure sampling only if sample_rate < 1.0
85
107
  if self.config.sample_rate < 1.0:
86
108
  sampler = ParentBased(root=TraceIdRatioBased(self.config.sample_rate))
87
109
  provider = TracerProvider(resource=resource, sampler=sampler)
88
110
  else:
89
111
  provider = TracerProvider(resource=resource)
90
-
112
+
91
113
  # Select exporter based on protocol
92
114
  if self.config.protocol == "grpc":
93
115
  if not GRPC_AVAILABLE:
94
- print("Warning: gRPC exporter not available. Install with: pip install opentelemetry-exporter-otlp-proto-grpc")
116
+ print(
117
+ "Warning: gRPC exporter not available. Install with: pip install opentelemetry-exporter-otlp-proto-grpc"
118
+ )
95
119
  print("Falling back to HTTP exporter")
96
120
  exporter = HTTPExporter(endpoint=self.config.endpoint)
97
121
  else:
98
122
  exporter = GRPCExporter(endpoint=self.config.endpoint)
99
123
  else:
100
124
  exporter = HTTPExporter(endpoint=self.config.endpoint)
101
-
125
+
102
126
  processor = BatchSpanProcessor(exporter)
103
127
  provider.add_span_processor(processor)
104
128
  trace.set_tracer_provider(provider)
@@ -106,10 +130,56 @@ class TracingListener:
106
130
  self.tracer = trace.get_tracer(__name__)
107
131
  self.span_stack = []
108
132
 
133
+ def _set_trace_context_variables(self):
134
+ """Set Robot Framework variables with current trace context."""
135
+ if not BUILTIN_AVAILABLE:
136
+ return
137
+
138
+ try:
139
+ # Get current trace context
140
+ headers = {}
141
+ inject(headers) # Injects traceparent, tracestate headers
142
+
143
+ # Get current span info
144
+ current_span = trace.get_current_span()
145
+ trace_id = None
146
+ span_id = None
147
+
148
+ if current_span.is_recording():
149
+ span_context = current_span.get_span_context()
150
+ trace_id = format(span_context.trace_id, "032x")
151
+ span_id = format(span_context.span_id, "016x")
152
+
153
+ # Set RF variables for different protocols
154
+ builtin = BuiltIn()
155
+
156
+ # HTTP headers (for REST APIs, web services)
157
+ builtin.set_test_variable("${TRACE_HEADERS}", headers)
158
+
159
+ # Individual trace components (for custom protocols like Diameter)
160
+ if trace_id:
161
+ builtin.set_test_variable("${TRACE_ID}", trace_id)
162
+ if span_id:
163
+ builtin.set_test_variable("${SPAN_ID}", span_id)
164
+
165
+ # W3C traceparent format (for manual header construction)
166
+ if headers.get("traceparent"):
167
+ builtin.set_test_variable("${TRACEPARENT}", headers["traceparent"])
168
+
169
+ # Tracestate (for vendor-specific data)
170
+ if headers.get("tracestate"):
171
+ builtin.set_test_variable("${TRACESTATE}", headers["tracestate"])
172
+
173
+ except Exception as e:
174
+ # Silently ignore errors to avoid breaking tests
175
+ pass
176
+
109
177
  def start_suite(self, data, result):
110
178
  """Create root span for suite."""
111
179
  try:
112
- span = SpanBuilder.create_suite_span(self.tracer, data, result, self.config.span_prefix_style)
180
+ span = SpanBuilder.create_suite_span(
181
+ self.tracer, data, result, self.config.span_prefix_style
182
+ )
113
183
  self.span_stack.append(span)
114
184
  except Exception as e:
115
185
  print(f"TracingListener error in start_suite: {e}")
@@ -130,8 +200,15 @@ class TracingListener:
130
200
  parent_context = (
131
201
  trace.set_span_in_context(self.span_stack[-1]) if self.span_stack else None
132
202
  )
133
- span = SpanBuilder.create_test_span(self.tracer, data, result, parent_context, self.config.span_prefix_style)
203
+ span = SpanBuilder.create_test_span(
204
+ self.tracer, data, result, parent_context, self.config.span_prefix_style
205
+ )
134
206
  self.span_stack.append(span)
207
+
208
+ # Set trace context variables within the span context
209
+ with trace.use_span(span):
210
+ self._set_trace_context_variables()
211
+
135
212
  except Exception as e:
136
213
  print(f"TracingListener error in start_test: {e}")
137
214
 
@@ -157,10 +234,15 @@ class TracingListener:
157
234
  trace.set_span_in_context(self.span_stack[-1]) if self.span_stack else None
158
235
  )
159
236
  span = SpanBuilder.create_keyword_span(
160
- self.tracer, data, result, parent_context, self.config.max_arg_length, self.config.span_prefix_style
237
+ self.tracer,
238
+ data,
239
+ result,
240
+ parent_context,
241
+ self.config.max_arg_length,
242
+ self.config.span_prefix_style,
161
243
  )
162
244
  self.span_stack.append(span)
163
-
245
+
164
246
  # Add event for setup/teardown start
165
247
  if data.type in ("SETUP", "TEARDOWN"):
166
248
  span.add_event(f"{data.type.lower()}.start", {"keyword": data.name})
@@ -172,14 +254,13 @@ class TracingListener:
172
254
  try:
173
255
  if self.span_stack:
174
256
  span = self.span_stack.pop()
175
-
257
+
176
258
  # Add event for setup/teardown end
177
259
  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
-
260
+ span.add_event(
261
+ f"{data.type.lower()}.end", {"keyword": data.name, "status": result.status}
262
+ )
263
+
183
264
  SpanBuilder.set_span_status(span, result)
184
265
  if result.status == "FAIL":
185
266
  SpanBuilder.add_error_event(span, result)
@@ -202,31 +283,31 @@ class TracingListener:
202
283
  try:
203
284
  if not self.config.capture_logs or not self.span_stack:
204
285
  return
205
-
286
+
206
287
  # Filter by log level
207
288
  log_levels = {"TRACE": 0, "DEBUG": 1, "INFO": 2, "WARN": 3, "ERROR": 4, "FAIL": 5}
208
289
  min_level = log_levels.get(self.config.log_level, 2)
209
290
  msg_level = log_levels.get(message.level, 2)
210
-
291
+
211
292
  if msg_level < min_level:
212
293
  return
213
-
294
+
214
295
  # Get current span
215
296
  current_span = self.span_stack[-1]
216
-
297
+
217
298
  # Limit message length
218
299
  log_text = message.message
219
300
  if len(log_text) > self.config.max_log_length:
220
- log_text = log_text[:self.config.max_log_length] + "..."
221
-
301
+ log_text = log_text[: self.config.max_log_length] + "..."
302
+
222
303
  # Add log as span event (convert timestamp to string)
223
304
  event_attrs = {
224
305
  "message": log_text,
225
306
  "level": message.level,
226
307
  }
227
- if hasattr(message, 'timestamp') and message.timestamp:
308
+ if hasattr(message, "timestamp") and message.timestamp:
228
309
  event_attrs["timestamp"] = str(message.timestamp)
229
-
310
+
230
311
  current_span.add_event(f"log.{message.level.lower()}", event_attrs)
231
312
  except RecursionError:
232
313
  # Avoid infinite recursion if logging causes more logs
@@ -43,16 +43,16 @@ class SpanBuilder:
43
43
  """Create root span for test suite."""
44
44
  attrs = AttributeExtractor.from_suite(data, result)
45
45
  name = SpanBuilder._add_prefix(data.name, "SUITE", prefix_style)
46
-
46
+
47
47
  # Create context with baggage
48
48
  ctx = baggage.set_baggage("rf.suite.id", result.id)
49
49
  ctx = baggage.set_baggage("rf.version", robot.version.get_version(), ctx)
50
-
50
+
51
51
  # Add suite metadata to baggage (limit to avoid too much data)
52
- if hasattr(data, 'metadata') and data.metadata:
52
+ if hasattr(data, "metadata") and data.metadata:
53
53
  for key, value in list(data.metadata.items())[:5]: # Limit to 5 metadata items
54
54
  ctx = baggage.set_baggage(f"rf.suite.metadata.{key}", str(value), ctx)
55
-
55
+
56
56
  span = tracer.start_span(name, context=ctx, kind=trace.SpanKind.INTERNAL, attributes=attrs)
57
57
  return span
58
58
 
@@ -67,10 +67,12 @@ class SpanBuilder:
67
67
  return span
68
68
 
69
69
  @staticmethod
70
- def create_keyword_span(tracer, data, result, parent_context, max_arg_length=200, prefix_style="none"):
70
+ def create_keyword_span(
71
+ tracer, data, result, parent_context, max_arg_length=200, prefix_style="none"
72
+ ):
71
73
  """Create child span for keyword."""
72
74
  attrs = AttributeExtractor.from_keyword(data, result, max_arg_length)
73
-
75
+
74
76
  # Build keyword name with arguments (like RF test step line)
75
77
  kw_name = data.name
76
78
  if data.args:
@@ -80,16 +82,16 @@ class SpanBuilder:
80
82
  if len(args_str) > 100:
81
83
  args_str = args_str[:100] + "..."
82
84
  kw_name = f"{data.name} {args_str}"
83
-
85
+
84
86
  # Determine span type for prefix
85
87
  if data.type in ("SETUP", "TEARDOWN"):
86
88
  span_type = data.type
87
89
  else:
88
90
  span_type = "KEYWORD"
89
-
91
+
90
92
  # Add prefix based on style
91
93
  kw_name = SpanBuilder._add_prefix(kw_name, span_type, prefix_style)
92
-
94
+
93
95
  span = tracer.start_span(
94
96
  kw_name, context=parent_context, kind=trace.SpanKind.INTERNAL, attributes=attrs
95
97
  )
@@ -115,14 +117,14 @@ class SpanBuilder:
115
117
  "message": result.message,
116
118
  "rf.status": "FAIL",
117
119
  }
118
-
120
+
119
121
  # Try to extract exception type if available
120
- if hasattr(result, 'error') and result.error:
122
+ if hasattr(result, "error") and result.error:
121
123
  event_attrs["exception.type"] = type(result.error).__name__
122
124
  event_attrs["exception.message"] = str(result.error)
123
-
125
+
124
126
  # Add timestamp
125
- if hasattr(result, 'endtime') and result.endtime:
127
+ if hasattr(result, "endtime") and result.endtime:
126
128
  event_attrs["timestamp"] = result.endtime
127
-
129
+
128
130
  span.add_event("test.failed", event_attrs)
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: robotframework-tracer
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: OpenTelemetry distributed tracing for Robot Framework
5
5
  Author: Robot Framework Tracer Contributors
6
6
  License: Apache-2.0
@@ -49,6 +49,7 @@ This enables you to:
49
49
  - **Analyze performance** and identify slow keywords or tests
50
50
  - **Correlate tests with application traces** in distributed systems
51
51
  - **Monitor test execution** across CI/CD pipelines
52
+ - **Propagate trace context** to your System Under Test (SUT)
52
53
 
53
54
  ![Robot Framework Trace Visualization](docs/robotframework-trace.jpg)
54
55
 
@@ -114,6 +115,33 @@ robot --listener robotframework_tracer.TracingListener tests/
114
115
 
115
116
  Open http://localhost:16686 in your browser to see your test traces in Jaeger UI.
116
117
 
118
+ ## Trace Context Propagation
119
+
120
+ The tracer automatically makes trace context available as Robot Framework variables for propagating to your System Under Test:
121
+
122
+ ```robot
123
+ *** Test Cases ***
124
+ Test API With Distributed Tracing
125
+ # HTTP headers automatically include trace context
126
+ ${response}= POST http://my-sut/api
127
+ ... json={"data": "test"}
128
+ ... headers=${TRACE_HEADERS}
129
+
130
+ # For custom protocols, use individual components
131
+ ${diameter_msg}= Create Diameter Request
132
+ ... trace_id=${TRACE_ID}
133
+ ... span_id=${SPAN_ID}
134
+ ```
135
+
136
+ **Available variables:**
137
+ - `${TRACE_HEADERS}` - HTTP headers dictionary
138
+ - `${TRACE_ID}` - 32-character hex trace ID
139
+ - `${SPAN_ID}` - 16-character hex span ID
140
+ - `${TRACEPARENT}` - W3C traceparent header
141
+ - `${TRACESTATE}` - W3C tracestate header
142
+
143
+ See [docs/trace-propagation.md](docs/trace-propagation.md) for complete examples.
144
+
117
145
  ## Configuration
118
146
 
119
147
  ### Basic usage
@@ -236,7 +264,7 @@ Apache License 2.0 - See [docs/LICENSE](docs/LICENSE) for details.
236
264
 
237
265
  ## Status
238
266
 
239
- **Current Version:** v0.1.0
240
- **Status:** Production-ready MVP
267
+ **Current Version:** v0.2.0
268
+ **Status:** Production-ready with trace propagation
241
269
 
242
270
  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.
@@ -1 +0,0 @@
1
- __version__ = "0.1.1"