rebrandly-otel 0.1.14__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.14
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.14
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.14",
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",
@@ -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:
@@ -160,11 +160,10 @@ class RebrandlyOTEL:
160
160
  from opentelemetry import propagate, context as otel_context
161
161
  extracted_context = propagate.extract(carrier)
162
162
  token = otel_context.attach(extracted_context)
163
- span_attributes['message.has_trace_context'] = True
164
163
 
165
164
  result = None
166
165
  try:
167
- # Increment invocation counter
166
+ # Increment invocation counter with standardized metric name
168
167
  self.meter.GlobalMetrics.invocations.add(1, {'function': span_name})
169
168
 
170
169
  # Create and execute within span
@@ -173,26 +172,63 @@ class RebrandlyOTEL:
173
172
  attributes=span_attributes,
174
173
  kind=kind
175
174
  ) as span:
176
- # Add invocation start event
177
- span.add_event("lambda.invocation.start", {
175
+ # Add invocation start event with standardized attributes
176
+ start_event_attrs = {
178
177
  'event.type': type(event).__name__ if event else 'None'
179
- })
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)
180
185
 
181
186
  # Execute handler
182
187
  result = func(event, lambda_context)
183
188
 
184
- # Process result
185
- 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})
186
212
 
187
213
  return result
188
214
 
189
215
  except Exception as e:
190
- # Increment error counter
216
+ # Increment error counter with standardized metric
191
217
  self.meter.GlobalMetrics.error_invocations.add(1, {
192
218
  'function': span_name,
193
219
  'error': type(e).__name__
194
220
  })
195
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
+
196
232
  # Log error
197
233
  self.logger.logger.error(f"Lambda execution failed: {e}", exc_info=True)
198
234
  raise
@@ -203,81 +239,68 @@ class RebrandlyOTEL:
203
239
  from opentelemetry import context as otel_context
204
240
  otel_context.detach(token)
205
241
 
206
- # Record duration
242
+ # Record duration with standardized metric
207
243
  duration = (datetime.now() - start_time).total_seconds() * 1000
208
244
  self.meter.GlobalMetrics.duration.record(duration, {'function': span_name})
209
245
 
210
246
  # Force flush if enabled
211
247
  if auto_flush:
212
- 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...")
213
249
  flush_success = self.force_flush(timeout_millis=1000)
214
250
  if not flush_success:
215
- 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")
216
252
 
217
253
  return wrapper
218
254
  return decorator
219
255
 
220
- def _process_lambda_result(self, result, span_context, span_name):
221
- """Helper method to process Lambda result and update span accordingly"""
222
- if isinstance(result, dict):
223
- if 'statusCode' in result:
224
- span_context.set_attribute("http.status_code", result['statusCode'])
225
- # Set span status based on HTTP status code
226
- if result['statusCode'] >= 400:
227
- span_context.set_status(Status(StatusCode.ERROR, f"HTTP {result['statusCode']}"))
228
- else:
229
- span_context.set_status(Status(StatusCode.OK))
230
-
231
- # Add completion event
232
- span_context.add_event("lambda.invocation.complete",
233
- attributes={"success": result.get('statusCode', 200) < 400})
234
- else:
235
- span_context.set_status(Status(StatusCode.OK))
236
- span_context.add_event("lambda.invocation.complete", attributes={"success": True})
237
-
238
- # Increment success counter
239
- self.meter.GlobalMetrics.successful_invocations.add(1, {'function': span_name})
240
-
241
256
  def aws_message_handler(self,
242
- name: Optional[str] = None,
243
- attributes: Optional[Dict[str, Any]] = None,
244
- kind: SpanKind = SpanKind.CONSUMER,
245
- 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):
246
261
  """
247
- 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.
248
264
  """
249
265
  def decorator(func):
250
266
  @functools.wraps(func)
251
267
  def wrapper(record=None, *args, **kwargs):
252
268
  # Determine span name
253
- span_name = name or f"lambda.{func.__name__}"
269
+ span_name = name or f"message.{func.__name__}"
254
270
  start_func = datetime.now()
255
271
 
256
272
  # Build span attributes
257
273
  span_attributes = attributes or {}
274
+ span_attributes['messaging.operation'] = 'process'
258
275
 
259
276
  result = None
260
277
  try:
261
- # Increment invocations counter
262
- print('XXX 2')
278
+ # Increment invocations counter with standardized metric
263
279
  self.meter.GlobalMetrics.invocations.add(1, {'handler': span_name})
264
280
 
265
281
  # Create span and execute function
266
282
  span_function = self.span
267
- if record is not None and 'MessageAttributes' in record:
283
+ if record is not None and ('MessageAttributes' in record or 'messageAttributes' in record):
268
284
  span_function = self.aws_message_span
269
285
 
270
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
+
271
290
  # Execute the actual handler function
272
291
  result = func(record, *args, **kwargs)
273
292
 
274
- # Add result attributes if applicable
293
+ # Process result
294
+ success = True
295
+ complete_event_attrs = {}
296
+
275
297
  if result and isinstance(result, dict):
276
298
  if 'statusCode' in result:
277
- span_context.set_attribute("handler.status_code", result['statusCode'])
299
+ span_context.set_attribute("http.status_code", result['statusCode'])
278
300
 
279
301
  # Set span status based on status code
280
302
  if result['statusCode'] >= 400:
303
+ success = False
281
304
  span_context.set_status(
282
305
  Status(StatusCode.ERROR, f"Handler returned {result['statusCode']}")
283
306
  )
@@ -286,32 +309,49 @@ class RebrandlyOTEL:
286
309
 
287
310
  # Add custom result attributes if present
288
311
  if 'processed' in result:
289
- span_context.set_attribute("handler.processed", result['processed'])
312
+ complete_event_attrs['processed'] = result['processed']
313
+ span_context.set_attribute("message.processed", result['processed'])
290
314
  if 'skipped' in result:
291
- 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))
292
319
 
293
- # Add completion event
294
- span_context.add_event("lambda.invocation.complete", attributes={
295
- "handler.success": True
296
- })
320
+ # Add completion event with standardized name
321
+ complete_event_attrs['success'] = success
322
+ span_context.add_event("message.processing.complete", complete_event_attrs)
297
323
 
298
- # Increment success counter
324
+ # Increment success counter with standardized metric
299
325
  self.meter.GlobalMetrics.successful_invocations.add(1, {'handler': span_name})
300
326
 
301
327
  return result
302
328
 
303
329
  except Exception as e:
304
- # Increment error counter
305
- 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
+ })
306
335
 
307
336
  # Record the exception in the span
308
- span_context.record_exception(e)
309
- 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
+ })
310
346
 
311
347
  # Re-raise the exception
312
348
  raise
313
349
 
314
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
+
315
355
  if auto_flush:
316
356
  self.force_flush(start_datetime=start_func)
317
357
 
@@ -324,6 +364,7 @@ class RebrandlyOTEL:
324
364
  This is CRITICAL for Lambda functions to ensure data is sent before function freezes.
325
365
 
326
366
  Args:
367
+ start_datetime: Optional start time for system metrics capture
327
368
  timeout_millis: Maximum time to wait for flush in milliseconds
328
369
 
329
370
  Returns:
@@ -334,14 +375,15 @@ class RebrandlyOTEL:
334
375
  if start_datetime is not None:
335
376
  end_func = datetime.now()
336
377
  duration = (end_func - start_datetime).total_seconds() * 1000
337
- cpu_percent = psutil.cpu_percent(interval=1)
378
+ cpu_percent = psutil.cpu_percent(interval=0.1) # Shorter interval for Lambda
338
379
  memory = psutil.virtual_memory()
339
380
 
340
381
  # Record metrics using standardized names
341
382
  self.meter.GlobalMetrics.duration.record(duration, {'source': 'force_flush'})
342
383
  self.meter.GlobalMetrics.memory_usage_bytes.set(memory.used)
343
384
  self.meter.GlobalMetrics.cpu_usage_percentage.set(cpu_percent)
344
- 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}%")
345
387
 
346
388
  try:
347
389
  # Flush traces
@@ -379,9 +421,8 @@ class RebrandlyOTEL:
379
421
  self._meter.shutdown()
380
422
  if self._logger:
381
423
  self._logger.shutdown()
382
- print("[OTEL] Shutdown completed")
383
424
  except Exception as e:
384
- print(f"[OTEL] Error during shutdown: {e}")
425
+ print(f"[Rebrandly OTEL] Error during shutdown: {e}")
385
426
 
386
427
  def _detect_lambda_trigger(self, event: Any) -> str:
387
428
  """Detect Lambda trigger type from event."""
@@ -455,6 +496,7 @@ class RebrandlyOTEL:
455
496
  from opentelemetry import trace, context as otel_context
456
497
 
457
498
  combined_attributes = attributes or {}
499
+ combined_attributes['messaging.operation'] = 'process'
458
500
 
459
501
  # Extract message attributes for linking/attributes
460
502
  if message and isinstance(message, dict):
@@ -463,16 +505,11 @@ class RebrandlyOTEL:
463
505
  sns_msg = message['Sns']
464
506
  if 'MessageId' in sns_msg:
465
507
  combined_attributes['messaging.message_id'] = sns_msg['MessageId']
508
+ if 'Subject' in sns_msg:
509
+ combined_attributes['messaging.sns.subject'] = sns_msg['Subject']
466
510
  if 'TopicArn' in sns_msg:
467
511
  combined_attributes['messaging.destination'] = sns_msg['TopicArn']
468
- combined_attributes['messaging.system'] = 'aws_sns'
469
-
470
- # Check for trace context in SNS
471
- if 'MessageAttributes' in sns_msg:
472
- for key, value in sns_msg['MessageAttributes'].items():
473
- if key == 'traceparent' and 'Value' in value:
474
- combined_attributes['message.traceparent'] = value['Value']
475
- combined_attributes['message.has_trace_context'] = True
512
+ combined_attributes['messaging.system'] = 'sns'
476
513
 
477
514
  elif 'messageId' in message:
478
515
  # SQS message
@@ -480,18 +517,11 @@ class RebrandlyOTEL:
480
517
  if 'eventSource' in message:
481
518
  combined_attributes['messaging.system'] = message['eventSource']
482
519
 
483
- # Check for trace context in SQS
484
- if 'MessageAttributes' in message or 'messageAttributes' in message:
485
- attrs = message.get('MessageAttributes') or message.get('messageAttributes', {})
486
- for key, value in attrs.items():
487
- if key == 'traceparent':
488
- tp_value = value.get('StringValue') or value.get('stringValue', '')
489
- combined_attributes['message.traceparent'] = tp_value
490
- combined_attributes['message.has_trace_context'] = True
491
520
 
492
521
  if 'awsRegion' in message:
493
522
  combined_attributes['cloud.region'] = message['awsRegion']
494
523
 
524
+
495
525
  # Use the tracer's start_span method directly to ensure it works
496
526
  # This creates a child span of whatever is currently active
497
527
  with self.tracer.start_span(
File without changes