rebrandly-otel 0.1.9__tar.gz → 0.1.11__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.

Potentially problematic release.


This version of rebrandly-otel might be problematic. Click here for more details.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rebrandly_otel
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Python OTEL wrapper by Rebrandly
5
5
  Home-page: https://github.com/rebrandly/rebrandly-otel-python
6
6
  Author: Antonio Romano
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rebrandly_otel
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Python OTEL wrapper by Rebrandly
5
5
  Home-page: https://github.com/rebrandly/rebrandly-otel-python
6
6
  Author: Antonio Romano
@@ -5,7 +5,7 @@ with open("README.md", "r") as fh:
5
5
 
6
6
  setuptools.setup(
7
7
  name="rebrandly_otel",
8
- version="0.1.9",
8
+ version="0.1.11",
9
9
  author="Antonio Romano",
10
10
  author_email="antonio@rebrandly.com",
11
11
  description="Python OTEL wrapper by Rebrandly",
@@ -99,6 +99,9 @@ class RebrandlyOTEL:
99
99
  return wrapper
100
100
  return decorator
101
101
 
102
+ # Fix for the lambda_handler method in rebrandly_otel.py
103
+ # Replace the lambda_handler method (around line 132) with this fixed version:
104
+
102
105
  def lambda_handler(self,
103
106
  name: Optional[str] = None,
104
107
  attributes: Optional[Dict[str, Any]] = None,
@@ -107,22 +110,6 @@ class RebrandlyOTEL:
107
110
  skip_aws_link: bool = True):
108
111
  """
109
112
  Decorator specifically for Lambda handlers with automatic flushing.
110
-
111
- Args:
112
- name: Optional span name (defaults to 'lambda.{function_name}')
113
- attributes: Additional span attributes
114
- kind: Span kind (defaults to SERVER)
115
- auto_flush: If True, automatically flush after handler completes
116
-
117
- Usage:
118
- @lambda_handler()
119
- def my_handler(event, context): ...
120
-
121
- @lambda_handler(name="custom_span_name")
122
- def my_handler(event, context): ...
123
-
124
- @lambda_handler(name="my_span", attributes={"env": "prod"})
125
- def my_handler(event, context): ...
126
113
  """
127
114
  def decorator(func):
128
115
  @functools.wraps(func)
@@ -150,50 +137,90 @@ class RebrandlyOTEL:
150
137
  # Increment invocations counter
151
138
  self.meter.GlobalMetrics.invocations.add(1, {'function': span_name})
152
139
 
153
- # Create span and execute function
154
- record = None
155
- span_function = self.span
156
- if not skip_aws_link and event is not None and 'Records' in event and len(event['Records']) > 0 and 'MessageAttributes' in event['Records'][0]:
157
- span_function = self.aws_message_span
158
- record = event['Records'][0]
159
-
160
- with span_function(span_name, message=record, attributes=span_attributes, kind=kind) as span_context:
161
- # Add event type as span event
162
- if event is not None:
163
- span_context.add_event("lambda.invocation.start", attributes={"event.type": type(event).__name__})
164
-
165
- result = func(event, context)
166
- else:
167
- result = func()
168
-
169
- # Add result information if applicable
170
- if isinstance(result, dict):
171
- if 'statusCode' in result:
172
- span_context.set_attribute("http.status_code", result['statusCode'])
173
- # Set span status based on HTTP status code
174
- if result['statusCode'] >= 400:
175
- span_context.set_status(Status(StatusCode.ERROR, f"HTTP {result['statusCode']}"))
176
- else:
177
- span_context.set_status(Status(StatusCode.OK))
178
-
179
- # Increment success counter
180
- self.meter.GlobalMetrics.successful_invocations.add(1, {'function': span_name})
181
-
182
- return result
140
+ # Determine if we should extract context from AWS message
141
+ extracted_context = None
142
+ if not skip_aws_link and event is not None and 'Records' in event and len(event['Records']) > 0:
143
+ first_record = event['Records'][0]
144
+
145
+ # Check different message formats
146
+ if 'messageAttributes' in first_record or 'MessageAttributes' in first_record:
147
+ # SQS format
148
+ extracted_context = self.extract_context(first_record)
149
+ elif 'Sns' in first_record and 'MessageAttributes' in first_record['Sns']:
150
+ # SNS format
151
+ extracted_context = self.extract_context(first_record)
152
+
153
+ # Create span - use extracted context if available
154
+ if extracted_context:
155
+ token = self.attach_context(extracted_context)
156
+ try:
157
+ with self.span(span_name, attributes=span_attributes, kind=kind) as span_context:
158
+ # Add event type as span event
159
+ if event is not None:
160
+ span_context.add_event("lambda.invocation.start",
161
+ attributes={"event.type": type(event).__name__})
162
+
163
+ result = func(event, context) if event is not None else func()
164
+
165
+ # Handle result
166
+ self._process_lambda_result(result, span_context, span_name)
167
+ finally:
168
+ self.detach_context(token)
169
+ else:
170
+ # No context extraction needed
171
+ with self.span(span_name, attributes=span_attributes, kind=kind) as span_context:
172
+ # Add event type as span event
173
+ if event is not None:
174
+ span_context.add_event("lambda.invocation.start",
175
+ attributes={"event.type": type(event).__name__})
176
+
177
+ result = func(event, context) if event is not None else func()
178
+
179
+ # Handle result
180
+ self._process_lambda_result(result, span_context, span_name)
181
+
182
+ return result
183
183
 
184
184
  except Exception as e:
185
185
  # Increment error counter
186
186
  self.meter.GlobalMetrics.error_invocations.add(1, {'function': span_name, 'error': type(e).__name__})
187
+
188
+ # Log error with context
189
+ self.logger.logger.error(f"Lambda execution failed: {e}", exc_info=True)
187
190
  raise
188
191
 
189
192
  finally:
193
+ # Record duration
194
+ duration = (datetime.now() - start_func).total_seconds() * 1000
195
+ self.meter.GlobalMetrics.duration.record(duration, {'function': span_name})
196
+
190
197
  if auto_flush:
191
- self.logger.logger.info(f"[OTEL] Lambda handler '{span_name}' completed, flushing telemetry...")
192
198
  self.force_flush(start_datetime=start_func)
193
199
 
194
200
  return wrapper
195
201
  return decorator
196
202
 
203
+ def _process_lambda_result(self, result, span_context, span_name):
204
+ """Helper method to process Lambda result and update span accordingly"""
205
+ if isinstance(result, dict):
206
+ if 'statusCode' in result:
207
+ span_context.set_attribute("http.status_code", result['statusCode'])
208
+ # Set span status based on HTTP status code
209
+ if result['statusCode'] >= 400:
210
+ span_context.set_status(Status(StatusCode.ERROR, f"HTTP {result['statusCode']}"))
211
+ else:
212
+ span_context.set_status(Status(StatusCode.OK))
213
+
214
+ # Add completion event
215
+ span_context.add_event("lambda.invocation.complete",
216
+ attributes={"success": result.get('statusCode', 200) < 400})
217
+ else:
218
+ span_context.set_status(Status(StatusCode.OK))
219
+ span_context.add_event("lambda.invocation.complete", attributes={"success": True})
220
+
221
+ # Increment success counter
222
+ self.meter.GlobalMetrics.successful_invocations.add(1, {'function': span_name})
223
+
197
224
  def aws_message_handler(self,
198
225
  name: Optional[str] = None,
199
226
  attributes: Optional[Dict[str, Any]] = None,
@@ -269,7 +296,6 @@ class RebrandlyOTEL:
269
296
 
270
297
  finally:
271
298
  if auto_flush:
272
- self.logger.logger.info(f"[OTEL] Lambda handler '{span_name}' completed, flushing telemetry...")
273
299
  self.force_flush(start_datetime=start_func)
274
300
 
275
301
  return wrapper
@@ -298,7 +324,7 @@ class RebrandlyOTEL:
298
324
  self.meter.GlobalMetrics.duration.record(duration, {'source': 'force_flush'})
299
325
  self.meter.GlobalMetrics.memory_usage_bytes.set(memory.used)
300
326
  self.meter.GlobalMetrics.cpu_usage_percentage.set(cpu_percent)
301
- self.logger.logger.info(f"[OTEL] Function duration: {duration}ms, Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
327
+ self.logger.logger.info(f"Function duration: {duration}ms, Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
302
328
 
303
329
  try:
304
330
  # Flush traces
@@ -336,9 +362,9 @@ class RebrandlyOTEL:
336
362
  self._meter.shutdown()
337
363
  if self._logger:
338
364
  self._logger.shutdown()
339
- self.logger.logger.info("[OTEL] Shutdown completed")
365
+ print("[OTEL] Shutdown completed")
340
366
  except Exception as e:
341
- self.logger.logger.info(f"[OTEL] Error during shutdown: {e}")
367
+ print(f"[OTEL] Error during shutdown: {e}")
342
368
 
343
369
  def _detect_lambda_trigger(self, event: Any) -> str:
344
370
  """Detect Lambda trigger type from event."""
@@ -407,43 +433,15 @@ class RebrandlyOTEL:
407
433
  message: Dict[str, Any]=None,
408
434
  attributes: Optional[Dict[str, Any]] = None,
409
435
  kind: SpanKind = SpanKind.CONSUMER):
410
- """Create span from AWS message with extracted context."""
411
- # Extract context from the message if it contains trace context
412
- token = None
413
- if message and isinstance(message, dict):
414
- carrier = {}
436
+ """Create span from AWS message - properly handling trace context."""
437
+
438
+ from opentelemetry import trace, context as otel_context
415
439
 
416
- # Check for trace context in different possible locations
417
- if 'MessageAttributes' in message:
418
- # SQS format
419
- for key, value in message.get('MessageAttributes', {}).items():
420
- if isinstance(value, dict) and 'StringValue' in value:
421
- carrier[key] = value['StringValue']
422
- elif 'Sns' in message and 'MessageAttributes' in message['Sns']:
423
- # SNS format - MessageAttributes are nested under 'Sns'
424
- for key, value in message['Sns'].get('MessageAttributes', {}).items():
425
- if isinstance(value, dict):
426
- # SNS uses 'Value' instead of 'StringValue'
427
- if 'Value' in value:
428
- carrier[key] = value['Value']
429
- elif 'StringValue' in value:
430
- carrier[key] = value['StringValue']
431
- elif 'messageAttributes' in message:
432
- # Alternative format
433
- for key, value in message.get('messageAttributes', {}).items():
434
- if isinstance(value, dict) and 'stringValue' in value:
435
- carrier[key] = value['stringValue']
436
-
437
- # If we found trace context, attach it
438
- if carrier:
439
- token = self.attach_context(carrier)
440
-
441
- # Create a span with the potentially extracted context
442
440
  combined_attributes = attributes or {}
443
441
 
444
- # Add message-specific attributes
442
+ # Extract message attributes for linking/attributes
445
443
  if message and isinstance(message, dict):
446
- # Add SNS-specific attributes
444
+ # Add message-specific attributes
447
445
  if 'Sns' in message:
448
446
  sns_msg = message['Sns']
449
447
  if 'MessageId' in sns_msg:
@@ -451,24 +449,40 @@ class RebrandlyOTEL:
451
449
  if 'TopicArn' in sns_msg:
452
450
  combined_attributes['messaging.destination'] = sns_msg['TopicArn']
453
451
  combined_attributes['messaging.system'] = 'aws_sns'
454
- # Add SQS-specific attributes
452
+
453
+ # Check for trace context in SNS
454
+ if 'MessageAttributes' in sns_msg:
455
+ for key, value in sns_msg['MessageAttributes'].items():
456
+ if key == 'traceparent' and 'Value' in value:
457
+ combined_attributes['message.traceparent'] = value['Value']
458
+ combined_attributes['message.has_trace_context'] = True
459
+
455
460
  elif 'messageId' in message:
461
+ # SQS message
456
462
  combined_attributes['messaging.message_id'] = message['messageId']
457
463
  if 'eventSource' in message:
458
464
  combined_attributes['messaging.system'] = message['eventSource']
459
465
 
460
- # Add common attributes
466
+ # Check for trace context in SQS
467
+ if 'MessageAttributes' in message or 'messageAttributes' in message:
468
+ attrs = message.get('MessageAttributes') or message.get('messageAttributes', {})
469
+ for key, value in attrs.items():
470
+ if key == 'traceparent':
471
+ tp_value = value.get('StringValue') or value.get('stringValue', '')
472
+ combined_attributes['message.traceparent'] = tp_value
473
+ combined_attributes['message.has_trace_context'] = True
474
+
461
475
  if 'awsRegion' in message:
462
476
  combined_attributes['cloud.region'] = message['awsRegion']
463
477
 
464
- try:
465
- # Use the regular span method which properly handles context
466
- with self.span(name, attributes=combined_attributes, kind=kind) as span:
467
- yield span
468
- finally:
469
- # Detach context if we attached one
470
- if token:
471
- self.detach_context(token)
478
+ # Use the tracer's start_span method directly to ensure it works
479
+ # This creates a child span of whatever is currently active
480
+ with self.tracer.start_span(
481
+ name=name,
482
+ attributes=combined_attributes,
483
+ kind=kind
484
+ ) as span:
485
+ yield span
472
486
 
473
487
 
474
488
  # Create Singleton instance
@@ -93,6 +93,7 @@ class RebrandlyTracer:
93
93
  attributes: Optional[Dict[str, Any]] = None,
94
94
  kind: trace.SpanKind = trace.SpanKind.INTERNAL) -> ContextManager[Span]:
95
95
  """Start a new span as the current span."""
96
+ # Ensure we use the tracer to create a child span of the current span
96
97
  with self.tracer.start_as_current_span(
97
98
  name,
98
99
  attributes=attributes,
File without changes