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.
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/PKG-INFO +88 -1
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/README.md +87 -0
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/rebrandly_otel.egg-info/PKG-INFO +88 -1
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/rebrandly_otel.egg-info/SOURCES.txt +2 -1
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/setup.py +1 -1
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/src/logs.py +7 -3
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/src/metrics.py +6 -2
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/src/otel_utils.py +31 -7
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/src/rebrandly_otel.py +2 -1
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/src/traces.py +10 -6
- rebrandly_otel-0.1.19/tests/test_usage.py +336 -0
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/LICENSE +0 -0
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/rebrandly_otel.egg-info/dependency_links.txt +0 -0
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/rebrandly_otel.egg-info/top_level.txt +0 -0
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/setup.cfg +0 -0
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/src/__init__.py +0 -0
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/src/fastapi_support.py +0 -0
- {rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/src/flask_support.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rebrandly_otel
|
|
3
|
-
Version: 0.1.
|
|
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.
|
|
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
|
|
|
@@ -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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
150
|
+
"""Record success on a span."""
|
|
147
151
|
target_span = span or self.get_current_span()
|
|
148
|
-
if target_span and hasattr(target_span, '
|
|
149
|
-
target_span.set_status(trace.Status(trace.StatusCode.OK)
|
|
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
|
{rebrandly_otel-0.1.18 → rebrandly_otel-0.1.19}/rebrandly_otel.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|