rebrandly-otel 0.1.18__tar.gz → 0.1.20__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.18
3
+ Version: 0.1.20
4
4
  Summary: Python OTEL wrapper by Rebrandly
5
5
  Home-page: https://github.com/rebrandly/rebrandly-otel-python
6
6
  Author: Antonio Romano
@@ -360,6 +360,93 @@ if __name__ == '__main__':
360
360
  app.run(debug=True)
361
361
  ```
362
362
 
363
+ ###
364
+ FastAPI
365
+
366
+ ```python
367
+
368
+ # main_fastapi.py
369
+ from fastapi import FastAPI, HTTPException, Depends
370
+ from contextlib import asynccontextmanager
371
+ from src.rebrandly_otel import otel, logger, force_flush
372
+ from src.fastapi_support import setup_fastapi, get_current_span
373
+ from datetime import datetime
374
+ from typing import Optional
375
+ import uvicorn
376
+
377
+ @asynccontextmanager
378
+ async def lifespan(app: FastAPI):
379
+ # Startup
380
+ logger.info("FastAPI application starting up")
381
+ yield
382
+ # Shutdown
383
+ logger.info("FastAPI application shutting down")
384
+ force_flush()
385
+
386
+ app = FastAPI(title="FastAPI OTEL Example", lifespan=lifespan)
387
+
388
+ # Setup FastAPI with OTEL
389
+ setup_fastapi(otel, app)
390
+
391
+ @app.get("/health")
392
+ async def health():
393
+ """Health check endpoint."""
394
+ logger.info("Health check requested")
395
+ return {"status": "healthy"}
396
+
397
+ @app.post("/process")
398
+ @app.get("/process")
399
+ async def process(span = Depends(get_current_span)):
400
+ """Process endpoint with custom span."""
401
+ with otel.span("process_request"):
402
+ logger.info("Processing request")
403
+
404
+ # You can also use the injected span directly
405
+ if span:
406
+ span.add_event("custom_processing_event", {
407
+ "timestamp": datetime.now().isoformat()
408
+ })
409
+
410
+ # Simulate some processing
411
+ result = {
412
+ "processed": True,
413
+ "timestamp": datetime.now().isoformat()
414
+ }
415
+
416
+ logger.info(f"Returning result: {result}")
417
+ return result
418
+
419
+ @app.get("/error")
420
+ async def error():
421
+ """Endpoint that raises an error."""
422
+ logger.error("Error endpoint called")
423
+ raise HTTPException(status_code=400, detail="Simulated error")
424
+
425
+ @app.get("/exception")
426
+ async def exception():
427
+ """Endpoint that raises an unhandled exception."""
428
+ logger.error("Exception endpoint called")
429
+ raise ValueError("Simulated unhandled exception")
430
+
431
+ @app.get("/items/{item_id}")
432
+ async def get_item(item_id: int, q: Optional[str] = None):
433
+ """Example endpoint with path and query parameters."""
434
+ with otel.span("fetch_item", attributes={"item_id": item_id, "query": q}):
435
+ logger.info(f"Fetching item {item_id} with query: {q}")
436
+
437
+ if item_id == 999:
438
+ raise HTTPException(status_code=404, detail="Item not found")
439
+
440
+ return {
441
+ "item_id": item_id,
442
+ "name": f"Item {item_id}",
443
+ "query": q
444
+ }
445
+
446
+ if __name__ == "__main__":
447
+ uvicorn.run(app, host="0.0.0.0", port=8000)
448
+ ```
449
+
363
450
  ### More examples
364
451
  You can find More examples [here](examples)
365
452
 
@@ -339,6 +339,93 @@ if __name__ == '__main__':
339
339
  app.run(debug=True)
340
340
  ```
341
341
 
342
+ ###
343
+ FastAPI
344
+
345
+ ```python
346
+
347
+ # main_fastapi.py
348
+ from fastapi import FastAPI, HTTPException, Depends
349
+ from contextlib import asynccontextmanager
350
+ from src.rebrandly_otel import otel, logger, force_flush
351
+ from src.fastapi_support import setup_fastapi, get_current_span
352
+ from datetime import datetime
353
+ from typing import Optional
354
+ import uvicorn
355
+
356
+ @asynccontextmanager
357
+ async def lifespan(app: FastAPI):
358
+ # Startup
359
+ logger.info("FastAPI application starting up")
360
+ yield
361
+ # Shutdown
362
+ logger.info("FastAPI application shutting down")
363
+ force_flush()
364
+
365
+ app = FastAPI(title="FastAPI OTEL Example", lifespan=lifespan)
366
+
367
+ # Setup FastAPI with OTEL
368
+ setup_fastapi(otel, app)
369
+
370
+ @app.get("/health")
371
+ async def health():
372
+ """Health check endpoint."""
373
+ logger.info("Health check requested")
374
+ return {"status": "healthy"}
375
+
376
+ @app.post("/process")
377
+ @app.get("/process")
378
+ async def process(span = Depends(get_current_span)):
379
+ """Process endpoint with custom span."""
380
+ with otel.span("process_request"):
381
+ logger.info("Processing request")
382
+
383
+ # You can also use the injected span directly
384
+ if span:
385
+ span.add_event("custom_processing_event", {
386
+ "timestamp": datetime.now().isoformat()
387
+ })
388
+
389
+ # Simulate some processing
390
+ result = {
391
+ "processed": True,
392
+ "timestamp": datetime.now().isoformat()
393
+ }
394
+
395
+ logger.info(f"Returning result: {result}")
396
+ return result
397
+
398
+ @app.get("/error")
399
+ async def error():
400
+ """Endpoint that raises an error."""
401
+ logger.error("Error endpoint called")
402
+ raise HTTPException(status_code=400, detail="Simulated error")
403
+
404
+ @app.get("/exception")
405
+ async def exception():
406
+ """Endpoint that raises an unhandled exception."""
407
+ logger.error("Exception endpoint called")
408
+ raise ValueError("Simulated unhandled exception")
409
+
410
+ @app.get("/items/{item_id}")
411
+ async def get_item(item_id: int, q: Optional[str] = None):
412
+ """Example endpoint with path and query parameters."""
413
+ with otel.span("fetch_item", attributes={"item_id": item_id, "query": q}):
414
+ logger.info(f"Fetching item {item_id} with query: {q}")
415
+
416
+ if item_id == 999:
417
+ raise HTTPException(status_code=404, detail="Item not found")
418
+
419
+ return {
420
+ "item_id": item_id,
421
+ "name": f"Item {item_id}",
422
+ "query": q
423
+ }
424
+
425
+ if __name__ == "__main__":
426
+ uvicorn.run(app, host="0.0.0.0", port=8000)
427
+ ```
428
+
342
429
  ### More examples
343
430
  You can find More examples [here](examples)
344
431
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rebrandly_otel
3
- Version: 0.1.18
3
+ Version: 0.1.20
4
4
  Summary: Python OTEL wrapper by Rebrandly
5
5
  Home-page: https://github.com/rebrandly/rebrandly-otel-python
6
6
  Author: Antonio Romano
@@ -360,6 +360,93 @@ if __name__ == '__main__':
360
360
  app.run(debug=True)
361
361
  ```
362
362
 
363
+ ###
364
+ FastAPI
365
+
366
+ ```python
367
+
368
+ # main_fastapi.py
369
+ from fastapi import FastAPI, HTTPException, Depends
370
+ from contextlib import asynccontextmanager
371
+ from src.rebrandly_otel import otel, logger, force_flush
372
+ from src.fastapi_support import setup_fastapi, get_current_span
373
+ from datetime import datetime
374
+ from typing import Optional
375
+ import uvicorn
376
+
377
+ @asynccontextmanager
378
+ async def lifespan(app: FastAPI):
379
+ # Startup
380
+ logger.info("FastAPI application starting up")
381
+ yield
382
+ # Shutdown
383
+ logger.info("FastAPI application shutting down")
384
+ force_flush()
385
+
386
+ app = FastAPI(title="FastAPI OTEL Example", lifespan=lifespan)
387
+
388
+ # Setup FastAPI with OTEL
389
+ setup_fastapi(otel, app)
390
+
391
+ @app.get("/health")
392
+ async def health():
393
+ """Health check endpoint."""
394
+ logger.info("Health check requested")
395
+ return {"status": "healthy"}
396
+
397
+ @app.post("/process")
398
+ @app.get("/process")
399
+ async def process(span = Depends(get_current_span)):
400
+ """Process endpoint with custom span."""
401
+ with otel.span("process_request"):
402
+ logger.info("Processing request")
403
+
404
+ # You can also use the injected span directly
405
+ if span:
406
+ span.add_event("custom_processing_event", {
407
+ "timestamp": datetime.now().isoformat()
408
+ })
409
+
410
+ # Simulate some processing
411
+ result = {
412
+ "processed": True,
413
+ "timestamp": datetime.now().isoformat()
414
+ }
415
+
416
+ logger.info(f"Returning result: {result}")
417
+ return result
418
+
419
+ @app.get("/error")
420
+ async def error():
421
+ """Endpoint that raises an error."""
422
+ logger.error("Error endpoint called")
423
+ raise HTTPException(status_code=400, detail="Simulated error")
424
+
425
+ @app.get("/exception")
426
+ async def exception():
427
+ """Endpoint that raises an unhandled exception."""
428
+ logger.error("Exception endpoint called")
429
+ raise ValueError("Simulated unhandled exception")
430
+
431
+ @app.get("/items/{item_id}")
432
+ async def get_item(item_id: int, q: Optional[str] = None):
433
+ """Example endpoint with path and query parameters."""
434
+ with otel.span("fetch_item", attributes={"item_id": item_id, "query": q}):
435
+ logger.info(f"Fetching item {item_id} with query: {q}")
436
+
437
+ if item_id == 999:
438
+ raise HTTPException(status_code=404, detail="Item not found")
439
+
440
+ return {
441
+ "item_id": item_id,
442
+ "name": f"Item {item_id}",
443
+ "query": q
444
+ }
445
+
446
+ if __name__ == "__main__":
447
+ uvicorn.run(app, host="0.0.0.0", port=8000)
448
+ ```
449
+
363
450
  ### More examples
364
451
  You can find More examples [here](examples)
365
452
 
@@ -12,4 +12,5 @@ src/logs.py
12
12
  src/metrics.py
13
13
  src/otel_utils.py
14
14
  src/rebrandly_otel.py
15
- src/traces.py
15
+ src/traces.py
16
+ tests/test_usage.py
@@ -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.18",
8
+ version="0.1.20",
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
  # logs.py
2
2
  """Logging implementation for Rebrandly OTEL SDK."""
3
3
  import logging
4
+ import sys
4
5
  from typing import Optional
5
6
  from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
6
7
  from opentelemetry.sdk._logs.export import (
@@ -34,12 +35,15 @@ class RebrandlyLogger:
34
35
  self._provider.add_log_record_processor(SimpleLogRecordProcessor(console_exporter))
35
36
 
36
37
  # Add OTLP exporter if configured
37
- if get_otlp_endpoint():
38
- otlp_exporter = OTLPLogExporter(endpoint=get_otlp_endpoint())
38
+ otel_endpoint = get_otlp_endpoint()
39
+ if otel_endpoint:
40
+ otlp_exporter = OTLPLogExporter(
41
+ timeout=5,
42
+ endpoint=otel_endpoint
43
+ )
39
44
  batch_processor = BatchLogRecordProcessor(otlp_exporter, export_timeout_millis=get_millis_batch_time())
40
45
  self._provider.add_log_record_processor(batch_processor)
41
46
 
42
- # Set as global provider
43
47
  set_logger_provider(self._provider)
44
48
 
45
49
  # Configure standard logging
@@ -78,8 +78,12 @@ class RebrandlyMeter:
78
78
  readers.append(console_reader)
79
79
 
80
80
  # Add OTLP exporter if configured
81
- if get_otlp_endpoint() is not None:
82
- otlp_exporter = OTLPMetricExporter(endpoint=get_otlp_endpoint())
81
+ otel_endpoint = get_otlp_endpoint()
82
+ if otel_endpoint is not None:
83
+ otlp_exporter = OTLPMetricExporter(
84
+ endpoint=otel_endpoint,
85
+ timeout=5
86
+ )
83
87
  otlp_reader = PeriodicExportingMetricReader(otlp_exporter, export_interval_millis=get_millis_batch_time())
84
88
  readers.append(otlp_reader)
85
89
 
@@ -3,6 +3,8 @@
3
3
 
4
4
  import os
5
5
  import sys
6
+ import grpc
7
+ import json
6
8
 
7
9
  from opentelemetry.sdk.resources import Resource
8
10
  from opentelemetry.semconv.attributes import service_attributes
@@ -33,8 +35,9 @@ def get_package_version():
33
35
  try:
34
36
  from importlib_metadata import version, PackageNotFoundError
35
37
  return version('rebrandly_otel')
36
- except:
37
- return '?.0.1'
38
+ except Exception as e:
39
+ print(f"[OTEL Utils] Warning: Could not get package version: {e}")
40
+ return '0.1.0'
38
41
 
39
42
 
40
43
  def get_service_name(service_name: str = None) -> str:
@@ -49,10 +52,31 @@ def get_service_version(service_version: str = None) -> str:
49
52
  return service_version
50
53
 
51
54
 
52
- def get_otlp_endpoint(otlp_endpoint: str = None) -> str:
53
- if otlp_endpoint is None:
54
- return os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT', None)
55
- return otlp_endpoint
55
+ def get_otlp_endpoint(otlp_endpoint: str = None) -> str | None:
56
+ endpoint = otlp_endpoint or os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT', None)
57
+
58
+ if endpoint is not None:
59
+ try:
60
+ from urllib.parse import urlparse
61
+
62
+ # Parse the endpoint
63
+ parsed = urlparse(endpoint if '://' in endpoint else f'http://{endpoint}')
64
+ host = parsed.hostname
65
+ port = parsed.port
66
+
67
+ # Test gRPC connection
68
+ channel = grpc.insecure_channel(f'{host}:{port}')
69
+ try:
70
+ # Wait for the channel to be ready
71
+ grpc.channel_ready_future(channel).result(timeout=3)
72
+ return endpoint
73
+ finally:
74
+ channel.close()
75
+
76
+ except Exception as e:
77
+ print(f"[OTEL] Failed to connect to OTLP endpoint {endpoint}: {e}")
78
+ return None
79
+ return endpoint
56
80
 
57
81
  def is_otel_debug() -> bool:
58
82
  return os.environ.get('OTEL_DEBUG', 'false').lower() == 'true'
@@ -61,5 +85,25 @@ def is_otel_debug() -> bool:
61
85
  def get_millis_batch_time():
62
86
  try:
63
87
  return int(os.environ.get('BATCH_EXPORT_TIME_MILLIS', 100))
64
- except:
65
- return 5000
88
+ except Exception as e:
89
+ print(f"[OTEL Utils] Warning: Invalid BATCH_EXPORT_TIME_MILLIS value, using default 5000ms: {e}")
90
+ return 5000
91
+
92
+ def extract_event_from(message) -> str | None:
93
+ body = None
94
+ if 'body' in message:
95
+ body = message['body']
96
+ if 'Body' in message:
97
+ body = message['Body']
98
+ if 'Message' in message:
99
+ body = message['Message']
100
+ if 'Sns' in message and 'Message' in message['Sns']:
101
+ body = message['Sns']['Message']
102
+ if body is not None:
103
+ try:
104
+ jbody = json.loads(body)
105
+ if 'event' in jbody:
106
+ return jbody['event']
107
+ except:
108
+ pass
109
+ return None
@@ -12,6 +12,7 @@ from opentelemetry import baggage, propagate, context
12
12
  from .traces import RebrandlyTracer
13
13
  from .metrics import RebrandlyMeter
14
14
  from .logs import RebrandlyLogger
15
+ from .otel_utils import extract_event_from
15
16
 
16
17
 
17
18
  T = TypeVar('T')
@@ -260,8 +261,11 @@ class RebrandlyOTEL:
260
261
  try:
261
262
  # Create span and execute function
262
263
  span_function = self.span
263
- if record is not None and ('MessageAttributes' in record or 'messageAttributes' in record):
264
+ if record is not None and (('MessageAttributes' in record or 'messageAttributes' in record) or ('Sns' in record and 'MessageAttributes' in record['Sns'])):
264
265
  span_function = self.aws_message_span
266
+ evt = extract_event_from(record)
267
+ if evt:
268
+ span_attributes['event.type'] = evt
265
269
 
266
270
  with span_function(span_name, message=record, attributes=span_attributes, kind=kind) as span_context:
267
271
  # Add processing start event with standardized name
@@ -370,6 +374,7 @@ class RebrandlyOTEL:
370
374
  time.sleep(0.1)
371
375
 
372
376
  except Exception as e:
377
+ print(f"[Rebrandly OTEL] Error during force flush: {e}")
373
378
  success = False
374
379
 
375
380
  return success
@@ -486,6 +491,10 @@ class RebrandlyOTEL:
486
491
  if 'awsRegion' in message:
487
492
  combined_attributes['cloud.region'] = message['awsRegion']
488
493
 
494
+ evt = extract_event_from(message)
495
+ if evt:
496
+ combined_attributes['event.type'] = evt
497
+
489
498
 
490
499
  # Use the tracer's start_span method directly to ensure it works
491
500
  # This creates a child span of whatever is currently active
@@ -32,8 +32,12 @@ class RebrandlyTracer:
32
32
  self._provider.add_span_processor(SimpleSpanProcessor(console_exporter))
33
33
 
34
34
  # Add OTLP exporter if configured
35
- if get_otlp_endpoint() is not None:
36
- otlp_exporter = OTLPSpanExporter(endpoint=get_otlp_endpoint())
35
+ otel_endpoint = get_otlp_endpoint()
36
+ if otel_endpoint is not None:
37
+ otlp_exporter = OTLPSpanExporter(
38
+ endpoint=otel_endpoint,
39
+ timeout=5
40
+ )
37
41
 
38
42
  # Use batch processor for production
39
43
  batch_processor = BatchSpanProcessor(otlp_exporter, export_timeout_millis=get_millis_batch_time())
@@ -139,14 +143,14 @@ class RebrandlyTracer:
139
143
  if target_span and hasattr(target_span, 'record_exception'):
140
144
  if exception is not None:
141
145
  target_span.record_exception(exception)
142
- target_span.set_status(trace.Status(trace.StatusCode.ERROR, str(exception)), description=msg)
146
+ target_span.set_status(trace.Status(trace.StatusCode.ERROR, str(exception)))
143
147
 
144
148
 
145
149
  def record_span_success(self, span: Optional[Span] = None, msg: Optional[str] = None):
146
- """Record an exception on a span."""
150
+ """Record success on a span."""
147
151
  target_span = span or self.get_current_span()
148
- if target_span and hasattr(target_span, 'record_exception'):
149
- target_span.set_status(trace.Status(trace.StatusCode.OK), description=msg)
152
+ if target_span and hasattr(target_span, 'set_status'):
153
+ target_span.set_status(trace.Status(trace.StatusCode.OK))
150
154
 
151
155
 
152
156
  def add_event(self, name: str, attributes: Optional[Dict[str, Any]] = None, span: Optional[Span] = None):
@@ -0,0 +1,336 @@
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock, ANY, call
3
+ import importlib
4
+ import os
5
+
6
+ # Import the module under test
7
+ import src.rebrandly_otel
8
+ from src.rebrandly_otel import RebrandlyOTEL, otel, lambda_handler, aws_message_handler, span, force_flush, shutdown
9
+ from opentelemetry.trace import SpanKind, Status, StatusCode
10
+
11
+
12
+ class TestRebrandlyOTELUsage(unittest.TestCase):
13
+
14
+ def setUp(self):
15
+ # Reset the singleton and reload the module before each test
16
+ RebrandlyOTEL._instance = None
17
+ RebrandlyOTEL._initialized = False
18
+ importlib.reload(src.rebrandly_otel)
19
+
20
+ # Patch external dependencies at the module level
21
+ self.mock_propagate = patch('opentelemetry.propagate').start()
22
+ self.mock_context = patch('opentelemetry.context').start()
23
+ self.mock_psutil = patch('src.rebrandly_otel.psutil').start()
24
+
25
+ # Patch the attributes of the otel instance directly
26
+ self.mock_tracer = MagicMock()
27
+ self.mock_meter = MagicMock()
28
+ self.mock_logger = MagicMock()
29
+
30
+ otel._tracer = self.mock_tracer
31
+ otel._meter = self.mock_meter
32
+ otel._logger = self.mock_logger
33
+
34
+ # Configure mock behaviors for tracer
35
+ mock_span_instance = MagicMock()
36
+ mock_span_instance.set_status = MagicMock()
37
+ mock_span_instance.record_exception = MagicMock()
38
+ mock_span_instance.set_attribute = MagicMock()
39
+
40
+ mock_start_span_context_manager = MagicMock()
41
+ mock_start_span_context_manager.__enter__.return_value = mock_span_instance
42
+ mock_start_span_context_manager.__exit__.return_value = None
43
+
44
+ self.mock_tracer.start_span.return_value = mock_start_span_context_manager
45
+
46
+ # Configure mock behaviors for meter
47
+ self.mock_meter.GlobalMetrics.memory_usage_bytes.set = MagicMock()
48
+ self.mock_meter.GlobalMetrics.cpu_usage_percentage.set = MagicMock()
49
+
50
+ # Configure mock behaviors for psutil
51
+ self.mock_psutil.cpu_percent.return_value = 10.0
52
+ mock_virtual_memory = MagicMock()
53
+ mock_virtual_memory.used = 1000000
54
+ mock_virtual_memory.percent = 50.0
55
+ self.mock_psutil.virtual_memory.return_value = mock_virtual_memory
56
+
57
+ self.addCleanup(patch.stopall)
58
+
59
+ def test_initialization(self):
60
+ # otel instance is already created and initialized in setUp
61
+ self.assertIsInstance(otel, RebrandlyOTEL)
62
+ # The tracer, meter, logger properties are accessed during otel initialization
63
+ # which happens when the module is reloaded in setUp.
64
+ # So, we check that the mocks were assigned.
65
+ self.assertIs(otel.tracer, self.mock_tracer)
66
+ self.assertIs(otel.meter, self.mock_meter)
67
+ self.assertIs(otel.logger, self.mock_logger)
68
+
69
+ def test_span_context_manager(self):
70
+ with span("test_span", attributes={"key": "value"}) as s:
71
+ self.assertIsNotNone(s)
72
+ s.set_attribute("another_key", "another_value")
73
+
74
+ self.mock_tracer.start_span.assert_called_once_with(
75
+ "test_span", attributes={"key": "value"}, kind=SpanKind.INTERNAL
76
+ )
77
+ span_mock = self.mock_tracer.start_span.return_value.__enter__.return_value
78
+ self.assertEqual(span_mock.set_status.call_args[0][0].status_code, StatusCode.OK)
79
+
80
+ def test_span_context_manager_with_exception(self):
81
+ with self.assertRaises(ValueError):
82
+ with span("error_span"):
83
+ raise ValueError("Something went wrong")
84
+
85
+ self.mock_tracer.start_span.assert_called_once_with(
86
+ "error_span", attributes=None, kind=SpanKind.INTERNAL
87
+ )
88
+ span_mock = self.mock_tracer.start_span.return_value.__enter__.return_value
89
+ span_mock.record_exception.assert_called_once()
90
+ self.assertEqual(span_mock.set_status.call_args[0][0].status_code, StatusCode.ERROR)
91
+ self.assertEqual(span_mock.set_status.call_args[0][0].description, "Something went wrong")
92
+
93
+ def test_lambda_handler_basic(self):
94
+ @lambda_handler("my_lambda")
95
+ def handler(event, context):
96
+ return {"statusCode": 200, "body": "OK"}
97
+
98
+ mock_context = MagicMock()
99
+ mock_context.aws_request_id = "req123"
100
+ mock_context.function_arn = "arn:aws:lambda:us-east-1:123456789012:function:my_lambda"
101
+ mock_context.function_name = "my_lambda"
102
+ mock_context.function_version = "$LATEST"
103
+
104
+ result = handler({}, mock_context)
105
+
106
+ self.assertEqual(result, {"statusCode": 200, "body": "OK"})
107
+ self.mock_tracer.start_span.assert_called_once_with(
108
+ name="my_lambda",
109
+ attributes={
110
+ "faas.trigger": "direct",
111
+ "faas.execution": "req123",
112
+ "faas.id": "arn:aws:lambda:us-east-1:123456789012:function:my_lambda",
113
+ "faas.name": "my_lambda",
114
+ "faas.version": "$LATEST"
115
+ },
116
+ kind=SpanKind.SERVER
117
+ )
118
+ span_mock = self.mock_tracer.start_span.return_value.__enter__.return_value
119
+ self.assertEqual(span_mock.set_status.call_args[0][0].status_code, StatusCode.OK)
120
+ self.mock_logger.logger.info.assert_called_with(ANY)
121
+ self.mock_psutil.cpu_percent.assert_called_once()
122
+ self.mock_psutil.virtual_memory.assert_called_once()
123
+ self.mock_meter.GlobalMetrics.memory_usage_bytes.set.assert_called_once()
124
+ self.mock_meter.GlobalMetrics.cpu_usage_percentage.set.assert_called_once()
125
+ self.mock_tracer.force_flush.assert_called_once()
126
+ self.mock_meter.force_flush.assert_called_once()
127
+ self.mock_logger.force_flush.assert_called_once()
128
+
129
+ def test_lambda_handler_with_sqs_event(self):
130
+ @lambda_handler("sqs_processor")
131
+ def handler(event, context):
132
+ return {"statusCode": 200}
133
+
134
+ sqs_event = {
135
+ "Records": [
136
+ {
137
+ "messageId": "msg1",
138
+ "eventSource": "aws:sqs",
139
+ "messageAttributes": {
140
+ "traceparent": {"stringValue": "00-0123456789abcdef0123456789abcdef-0123456789abcdef-01", "dataType": "String"}
141
+ }
142
+ }
143
+ ]
144
+ }
145
+ mock_context = MagicMock()
146
+ mock_context.aws_request_id = "req456"
147
+
148
+ handler(sqs_event, mock_context)
149
+
150
+ self.mock_propagate.extract.assert_called_once_with({'traceparent': '00-0123456789abcdef0123456789abcdef-0123456789abcdef-01'})
151
+ self.mock_context.attach.assert_called_once()
152
+ self.mock_context.detach.assert_called_once()
153
+ self.mock_tracer.start_span.assert_called_once_with(
154
+ name="sqs_processor",
155
+ attributes=ANY,
156
+ kind=SpanKind.SERVER
157
+ )
158
+ span_mock = self.mock_tracer.start_span.return_value.__enter__.return_value
159
+ self.assertEqual(span_mock.set_status.call_args[0][0].status_code, StatusCode.OK)
160
+ self.mock_logger.logger.info.assert_called_with(ANY)
161
+ self.mock_psutil.cpu_percent.assert_called_once()
162
+ self.mock_psutil.virtual_memory.assert_called_once()
163
+ self.mock_meter.GlobalMetrics.memory_usage_bytes.set.assert_called_once()
164
+ self.mock_meter.GlobalMetrics.cpu_usage_percentage.set.assert_called_once()
165
+ self.mock_tracer.force_flush.assert_called_once()
166
+ self.mock_meter.force_flush.assert_called_once()
167
+ self.mock_logger.force_flush.assert_called_once()
168
+
169
+ def test_lambda_handler_with_sns_event(self):
170
+ @lambda_handler("sns_processor")
171
+ def handler(event, context):
172
+ return {"statusCode": 200}
173
+
174
+ sns_event = {
175
+ "Records": [
176
+ {
177
+ "EventSource": "aws:sns",
178
+ "Sns": {
179
+ "MessageId": "sns_msg1",
180
+ "TopicArn": "arn:aws:sns:us-east-1:123456789012:my-topic",
181
+ "Subject": "test_subject",
182
+ "MessageAttributes": {
183
+ "traceparent": {"Type": "String", "Value": "00-fedcba9876543210fedcba9876543210-fedcba9876543210-01"}
184
+ }
185
+ }
186
+ }
187
+ ]
188
+ }
189
+ mock_context = MagicMock()
190
+ mock_context.aws_request_id = "req789"
191
+
192
+ handler(sns_event, mock_context)
193
+
194
+ self.mock_propagate.extract.assert_called_once_with({'traceparent': '00-fedcba9876543210fedcba9876543210-fedcba9876543210-01'})
195
+ self.mock_context.attach.assert_called_once()
196
+ self.mock_context.detach.assert_called_once()
197
+ self.mock_tracer.start_span.assert_called_once_with(
198
+ name="sns_processor",
199
+ attributes=ANY,
200
+ kind=SpanKind.SERVER
201
+ )
202
+ span_mock = self.mock_tracer.start_span.return_value.__enter__.return_value
203
+ self.assertEqual(span_mock.set_status.call_args[0][0].status_code, StatusCode.OK)
204
+ self.mock_logger.logger.info.assert_called_with(ANY)
205
+ self.mock_psutil.cpu_percent.assert_called_once()
206
+ self.mock_psutil.virtual_memory.assert_called_once()
207
+ self.mock_meter.GlobalMetrics.memory_usage_bytes.set.assert_called_once()
208
+ self.mock_meter.GlobalMetrics.cpu_usage_percentage.set.assert_called_once()
209
+ self.mock_tracer.force_flush.assert_called_once()
210
+ self.mock_meter.force_flush.assert_called_once()
211
+ self.mock_logger.force_flush.assert_called_once()
212
+
213
+ def test_aws_message_handler_basic(self):
214
+ @aws_message_handler("msg_processor")
215
+ def handler(record):
216
+ return {"processed": True}
217
+
218
+ record = {"messageId": "rec123"}
219
+ result = handler(record)
220
+
221
+ self.assertEqual(result, {"processed": True})
222
+ self.mock_tracer.start_span.assert_called_once_with(
223
+ name="msg_processor",
224
+ attributes={'messaging.operation': 'process'},
225
+ kind=SpanKind.CONSUMER
226
+ )
227
+ span_mock = self.mock_tracer.start_span.return_value.__enter__.return_value
228
+ self.assertEqual(span_mock.set_status.call_args[0][0].status_code, StatusCode.OK)
229
+ self.mock_psutil.cpu_percent.assert_called_once()
230
+ self.mock_psutil.virtual_memory.assert_called_once()
231
+ self.mock_meter.GlobalMetrics.memory_usage_bytes.set.assert_called_once()
232
+ self.mock_meter.GlobalMetrics.cpu_usage_percentage.set.assert_called_once()
233
+ self.mock_tracer.force_flush.assert_called_once()
234
+ self.mock_meter.force_flush.assert_called_once()
235
+ self.mock_logger.force_flush.assert_called_once()
236
+
237
+ def test_aws_message_span_context_manager(self):
238
+ sqs_message = {
239
+ "messageId": "sqs_msg_id",
240
+ "eventSource": "aws:sqs",
241
+ "awsRegion": "us-east-1",
242
+ "messageAttributes": {
243
+ "traceparent": {"stringValue": "00-11111111111111111111111111111111-2222222222222222-01", "dataType": "String"}
244
+ }
245
+ }
246
+ with otel.aws_message_span("process_sqs", message=sqs_message) as s:
247
+ self.assertIsNotNone(s)
248
+ s.set_attribute("custom_attr", "custom_value")
249
+
250
+ self.mock_tracer.start_span.assert_called_once_with(
251
+ name="process_sqs",
252
+ attributes={
253
+ 'messaging.operation': 'process',
254
+ 'messaging.message_id': 'sqs_msg_id',
255
+ 'messaging.system': 'aws:sqs',
256
+ 'cloud.region': 'us-east-1'
257
+ },
258
+ kind=SpanKind.CONSUMER
259
+ )
260
+ span_mock = self.mock_tracer.start_span.return_value.__enter__.return_value
261
+ span_mock.set_attribute.assert_called_once_with("custom_attr", "custom_value")
262
+ self.assertEqual(span_mock.set_status.call_args[0][0].status_code, StatusCode.OK)
263
+
264
+ def test_force_flush(self):
265
+ otel.force_flush()
266
+ self.mock_tracer.force_flush.assert_called_once()
267
+ self.mock_meter.force_flush.assert_called_once()
268
+ self.mock_logger.force_flush.assert_called_once()
269
+
270
+ def test_shutdown(self):
271
+ otel.shutdown()
272
+ self.mock_tracer.shutdown.assert_called_once()
273
+ self.mock_meter.shutdown.assert_called_once()
274
+ self.mock_logger.shutdown.assert_called_once()
275
+
276
+ def test_detect_lambda_trigger(self):
277
+ # Test direct invocation
278
+ self.assertEqual(otel._detect_lambda_trigger(None), 'direct')
279
+ self.assertEqual(otel._detect_lambda_trigger({}), 'direct')
280
+
281
+ # Test SQS
282
+ sqs_event = {'Records': [{'eventSource': 'aws:sqs'}]}
283
+ self.assertEqual(otel._detect_lambda_trigger(sqs_event), 'sqs')
284
+
285
+ # Test SNS
286
+ sns_event = {'Records': [{'eventSource': 'aws:sns'}]}
287
+ self.assertEqual(otel._detect_lambda_trigger(sns_event), 'sns')
288
+
289
+ # Test API Gateway
290
+ api_gw_event = {'httpMethod': 'GET'}
291
+ self.assertEqual(otel._detect_lambda_trigger(api_gw_event), 'api_gateway')
292
+
293
+ api_gw_v2_event = {'requestContext': {'http': {}}}
294
+ self.assertEqual(otel._detect_lambda_trigger(api_gw_v2_event), 'api_gateway_v2')
295
+
296
+ # Test EventBridge
297
+ eventbridge_event = {'source': 'aws.events'}
298
+ self.assertEqual(otel._detect_lambda_trigger(eventbridge_event), 'eventbridge')
299
+
300
+ # Test unknown
301
+ unknown_event = {'some_key': 'some_value'}
302
+ self.assertEqual(otel._detect_lambda_trigger(unknown_event), 'unknown')
303
+
304
+ def test_context_propagation_methods(self):
305
+ # Mock the global propagate and context objects
306
+ mock_propagate = MagicMock()
307
+ mock_context = MagicMock()
308
+
309
+ with patch('src.rebrandly_otel.propagate', mock_propagate), patch('src.rebrandly_otel.context', mock_context):
310
+
311
+ # Test inject_context
312
+ carrier = {}
313
+ otel.inject_context(carrier)
314
+ mock_propagate.inject.assert_called_once_with(carrier)
315
+
316
+ # Test extract_context
317
+ extracted_ctx = otel.extract_context(carrier)
318
+ mock_propagate.extract.assert_called_once_with(carrier)
319
+ self.assertEqual(extracted_ctx, mock_propagate.extract.return_value)
320
+
321
+ # Test attach_context
322
+ mock_propagate.extract.reset_mock()
323
+ mock_context.attach.reset_mock()
324
+ token = otel.attach_context(carrier)
325
+ mock_propagate.extract.assert_called_once_with(carrier)
326
+ mock_context.attach.assert_called_once_with(mock_propagate.extract.return_value)
327
+ self.assertEqual(token, mock_context.attach.return_value)
328
+
329
+ # Test detach_context
330
+ mock_context.detach.reset_mock()
331
+ otel.detach_context(token)
332
+ mock_context.detach.assert_called_once_with(token)
333
+
334
+
335
+ if __name__ == '__main__':
336
+ unittest.main()
File without changes