rebrandly-otel 0.1.17__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.17 → rebrandly_otel-0.1.19}/PKG-INFO +88 -16
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/README.md +87 -15
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/rebrandly_otel.egg-info/PKG-INFO +88 -16
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/rebrandly_otel.egg-info/SOURCES.txt +3 -1
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/setup.py +1 -1
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/src/__init__.py +4 -2
- rebrandly_otel-0.1.19/src/fastapi_support.py +198 -0
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/src/flask_support.py +35 -52
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/src/logs.py +7 -3
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/src/metrics.py +13 -36
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/src/otel_utils.py +31 -7
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/src/rebrandly_otel.py +4 -35
- {rebrandly_otel-0.1.17 → 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.17 → rebrandly_otel-0.1.19}/LICENSE +0 -0
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/rebrandly_otel.egg-info/dependency_links.txt +0 -0
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/rebrandly_otel.egg-info/top_level.txt +0 -0
- {rebrandly_otel-0.1.17 → rebrandly_otel-0.1.19}/setup.cfg +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
|
|
@@ -102,22 +102,9 @@ The SDK automatically registers and tracks the following metrics:
|
|
|
102
102
|
|
|
103
103
|
### Standard Metrics
|
|
104
104
|
|
|
105
|
-
- **`invocations`** (Counter): Total number of function invocations
|
|
106
|
-
- **`successful_invocations`** (Counter): Number of successful completions
|
|
107
|
-
- **`error_invocations`** (Counter): Number of failed invocations
|
|
108
|
-
- **`duration`** (Histogram): Execution duration in milliseconds
|
|
109
105
|
- **`cpu_usage_percentage`** (Gauge): CPU utilization percentage
|
|
110
106
|
- **`memory_usage_bytes`** (Gauge): Memory usage in bytes
|
|
111
107
|
|
|
112
|
-
### Global Metrics Access
|
|
113
|
-
|
|
114
|
-
```python
|
|
115
|
-
from rebrandly_otel import meter
|
|
116
|
-
|
|
117
|
-
# Access pre-configured metrics
|
|
118
|
-
meter.GlobalMetrics.invocations.add(1, {'function': 'my_function'})
|
|
119
|
-
meter.GlobalMetrics.duration.record(150.5, {'source': 'api'})
|
|
120
|
-
```
|
|
121
108
|
|
|
122
109
|
### Custom Metrics
|
|
123
110
|
|
|
@@ -195,10 +182,8 @@ Automatically detects and labels Lambda triggers:
|
|
|
195
182
|
### Automatic Metrics
|
|
196
183
|
|
|
197
184
|
For Lambda functions, the SDK automatically captures:
|
|
198
|
-
- Function duration
|
|
199
185
|
- Memory usage
|
|
200
186
|
- CPU utilization
|
|
201
|
-
- Invocation counts by status
|
|
202
187
|
|
|
203
188
|
### Context Extraction
|
|
204
189
|
|
|
@@ -375,6 +360,93 @@ if __name__ == '__main__':
|
|
|
375
360
|
app.run(debug=True)
|
|
376
361
|
```
|
|
377
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
|
+
|
|
378
450
|
### More examples
|
|
379
451
|
You can find More examples [here](examples)
|
|
380
452
|
|
|
@@ -81,22 +81,9 @@ The SDK automatically registers and tracks the following metrics:
|
|
|
81
81
|
|
|
82
82
|
### Standard Metrics
|
|
83
83
|
|
|
84
|
-
- **`invocations`** (Counter): Total number of function invocations
|
|
85
|
-
- **`successful_invocations`** (Counter): Number of successful completions
|
|
86
|
-
- **`error_invocations`** (Counter): Number of failed invocations
|
|
87
|
-
- **`duration`** (Histogram): Execution duration in milliseconds
|
|
88
84
|
- **`cpu_usage_percentage`** (Gauge): CPU utilization percentage
|
|
89
85
|
- **`memory_usage_bytes`** (Gauge): Memory usage in bytes
|
|
90
86
|
|
|
91
|
-
### Global Metrics Access
|
|
92
|
-
|
|
93
|
-
```python
|
|
94
|
-
from rebrandly_otel import meter
|
|
95
|
-
|
|
96
|
-
# Access pre-configured metrics
|
|
97
|
-
meter.GlobalMetrics.invocations.add(1, {'function': 'my_function'})
|
|
98
|
-
meter.GlobalMetrics.duration.record(150.5, {'source': 'api'})
|
|
99
|
-
```
|
|
100
87
|
|
|
101
88
|
### Custom Metrics
|
|
102
89
|
|
|
@@ -174,10 +161,8 @@ Automatically detects and labels Lambda triggers:
|
|
|
174
161
|
### Automatic Metrics
|
|
175
162
|
|
|
176
163
|
For Lambda functions, the SDK automatically captures:
|
|
177
|
-
- Function duration
|
|
178
164
|
- Memory usage
|
|
179
165
|
- CPU utilization
|
|
180
|
-
- Invocation counts by status
|
|
181
166
|
|
|
182
167
|
### Context Extraction
|
|
183
168
|
|
|
@@ -354,6 +339,93 @@ if __name__ == '__main__':
|
|
|
354
339
|
app.run(debug=True)
|
|
355
340
|
```
|
|
356
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
|
+
|
|
357
429
|
### More examples
|
|
358
430
|
You can find More examples [here](examples)
|
|
359
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
|
|
@@ -102,22 +102,9 @@ The SDK automatically registers and tracks the following metrics:
|
|
|
102
102
|
|
|
103
103
|
### Standard Metrics
|
|
104
104
|
|
|
105
|
-
- **`invocations`** (Counter): Total number of function invocations
|
|
106
|
-
- **`successful_invocations`** (Counter): Number of successful completions
|
|
107
|
-
- **`error_invocations`** (Counter): Number of failed invocations
|
|
108
|
-
- **`duration`** (Histogram): Execution duration in milliseconds
|
|
109
105
|
- **`cpu_usage_percentage`** (Gauge): CPU utilization percentage
|
|
110
106
|
- **`memory_usage_bytes`** (Gauge): Memory usage in bytes
|
|
111
107
|
|
|
112
|
-
### Global Metrics Access
|
|
113
|
-
|
|
114
|
-
```python
|
|
115
|
-
from rebrandly_otel import meter
|
|
116
|
-
|
|
117
|
-
# Access pre-configured metrics
|
|
118
|
-
meter.GlobalMetrics.invocations.add(1, {'function': 'my_function'})
|
|
119
|
-
meter.GlobalMetrics.duration.record(150.5, {'source': 'api'})
|
|
120
|
-
```
|
|
121
108
|
|
|
122
109
|
### Custom Metrics
|
|
123
110
|
|
|
@@ -195,10 +182,8 @@ Automatically detects and labels Lambda triggers:
|
|
|
195
182
|
### Automatic Metrics
|
|
196
183
|
|
|
197
184
|
For Lambda functions, the SDK automatically captures:
|
|
198
|
-
- Function duration
|
|
199
185
|
- Memory usage
|
|
200
186
|
- CPU utilization
|
|
201
|
-
- Invocation counts by status
|
|
202
187
|
|
|
203
188
|
### Context Extraction
|
|
204
189
|
|
|
@@ -375,6 +360,93 @@ if __name__ == '__main__':
|
|
|
375
360
|
app.run(debug=True)
|
|
376
361
|
```
|
|
377
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
|
+
|
|
378
450
|
### More examples
|
|
379
451
|
You can find More examples [here](examples)
|
|
380
452
|
|
|
@@ -6,9 +6,11 @@ rebrandly_otel.egg-info/SOURCES.txt
|
|
|
6
6
|
rebrandly_otel.egg-info/dependency_links.txt
|
|
7
7
|
rebrandly_otel.egg-info/top_level.txt
|
|
8
8
|
src/__init__.py
|
|
9
|
+
src/fastapi_support.py
|
|
9
10
|
src/flask_support.py
|
|
10
11
|
src/logs.py
|
|
11
12
|
src/metrics.py
|
|
12
13
|
src/otel_utils.py
|
|
13
14
|
src/rebrandly_otel.py
|
|
14
|
-
src/traces.py
|
|
15
|
+
src/traces.py
|
|
16
|
+
tests/test_usage.py
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# src/__init__.py
|
|
2
2
|
from .rebrandly_otel import *
|
|
3
|
-
from .flask_support import
|
|
3
|
+
from .flask_support import setup_flask
|
|
4
|
+
from .fastapi_support import setup_fastapi
|
|
4
5
|
|
|
5
6
|
# Explicitly define what's available
|
|
6
7
|
__all__ = [
|
|
@@ -14,5 +15,6 @@ __all__ = [
|
|
|
14
15
|
'force_flush',
|
|
15
16
|
'aws_message_handler',
|
|
16
17
|
'shutdown',
|
|
17
|
-
'setup_flask'
|
|
18
|
+
'setup_flask',
|
|
19
|
+
'setup_fastapi'
|
|
18
20
|
]
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# fastapi_integration.py
|
|
2
|
+
"""FastAPI integration for Rebrandly OTEL SDK."""
|
|
3
|
+
import json
|
|
4
|
+
from rebrandly_otel import Status, StatusCode, SpanKind
|
|
5
|
+
from fastapi import HTTPException, Depends
|
|
6
|
+
from starlette.requests import Request
|
|
7
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
def setup_fastapi(otel , app):
|
|
13
|
+
"""
|
|
14
|
+
Setup FastAPI application with OTEL instrumentation.
|
|
15
|
+
|
|
16
|
+
Example:
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
from rebrandly_otel import otel
|
|
19
|
+
from rebrandly_otel.fastapi_integration import setup_fastapi
|
|
20
|
+
|
|
21
|
+
app = FastAPI()
|
|
22
|
+
setup_fastapi(otel, app)
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# Add middleware
|
|
26
|
+
add_otel_middleware(otel, app)
|
|
27
|
+
|
|
28
|
+
# Add exception handlers
|
|
29
|
+
app.add_exception_handler(HTTPException, lambda request, exc: fastapi_exception_handler(otel, request, exc))
|
|
30
|
+
app.add_exception_handler(Exception, lambda request, exc: fastapi_exception_handler(otel, request, exc))
|
|
31
|
+
|
|
32
|
+
return app
|
|
33
|
+
|
|
34
|
+
def add_otel_middleware(otel, app):
|
|
35
|
+
"""
|
|
36
|
+
Add OTEL middleware to FastAPI application.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
class OTELMiddleware(BaseHTTPMiddleware):
|
|
40
|
+
def __init__(self, app):
|
|
41
|
+
super().__init__(app)
|
|
42
|
+
self.otel = otel
|
|
43
|
+
|
|
44
|
+
async def dispatch(self, request: Request, call_next):
|
|
45
|
+
# Extract trace context from headers
|
|
46
|
+
headers = dict(request.headers)
|
|
47
|
+
token = self.otel.attach_context(headers)
|
|
48
|
+
|
|
49
|
+
# Start span for request
|
|
50
|
+
span_name = f"{request.method} {request.url.path}"
|
|
51
|
+
|
|
52
|
+
# Use start_as_current_span for proper context propagation
|
|
53
|
+
with self.otel.tracer.tracer.start_as_current_span(
|
|
54
|
+
span_name,
|
|
55
|
+
attributes={
|
|
56
|
+
# Required HTTP attributes per semantic conventions
|
|
57
|
+
"http.request.method": request.method,
|
|
58
|
+
"http.request.headers": json.dumps(request.headers, default=str),
|
|
59
|
+
"http.response.status_code": None, # Will be set after response
|
|
60
|
+
"url.full": str(request.url),
|
|
61
|
+
"url.scheme": request.url.scheme,
|
|
62
|
+
"url.path": request.url.path,
|
|
63
|
+
"url.query": request.url.query if request.url.query else None,
|
|
64
|
+
"user_agent.original": request.headers.get("user-agent"),
|
|
65
|
+
"http.route": None, # Will be set after routing
|
|
66
|
+
"network.protocol.version": "1.1", # FastAPI/Starlette typically uses HTTP/1.1
|
|
67
|
+
"server.address": request.url.hostname,
|
|
68
|
+
"server.port": request.url.port or (443 if request.url.scheme == 'https' else 80),
|
|
69
|
+
"client.address": request.client.host if request.client else None,
|
|
70
|
+
},
|
|
71
|
+
kind=SpanKind.SERVER
|
|
72
|
+
) as span:
|
|
73
|
+
# Log request start
|
|
74
|
+
self.otel.logger.logger.info(f"Request started: {request.method} {request.url.path}",
|
|
75
|
+
extra={"http.method": request.method, "http.path": request.url.path})
|
|
76
|
+
|
|
77
|
+
# Store span in request state for access in routes
|
|
78
|
+
request.state.span = span
|
|
79
|
+
request.state.trace_token = token
|
|
80
|
+
|
|
81
|
+
start_time = time.time()
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
# Process request
|
|
85
|
+
response = await call_next(request)
|
|
86
|
+
|
|
87
|
+
# After routing, update span name and route if available
|
|
88
|
+
if hasattr(request, 'scope') and 'path' in request.scope:
|
|
89
|
+
route = request.scope.get('path', request.url.path)
|
|
90
|
+
span.update_name(f"{request.method} {route}")
|
|
91
|
+
span.set_attribute("http.route", route)
|
|
92
|
+
|
|
93
|
+
# Set response attributes using new semantic conventions
|
|
94
|
+
span.set_attribute("http.response.status_code", response.status_code)
|
|
95
|
+
span.set_attribute("http.status_code", response.status_code) # Deprecated
|
|
96
|
+
|
|
97
|
+
# Set span status based on HTTP status code
|
|
98
|
+
if response.status_code >= 400:
|
|
99
|
+
span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
|
|
100
|
+
else:
|
|
101
|
+
span.set_status(Status(StatusCode.OK))
|
|
102
|
+
|
|
103
|
+
# Log request completion
|
|
104
|
+
self.otel.logger.logger.info(f"Request completed: {response.status_code}",
|
|
105
|
+
extra={"http.status_code": response.status_code})
|
|
106
|
+
otel.force_flush(timeout_millis=100)
|
|
107
|
+
return response
|
|
108
|
+
|
|
109
|
+
except Exception as e:
|
|
110
|
+
# Record exception
|
|
111
|
+
span.record_exception(e)
|
|
112
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
113
|
+
span.add_event("exception", {
|
|
114
|
+
"exception.type": type(e).__name__,
|
|
115
|
+
"exception.message": str(e)
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
# Log error
|
|
119
|
+
self.otel.logger.logger.error(f"Unhandled exception: {e}",
|
|
120
|
+
exc_info=True,
|
|
121
|
+
extra={"exception.type": type(e).__name__})
|
|
122
|
+
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
finally:
|
|
126
|
+
# Detach context
|
|
127
|
+
self.otel.detach_context(token)
|
|
128
|
+
|
|
129
|
+
# Add middleware to app
|
|
130
|
+
app.add_middleware(OTELMiddleware)
|
|
131
|
+
|
|
132
|
+
def fastapi_exception_handler(otel, request, exc):
|
|
133
|
+
"""
|
|
134
|
+
Handle FastAPI exceptions and record them in the current span.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
# Determine the status code
|
|
138
|
+
if isinstance(exc, HTTPException):
|
|
139
|
+
status_code = exc.status_code
|
|
140
|
+
error_detail = exc.detail
|
|
141
|
+
elif hasattr(exc, 'status_code'):
|
|
142
|
+
status_code = exc.status_code
|
|
143
|
+
error_detail = str(exc)
|
|
144
|
+
elif hasattr(exc, 'code'):
|
|
145
|
+
status_code = exc.code if isinstance(exc.code, int) else 500
|
|
146
|
+
error_detail = str(exc)
|
|
147
|
+
else:
|
|
148
|
+
status_code = 500
|
|
149
|
+
error_detail = str(exc)
|
|
150
|
+
|
|
151
|
+
# Record exception in span if available and still recording
|
|
152
|
+
if hasattr(request.state, 'span') and request.state.span.is_recording():
|
|
153
|
+
# Update both new and old attribute names for compatibility
|
|
154
|
+
request.state.span.set_attribute("http.response.status_code", status_code)
|
|
155
|
+
request.state.span.set_attribute("error.type", type(exc).__name__)
|
|
156
|
+
|
|
157
|
+
request.state.span.record_exception(exc)
|
|
158
|
+
request.state.span.set_status(Status(StatusCode.ERROR, str(exc)))
|
|
159
|
+
request.state.span.add_event("exception", {
|
|
160
|
+
"exception.type": type(exc).__name__,
|
|
161
|
+
"exception.message": str(exc)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
# Log the error
|
|
165
|
+
otel.logger.logger.error(f"Unhandled exception: {exc} (status: {status_code})",
|
|
166
|
+
exc_info=True,
|
|
167
|
+
extra={
|
|
168
|
+
"exception.type": type(exc).__name__,
|
|
169
|
+
"http.status_code": status_code
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
# Return error response
|
|
173
|
+
return JSONResponse(
|
|
174
|
+
status_code=status_code,
|
|
175
|
+
content={
|
|
176
|
+
"error": error_detail,
|
|
177
|
+
"type": type(exc).__name__
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Optional: Dependency injection helper for accessing the span in routes
|
|
182
|
+
def get_current_span(request: Request):
|
|
183
|
+
"""
|
|
184
|
+
FastAPI dependency to get the current span in route handlers.
|
|
185
|
+
|
|
186
|
+
Example:
|
|
187
|
+
from fastapi import Depends
|
|
188
|
+
from rebrandly_otel.fastapi_integration import get_current_span
|
|
189
|
+
|
|
190
|
+
@app.get("/example")
|
|
191
|
+
async def example(span = Depends(get_current_span)):
|
|
192
|
+
if span:
|
|
193
|
+
span.add_event("custom_event", {"key": "value"})
|
|
194
|
+
return {"status": "ok"}
|
|
195
|
+
"""
|
|
196
|
+
if hasattr(request.state, 'span'):
|
|
197
|
+
return request.state.span
|
|
198
|
+
return None
|