rebrandly-otel 0.1.18__tar.gz → 0.1.19__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.19
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.19
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.19",
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,7 @@
3
3
 
4
4
  import os
5
5
  import sys
6
+ import grpc
6
7
 
7
8
  from opentelemetry.sdk.resources import Resource
8
9
  from opentelemetry.semconv.attributes import service_attributes
@@ -33,8 +34,9 @@ def get_package_version():
33
34
  try:
34
35
  from importlib_metadata import version, PackageNotFoundError
35
36
  return version('rebrandly_otel')
36
- except:
37
- return '?.0.1'
37
+ except Exception as e:
38
+ print(f"[OTEL Utils] Warning: Could not get package version: {e}")
39
+ return '0.1.0'
38
40
 
39
41
 
40
42
  def get_service_name(service_name: str = None) -> str:
@@ -49,10 +51,31 @@ def get_service_version(service_version: str = None) -> str:
49
51
  return service_version
50
52
 
51
53
 
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
54
+ def get_otlp_endpoint(otlp_endpoint: str = None) -> str | None:
55
+ endpoint = otlp_endpoint or os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT', None)
56
+
57
+ if endpoint is not None:
58
+ try:
59
+ from urllib.parse import urlparse
60
+
61
+ # Parse the endpoint
62
+ parsed = urlparse(endpoint if '://' in endpoint else f'http://{endpoint}')
63
+ host = parsed.hostname
64
+ port = parsed.port
65
+
66
+ # Test gRPC connection
67
+ channel = grpc.insecure_channel(f'{host}:{port}')
68
+ try:
69
+ # Wait for the channel to be ready
70
+ grpc.channel_ready_future(channel).result(timeout=3)
71
+ return endpoint
72
+ finally:
73
+ channel.close()
74
+
75
+ except Exception as e:
76
+ print(f"[OTEL] Failed to connect to OTLP endpoint {endpoint}: {e}")
77
+ return None
78
+ return endpoint
56
79
 
57
80
  def is_otel_debug() -> bool:
58
81
  return os.environ.get('OTEL_DEBUG', 'false').lower() == 'true'
@@ -61,5 +84,6 @@ def is_otel_debug() -> bool:
61
84
  def get_millis_batch_time():
62
85
  try:
63
86
  return int(os.environ.get('BATCH_EXPORT_TIME_MILLIS', 100))
64
- except:
87
+ except Exception as e:
88
+ print(f"[OTEL Utils] Warning: Invalid BATCH_EXPORT_TIME_MILLIS value, using default 5000ms: {e}")
65
89
  return 5000
@@ -260,7 +260,7 @@ class RebrandlyOTEL:
260
260
  try:
261
261
  # Create span and execute function
262
262
  span_function = self.span
263
- if record is not None and ('MessageAttributes' in record or 'messageAttributes' in record):
263
+ if record is not None and (('MessageAttributes' in record or 'messageAttributes' in record) or ('Sns' in record and 'MessageAttributes' in record['Sns'])):
264
264
  span_function = self.aws_message_span
265
265
 
266
266
  with span_function(span_name, message=record, attributes=span_attributes, kind=kind) as span_context:
@@ -370,6 +370,7 @@ class RebrandlyOTEL:
370
370
  time.sleep(0.1)
371
371
 
372
372
  except Exception as e:
373
+ print(f"[Rebrandly OTEL] Error during force flush: {e}")
373
374
  success = False
374
375
 
375
376
  return success
@@ -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