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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rebrandly_otel
3
- Version: 0.1.17
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.17
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
@@ -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.17",
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
  # 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