rebrandly-otel 0.1.10__tar.gz → 0.1.13__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.10
3
+ Version: 0.1.13
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.10
3
+ Version: 0.1.13
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.10",
8
+ version="0.1.13",
9
9
  author="Antonio Romano",
10
10
  author_email="antonio@rebrandly.com",
11
11
  description="Python OTEL wrapper by Rebrandly",
@@ -101,102 +101,115 @@ class RebrandlyOTEL:
101
101
 
102
102
  # Fix for the lambda_handler method in rebrandly_otel.py
103
103
  # Replace the lambda_handler method (around line 132) with this fixed version:
104
-
105
104
  def lambda_handler(self,
106
105
  name: Optional[str] = None,
107
106
  attributes: Optional[Dict[str, Any]] = None,
108
- kind: SpanKind = SpanKind.CONSUMER,
107
+ kind: SpanKind = SpanKind.SERVER,
109
108
  auto_flush: bool = True,
110
- skip_aws_link: bool = True):
109
+ skip_aws_link: bool = False):
111
110
  """
112
111
  Decorator specifically for Lambda handlers with automatic flushing.
113
112
  """
114
113
  def decorator(func):
115
114
  @functools.wraps(func)
116
- def wrapper(event=None, context=None):
115
+ def wrapper(event=None, lambda_context=None):
117
116
  # Determine span name
118
117
  span_name = name or f"lambda.{func.__name__}"
119
- start_func = datetime.now()
118
+ start_time = datetime.now()
120
119
 
121
120
  # Build span attributes
122
121
  span_attributes = attributes or {}
123
-
124
122
  span_attributes['faas.trigger'] = self._detect_lambda_trigger(event)
125
123
 
126
124
  # Add Lambda-specific attributes if context is available
127
- if context is not None:
125
+ if lambda_context is not None:
128
126
  span_attributes.update({
129
- "faas.execution": getattr(context, 'aws_request_id', 'unknown'),
130
- "faas.id": getattr(context, 'function_name', 'unknown'),
131
- "cloud.provider": "aws",
132
- "cloud.platform": "aws_lambda"
127
+ "faas.execution": getattr(lambda_context, 'aws_request_id', 'unknown'),
128
+ "faas.id": getattr(lambda_context, 'function_arn', 'unknown'),
129
+ "faas.name": getattr(lambda_context, 'function_name', 'unknown'),
130
+ "faas.version": getattr(lambda_context, 'function_version', 'unknown')
133
131
  })
134
132
 
133
+ # Handle context extraction from AWS events
134
+ token = None
135
+ if not skip_aws_link and event and isinstance(event, dict) and 'Records' in event:
136
+ first_record = event['Records'][0] if event['Records'] else None
137
+ if first_record:
138
+ carrier = {}
139
+
140
+ # Extract from SQS
141
+ if 'MessageAttributes' in first_record:
142
+ for key, value in first_record['MessageAttributes'].items():
143
+ if isinstance(value, dict) and 'StringValue' in value:
144
+ carrier[key] = value['StringValue']
145
+
146
+ # Extract from SNS
147
+ elif 'Sns' in first_record and 'MessageAttributes' in first_record['Sns']:
148
+ for key, value in first_record['Sns']['MessageAttributes'].items():
149
+ if isinstance(value, dict):
150
+ if 'Value' in value:
151
+ carrier[key] = value['Value']
152
+ elif 'StringValue' in value:
153
+ carrier[key] = value['StringValue']
154
+
155
+ # Attach extracted context
156
+ if carrier:
157
+ from opentelemetry import propagate, context as otel_context
158
+ extracted_context = propagate.extract(carrier)
159
+ token = otel_context.attach(extracted_context)
160
+ span_attributes['message.has_trace_context'] = True
161
+
135
162
  result = None
136
163
  try:
137
- # Increment invocations counter
164
+ # Increment invocation counter
138
165
  self.meter.GlobalMetrics.invocations.add(1, {'function': span_name})
139
166
 
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]
167
+ # Create and execute within span
168
+ with self.tracer.start_span(
169
+ name=span_name,
170
+ attributes=span_attributes,
171
+ kind=kind
172
+ ) as span:
173
+ # Add invocation start event
174
+ span.add_event("lambda.invocation.start", {
175
+ 'event.type': type(event).__name__ if event else 'None'
176
+ })
144
177
 
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)
178
+ # Execute handler
179
+ result = func(event, lambda_context)
180
+
181
+ # Process result
182
+ self._process_lambda_result(result, span, span_name)
181
183
 
182
184
  return result
183
185
 
184
186
  except Exception as e:
185
187
  # Increment error counter
186
- self.meter.GlobalMetrics.error_invocations.add(1, {'function': span_name, 'error': type(e).__name__})
188
+ self.meter.GlobalMetrics.error_invocations.add(1, {
189
+ 'function': span_name,
190
+ 'error': type(e).__name__
191
+ })
187
192
 
188
- # Log error with context
193
+ # Log error
189
194
  self.logger.logger.error(f"Lambda execution failed: {e}", exc_info=True)
190
195
  raise
191
196
 
192
197
  finally:
198
+ # Always detach context if we attached it
199
+ if token is not None:
200
+ from opentelemetry import context as otel_context
201
+ otel_context.detach(token)
202
+
193
203
  # Record duration
194
- duration = (datetime.now() - start_func).total_seconds() * 1000
204
+ duration = (datetime.now() - start_time).total_seconds() * 1000
195
205
  self.meter.GlobalMetrics.duration.record(duration, {'function': span_name})
196
206
 
207
+ # Force flush if enabled
197
208
  if auto_flush:
198
- self.logger.logger.info(f"[OTEL] Lambda handler '{span_name}' completed in {duration:.2f}ms, flushing telemetry...")
199
- self.force_flush(start_datetime=start_func)
209
+ self.logger.logger.info(f"[OTEL] Lambda '{span_name}' completed in {duration:.2f}ms, flushing...")
210
+ flush_success = self.force_flush(timeout_millis=1000)
211
+ if not flush_success:
212
+ self.logger.logger.warning("[OTEL] Force flush may not have completed fully")
200
213
 
201
214
  return wrapper
202
215
  return decorator
@@ -297,7 +310,6 @@ class RebrandlyOTEL:
297
310
 
298
311
  finally:
299
312
  if auto_flush:
300
- self.logger.logger.info(f"[OTEL] Lambda handler '{span_name}' completed, flushing telemetry...")
301
313
  self.force_flush(start_datetime=start_func)
302
314
 
303
315
  return wrapper
@@ -326,7 +338,7 @@ class RebrandlyOTEL:
326
338
  self.meter.GlobalMetrics.duration.record(duration, {'source': 'force_flush'})
327
339
  self.meter.GlobalMetrics.memory_usage_bytes.set(memory.used)
328
340
  self.meter.GlobalMetrics.cpu_usage_percentage.set(cpu_percent)
329
- self.logger.logger.info(f"[OTEL] Function duration: {duration}ms, Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
341
+ self.logger.logger.info(f"Function duration: {duration}ms, Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
330
342
 
331
343
  try:
332
344
  # Flush traces
@@ -364,9 +376,9 @@ class RebrandlyOTEL:
364
376
  self._meter.shutdown()
365
377
  if self._logger:
366
378
  self._logger.shutdown()
367
- self.logger.logger.info("[OTEL] Shutdown completed")
379
+ print("[OTEL] Shutdown completed")
368
380
  except Exception as e:
369
- self.logger.logger.info(f"[OTEL] Error during shutdown: {e}")
381
+ print(f"[OTEL] Error during shutdown: {e}")
370
382
 
371
383
  def _detect_lambda_trigger(self, event: Any) -> str:
372
384
  """Detect Lambda trigger type from event."""
@@ -435,43 +447,15 @@ class RebrandlyOTEL:
435
447
  message: Dict[str, Any]=None,
436
448
  attributes: Optional[Dict[str, Any]] = None,
437
449
  kind: SpanKind = SpanKind.CONSUMER):
438
- """Create span from AWS message with extracted context."""
439
- # Extract context from the message if it contains trace context
440
- token = None
441
- if message and isinstance(message, dict):
442
- carrier = {}
450
+ """Create span from AWS message - properly handling trace context."""
451
+
452
+ from opentelemetry import trace, context as otel_context
443
453
 
444
- # Check for trace context in different possible locations
445
- if 'MessageAttributes' in message:
446
- # SQS format
447
- for key, value in message.get('MessageAttributes', {}).items():
448
- if isinstance(value, dict) and 'StringValue' in value:
449
- carrier[key] = value['StringValue']
450
- elif 'Sns' in message and 'MessageAttributes' in message['Sns']:
451
- # SNS format - MessageAttributes are nested under 'Sns'
452
- for key, value in message['Sns'].get('MessageAttributes', {}).items():
453
- if isinstance(value, dict):
454
- # SNS uses 'Value' instead of 'StringValue'
455
- if 'Value' in value:
456
- carrier[key] = value['Value']
457
- elif 'StringValue' in value:
458
- carrier[key] = value['StringValue']
459
- elif 'messageAttributes' in message:
460
- # Alternative format
461
- for key, value in message.get('messageAttributes', {}).items():
462
- if isinstance(value, dict) and 'stringValue' in value:
463
- carrier[key] = value['stringValue']
464
-
465
- # If we found trace context, attach it
466
- if carrier:
467
- token = self.attach_context(carrier)
468
-
469
- # Create a span with the potentially extracted context
470
454
  combined_attributes = attributes or {}
471
455
 
472
- # Add message-specific attributes
456
+ # Extract message attributes for linking/attributes
473
457
  if message and isinstance(message, dict):
474
- # Add SNS-specific attributes
458
+ # Add message-specific attributes
475
459
  if 'Sns' in message:
476
460
  sns_msg = message['Sns']
477
461
  if 'MessageId' in sns_msg:
@@ -479,24 +463,40 @@ class RebrandlyOTEL:
479
463
  if 'TopicArn' in sns_msg:
480
464
  combined_attributes['messaging.destination'] = sns_msg['TopicArn']
481
465
  combined_attributes['messaging.system'] = 'aws_sns'
482
- # Add SQS-specific attributes
466
+
467
+ # Check for trace context in SNS
468
+ if 'MessageAttributes' in sns_msg:
469
+ for key, value in sns_msg['MessageAttributes'].items():
470
+ if key == 'traceparent' and 'Value' in value:
471
+ combined_attributes['message.traceparent'] = value['Value']
472
+ combined_attributes['message.has_trace_context'] = True
473
+
483
474
  elif 'messageId' in message:
475
+ # SQS message
484
476
  combined_attributes['messaging.message_id'] = message['messageId']
485
477
  if 'eventSource' in message:
486
478
  combined_attributes['messaging.system'] = message['eventSource']
487
479
 
488
- # Add common attributes
480
+ # Check for trace context in SQS
481
+ if 'MessageAttributes' in message or 'messageAttributes' in message:
482
+ attrs = message.get('MessageAttributes') or message.get('messageAttributes', {})
483
+ for key, value in attrs.items():
484
+ if key == 'traceparent':
485
+ tp_value = value.get('StringValue') or value.get('stringValue', '')
486
+ combined_attributes['message.traceparent'] = tp_value
487
+ combined_attributes['message.has_trace_context'] = True
488
+
489
489
  if 'awsRegion' in message:
490
490
  combined_attributes['cloud.region'] = message['awsRegion']
491
491
 
492
- try:
493
- # Use the regular span method which properly handles context
494
- with self.span(name, attributes=combined_attributes, kind=kind) as span:
495
- yield span
496
- finally:
497
- # Detach context if we attached one
498
- if token:
499
- self.detach_context(token)
492
+ # Use the tracer's start_span method directly to ensure it works
493
+ # This creates a child span of whatever is currently active
494
+ with self.tracer.start_span(
495
+ name=name,
496
+ attributes=combined_attributes,
497
+ kind=kind
498
+ ) as span:
499
+ yield span
500
500
 
501
501
 
502
502
  # 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