rebrandly-otel 0.1.13__tar.gz → 0.1.16__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.13
3
+ Version: 0.1.16
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.13
3
+ Version: 0.1.16
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.13",
8
+ version="0.1.16",
9
9
  author="Antonio Romano",
10
10
  author_email="antonio@rebrandly.com",
11
11
  description="Python OTEL wrapper by Rebrandly",
@@ -3,7 +3,6 @@ from .rebrandly_otel import *
3
3
 
4
4
  # Explicitly define what's available
5
5
  __all__ = [
6
- 'otel',
7
6
  'lambda_handler',
8
7
  'span',
9
8
  'aws_message_span',
@@ -1,6 +1,7 @@
1
1
  # rebrandly_otel.py
2
2
  """
3
3
  Rebrandly OpenTelemetry SDK - Simplified instrumentation for Rebrandly services.
4
+ Updated for consistency with Node.js SDK
4
5
  """
5
6
  import time
6
7
  import psutil
@@ -99,8 +100,6 @@ class RebrandlyOTEL:
99
100
  return wrapper
100
101
  return decorator
101
102
 
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
103
  def lambda_handler(self,
105
104
  name: Optional[str] = None,
106
105
  attributes: Optional[Dict[str, Any]] = None,
@@ -132,6 +131,7 @@ class RebrandlyOTEL:
132
131
 
133
132
  # Handle context extraction from AWS events
134
133
  token = None
134
+
135
135
  if not skip_aws_link and event and isinstance(event, dict) and 'Records' in event:
136
136
  first_record = event['Records'][0] if event['Records'] else None
137
137
  if first_record:
@@ -142,6 +142,9 @@ class RebrandlyOTEL:
142
142
  for key, value in first_record['MessageAttributes'].items():
143
143
  if isinstance(value, dict) and 'StringValue' in value:
144
144
  carrier[key] = value['StringValue']
145
+ if ('messageAttributes' in first_record and 'traceparent' in first_record['messageAttributes']
146
+ and 'stringValue' in first_record['messageAttributes']['traceparent']):
147
+ carrier['traceparent'] = first_record['messageAttributes']['traceparent']['stringValue']
145
148
 
146
149
  # Extract from SNS
147
150
  elif 'Sns' in first_record and 'MessageAttributes' in first_record['Sns']:
@@ -157,11 +160,10 @@ class RebrandlyOTEL:
157
160
  from opentelemetry import propagate, context as otel_context
158
161
  extracted_context = propagate.extract(carrier)
159
162
  token = otel_context.attach(extracted_context)
160
- span_attributes['message.has_trace_context'] = True
161
163
 
162
164
  result = None
163
165
  try:
164
- # Increment invocation counter
166
+ # Increment invocation counter with standardized metric name
165
167
  self.meter.GlobalMetrics.invocations.add(1, {'function': span_name})
166
168
 
167
169
  # Create and execute within span
@@ -170,26 +172,63 @@ class RebrandlyOTEL:
170
172
  attributes=span_attributes,
171
173
  kind=kind
172
174
  ) as span:
173
- # Add invocation start event
174
- span.add_event("lambda.invocation.start", {
175
+ # Add invocation start event with standardized attributes
176
+ start_event_attrs = {
175
177
  'event.type': type(event).__name__ if event else 'None'
176
- })
178
+ }
179
+
180
+ # Add records count if present
181
+ if event and isinstance(event, dict) and 'Records' in event:
182
+ start_event_attrs['event.records'] = f"{len(event['Records'])}"
183
+
184
+ span.add_event("lambda.invocation.start", start_event_attrs)
177
185
 
178
186
  # Execute handler
179
187
  result = func(event, lambda_context)
180
188
 
181
- # Process result
182
- self._process_lambda_result(result, span, span_name)
189
+ # Process result with standardized attributes
190
+ success = True
191
+ complete_event_attrs = {}
192
+
193
+ if isinstance(result, dict) and 'statusCode' in result:
194
+ span.set_attribute("http.status_code", result['statusCode'])
195
+ complete_event_attrs['status_code'] = result['statusCode']
196
+
197
+ # Set span status based on HTTP status code
198
+ if result['statusCode'] >= 400:
199
+ success = False
200
+ span.set_status(Status(StatusCode.ERROR, f"HTTP {result['statusCode']}"))
201
+ else:
202
+ span.set_status(Status(StatusCode.OK))
203
+ else:
204
+ span.set_status(Status(StatusCode.OK))
205
+
206
+ # Add completion event with success indicator
207
+ complete_event_attrs['success'] = success
208
+ span.add_event("lambda.invocation.complete", complete_event_attrs)
209
+
210
+ # Increment success counter with standardized metric
211
+ self.meter.GlobalMetrics.successful_invocations.add(1, {'function': span_name})
183
212
 
184
213
  return result
185
214
 
186
215
  except Exception as e:
187
- # Increment error counter
216
+ # Increment error counter with standardized metric
188
217
  self.meter.GlobalMetrics.error_invocations.add(1, {
189
218
  'function': span_name,
190
219
  'error': type(e).__name__
191
220
  })
192
221
 
222
+ # Add failed completion event with error attribute (THIS IS THE KEY ADDITION)
223
+ span.add_event("lambda.invocation.complete", {
224
+ 'success': False,
225
+ 'error': type(e).__name__
226
+ })
227
+
228
+ # Record the exception in the span
229
+ span.record_exception(e)
230
+ span.set_status(Status(StatusCode.ERROR, str(e)))
231
+
193
232
  # Log error
194
233
  self.logger.logger.error(f"Lambda execution failed: {e}", exc_info=True)
195
234
  raise
@@ -200,81 +239,68 @@ class RebrandlyOTEL:
200
239
  from opentelemetry import context as otel_context
201
240
  otel_context.detach(token)
202
241
 
203
- # Record duration
242
+ # Record duration with standardized metric
204
243
  duration = (datetime.now() - start_time).total_seconds() * 1000
205
244
  self.meter.GlobalMetrics.duration.record(duration, {'function': span_name})
206
245
 
207
246
  # Force flush if enabled
208
247
  if auto_flush:
209
- self.logger.logger.info(f"[OTEL] Lambda '{span_name}' completed in {duration:.2f}ms, flushing...")
248
+ self.logger.logger.info(f"[Rebrandly OTEL] Lambda '{span_name}' completed in {duration:.2f}ms, flushing...")
210
249
  flush_success = self.force_flush(timeout_millis=1000)
211
250
  if not flush_success:
212
- self.logger.logger.warning("[OTEL] Force flush may not have completed fully")
251
+ self.logger.logger.warning("[Rebrandly OTEL] Force flush may not have completed fully")
213
252
 
214
253
  return wrapper
215
254
  return decorator
216
255
 
217
- def _process_lambda_result(self, result, span_context, span_name):
218
- """Helper method to process Lambda result and update span accordingly"""
219
- if isinstance(result, dict):
220
- if 'statusCode' in result:
221
- span_context.set_attribute("http.status_code", result['statusCode'])
222
- # Set span status based on HTTP status code
223
- if result['statusCode'] >= 400:
224
- span_context.set_status(Status(StatusCode.ERROR, f"HTTP {result['statusCode']}"))
225
- else:
226
- span_context.set_status(Status(StatusCode.OK))
227
-
228
- # Add completion event
229
- span_context.add_event("lambda.invocation.complete",
230
- attributes={"success": result.get('statusCode', 200) < 400})
231
- else:
232
- span_context.set_status(Status(StatusCode.OK))
233
- span_context.add_event("lambda.invocation.complete", attributes={"success": True})
234
-
235
- # Increment success counter
236
- self.meter.GlobalMetrics.successful_invocations.add(1, {'function': span_name})
237
-
238
256
  def aws_message_handler(self,
239
- name: Optional[str] = None,
240
- attributes: Optional[Dict[str, Any]] = None,
241
- kind: SpanKind = SpanKind.CONSUMER,
242
- auto_flush: bool = True):
257
+ name: Optional[str] = None,
258
+ attributes: Optional[Dict[str, Any]] = None,
259
+ kind: SpanKind = SpanKind.CONSUMER,
260
+ auto_flush: bool = True):
243
261
  """
244
- require a record object parameter to the function
262
+ Decorator for AWS message handlers (SQS/SNS record processing).
263
+ Requires a record object parameter to the function.
245
264
  """
246
265
  def decorator(func):
247
266
  @functools.wraps(func)
248
267
  def wrapper(record=None, *args, **kwargs):
249
268
  # Determine span name
250
- span_name = name or f"lambda.{func.__name__}"
269
+ span_name = name or f"message.{func.__name__}"
251
270
  start_func = datetime.now()
252
271
 
253
272
  # Build span attributes
254
273
  span_attributes = attributes or {}
274
+ span_attributes['messaging.operation'] = 'process'
255
275
 
256
276
  result = None
257
277
  try:
258
- # Increment invocations counter
259
- print('XXX 2')
278
+ # Increment invocations counter with standardized metric
260
279
  self.meter.GlobalMetrics.invocations.add(1, {'handler': span_name})
261
280
 
262
281
  # Create span and execute function
263
282
  span_function = self.span
264
- if record is not None and 'MessageAttributes' in record:
283
+ if record is not None and ('MessageAttributes' in record or 'messageAttributes' in record):
265
284
  span_function = self.aws_message_span
266
285
 
267
286
  with span_function(span_name, message=record, attributes=span_attributes, kind=kind) as span_context:
287
+ # Add processing start event with standardized name
288
+ span_context.add_event("message.processing.start", {})
289
+
268
290
  # Execute the actual handler function
269
291
  result = func(record, *args, **kwargs)
270
292
 
271
- # Add result attributes if applicable
293
+ # Process result
294
+ success = True
295
+ complete_event_attrs = {}
296
+
272
297
  if result and isinstance(result, dict):
273
298
  if 'statusCode' in result:
274
- span_context.set_attribute("handler.status_code", result['statusCode'])
299
+ span_context.set_attribute("http.status_code", result['statusCode'])
275
300
 
276
301
  # Set span status based on status code
277
302
  if result['statusCode'] >= 400:
303
+ success = False
278
304
  span_context.set_status(
279
305
  Status(StatusCode.ERROR, f"Handler returned {result['statusCode']}")
280
306
  )
@@ -283,32 +309,49 @@ class RebrandlyOTEL:
283
309
 
284
310
  # Add custom result attributes if present
285
311
  if 'processed' in result:
286
- span_context.set_attribute("handler.processed", result['processed'])
312
+ complete_event_attrs['processed'] = result['processed']
313
+ span_context.set_attribute("message.processed", result['processed'])
287
314
  if 'skipped' in result:
288
- span_context.set_attribute("handler.skipped", result['skipped'])
315
+ complete_event_attrs['skipped'] = result['skipped']
316
+ span_context.set_attribute("message.skipped", result['skipped'])
317
+ else:
318
+ span_context.set_status(Status(StatusCode.OK))
289
319
 
290
- # Add completion event
291
- span_context.add_event("lambda.invocation.complete", attributes={
292
- "handler.success": True
293
- })
320
+ # Add completion event with standardized name
321
+ complete_event_attrs['success'] = success
322
+ span_context.add_event("message.processing.complete", complete_event_attrs)
294
323
 
295
- # Increment success counter
324
+ # Increment success counter with standardized metric
296
325
  self.meter.GlobalMetrics.successful_invocations.add(1, {'handler': span_name})
297
326
 
298
327
  return result
299
328
 
300
329
  except Exception as e:
301
- # Increment error counter
302
- self.meter.GlobalMetrics.error_invocations.add(1, {'handler': span_name, 'error': type(e).__name__})
330
+ # Increment error counter with standardized metric
331
+ self.meter.GlobalMetrics.error_invocations.add(1, {
332
+ 'handler': span_name,
333
+ 'error': type(e).__name__
334
+ })
303
335
 
304
336
  # Record the exception in the span
305
- span_context.record_exception(e)
306
- span_context.set_status(Status(StatusCode.ERROR, str(e)))
337
+ if 'span_context' in locals():
338
+ span_context.record_exception(e)
339
+ span_context.set_status(Status(StatusCode.ERROR, str(e)))
340
+
341
+ # Add failed processing event
342
+ span_context.add_event("message.processing.complete", {
343
+ 'success': False,
344
+ 'error': type(e).__name__
345
+ })
307
346
 
308
347
  # Re-raise the exception
309
348
  raise
310
349
 
311
350
  finally:
351
+ # Record duration with standardized metric
352
+ duration = (datetime.now() - start_func).total_seconds() * 1000
353
+ self.meter.GlobalMetrics.duration.record(duration, {'handler': span_name})
354
+
312
355
  if auto_flush:
313
356
  self.force_flush(start_datetime=start_func)
314
357
 
@@ -321,6 +364,7 @@ class RebrandlyOTEL:
321
364
  This is CRITICAL for Lambda functions to ensure data is sent before function freezes.
322
365
 
323
366
  Args:
367
+ start_datetime: Optional start time for system metrics capture
324
368
  timeout_millis: Maximum time to wait for flush in milliseconds
325
369
 
326
370
  Returns:
@@ -331,14 +375,15 @@ class RebrandlyOTEL:
331
375
  if start_datetime is not None:
332
376
  end_func = datetime.now()
333
377
  duration = (end_func - start_datetime).total_seconds() * 1000
334
- cpu_percent = psutil.cpu_percent(interval=1)
378
+ cpu_percent = psutil.cpu_percent(interval=0.1) # Shorter interval for Lambda
335
379
  memory = psutil.virtual_memory()
336
380
 
337
381
  # Record metrics using standardized names
338
382
  self.meter.GlobalMetrics.duration.record(duration, {'source': 'force_flush'})
339
383
  self.meter.GlobalMetrics.memory_usage_bytes.set(memory.used)
340
384
  self.meter.GlobalMetrics.cpu_usage_percentage.set(cpu_percent)
341
- self.logger.logger.info(f"Function duration: {duration}ms, Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
385
+
386
+ print(f"Function duration: {duration}ms, Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
342
387
 
343
388
  try:
344
389
  # Flush traces
@@ -376,9 +421,8 @@ class RebrandlyOTEL:
376
421
  self._meter.shutdown()
377
422
  if self._logger:
378
423
  self._logger.shutdown()
379
- print("[OTEL] Shutdown completed")
380
424
  except Exception as e:
381
- print(f"[OTEL] Error during shutdown: {e}")
425
+ print(f"[Rebrandly OTEL] Error during shutdown: {e}")
382
426
 
383
427
  def _detect_lambda_trigger(self, event: Any) -> str:
384
428
  """Detect Lambda trigger type from event."""
@@ -452,6 +496,7 @@ class RebrandlyOTEL:
452
496
  from opentelemetry import trace, context as otel_context
453
497
 
454
498
  combined_attributes = attributes or {}
499
+ combined_attributes['messaging.operation'] = 'process'
455
500
 
456
501
  # Extract message attributes for linking/attributes
457
502
  if message and isinstance(message, dict):
@@ -460,16 +505,11 @@ class RebrandlyOTEL:
460
505
  sns_msg = message['Sns']
461
506
  if 'MessageId' in sns_msg:
462
507
  combined_attributes['messaging.message_id'] = sns_msg['MessageId']
508
+ if 'Subject' in sns_msg:
509
+ combined_attributes['messaging.sns.subject'] = sns_msg['Subject']
463
510
  if 'TopicArn' in sns_msg:
464
511
  combined_attributes['messaging.destination'] = sns_msg['TopicArn']
465
- combined_attributes['messaging.system'] = 'aws_sns'
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
512
+ combined_attributes['messaging.system'] = 'sns'
473
513
 
474
514
  elif 'messageId' in message:
475
515
  # SQS message
@@ -477,18 +517,11 @@ class RebrandlyOTEL:
477
517
  if 'eventSource' in message:
478
518
  combined_attributes['messaging.system'] = message['eventSource']
479
519
 
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
520
 
489
521
  if 'awsRegion' in message:
490
522
  combined_attributes['cloud.region'] = message['awsRegion']
491
523
 
524
+
492
525
  # Use the tracer's start_span method directly to ensure it works
493
526
  # This creates a child span of whatever is currently active
494
527
  with self.tracer.start_span(
File without changes