rebrandly-otel 0.1.16__py3-none-any.whl → 0.1.18__py3-none-any.whl
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/__init__.py +5 -1
- rebrandly_otel/fastapi_support.py +198 -0
- rebrandly_otel/flask_support.py +153 -0
- rebrandly_otel/metrics.py +7 -34
- rebrandly_otel/otel_utils.py +12 -1
- rebrandly_otel/rebrandly_otel.py +5 -40
- {rebrandly_otel-0.1.16.dist-info → rebrandly_otel-0.1.18.dist-info}/METADATA +54 -13
- rebrandly_otel-0.1.18.dist-info/RECORD +13 -0
- rebrandly_otel-0.1.16.dist-info/RECORD +0 -11
- {rebrandly_otel-0.1.16.dist-info → rebrandly_otel-0.1.18.dist-info}/WHEEL +0 -0
- {rebrandly_otel-0.1.16.dist-info → rebrandly_otel-0.1.18.dist-info}/licenses/LICENSE +0 -0
- {rebrandly_otel-0.1.16.dist-info → rebrandly_otel-0.1.18.dist-info}/top_level.txt +0 -0
rebrandly_otel/__init__.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# src/__init__.py
|
|
2
2
|
from .rebrandly_otel import *
|
|
3
|
+
from .flask_support import setup_flask
|
|
4
|
+
from .fastapi_support import setup_fastapi
|
|
3
5
|
|
|
4
6
|
# Explicitly define what's available
|
|
5
7
|
__all__ = [
|
|
@@ -12,5 +14,7 @@ __all__ = [
|
|
|
12
14
|
'logger',
|
|
13
15
|
'force_flush',
|
|
14
16
|
'aws_message_handler',
|
|
15
|
-
'shutdown'
|
|
17
|
+
'shutdown',
|
|
18
|
+
'setup_flask',
|
|
19
|
+
'setup_fastapi'
|
|
16
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
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# flask_integration.py
|
|
2
|
+
"""Flask integration for Rebrandly OTEL SDK."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from rebrandly_otel import Status, StatusCode, SpanKind
|
|
6
|
+
|
|
7
|
+
from flask import request, jsonify
|
|
8
|
+
from werkzeug.exceptions import HTTPException
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def setup_flask(otel, app):
|
|
12
|
+
"""
|
|
13
|
+
Setup Flask application with OTEL instrumentation.
|
|
14
|
+
|
|
15
|
+
Example:
|
|
16
|
+
from flask import Flask
|
|
17
|
+
from rebrandly_otel import otel
|
|
18
|
+
from rebrandly_otel.flask_integration import setup_flask
|
|
19
|
+
|
|
20
|
+
app = Flask(__name__)
|
|
21
|
+
setup_flask(otel, app)
|
|
22
|
+
"""
|
|
23
|
+
app.before_request(lambda: app_before_request(otel))
|
|
24
|
+
app.after_request(lambda response: app_after_request(otel, response))
|
|
25
|
+
app.register_error_handler(Exception, lambda e: flask_error_handler(otel, e))
|
|
26
|
+
return app
|
|
27
|
+
|
|
28
|
+
def app_before_request(otel):
|
|
29
|
+
"""
|
|
30
|
+
Setup tracing for incoming Flask request.
|
|
31
|
+
To be used with Flask's before_request hook.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
# Extract trace context from headers
|
|
35
|
+
headers = dict(request.headers)
|
|
36
|
+
token = otel.attach_context(headers)
|
|
37
|
+
request.trace_token = token
|
|
38
|
+
|
|
39
|
+
# Determine span name - use route if available, otherwise just method
|
|
40
|
+
# Route will be available after request routing is done
|
|
41
|
+
span_name = f"{request.method} {request.path}"
|
|
42
|
+
|
|
43
|
+
# Start span for request using start_as_current_span to make it the active span
|
|
44
|
+
span = otel.tracer.tracer.start_as_current_span(
|
|
45
|
+
span_name,
|
|
46
|
+
attributes={
|
|
47
|
+
# Required HTTP attributes per semantic conventions
|
|
48
|
+
"http.request.method": request.method,
|
|
49
|
+
"http.request.headers": json.dumps(request.headers, default=str),
|
|
50
|
+
"http.response.status_code": None, # Will be set in after_request
|
|
51
|
+
"url.full": request.url,
|
|
52
|
+
"url.scheme": request.scheme,
|
|
53
|
+
"url.path": request.path,
|
|
54
|
+
"url.query": request.query_string.decode('utf-8') if request.query_string else None,
|
|
55
|
+
"user_agent.original": request.user_agent.string if request.user_agent else None,
|
|
56
|
+
"http.route": request.path, # Flask doesn't expose route pattern easily
|
|
57
|
+
"network.protocol.version": request.environ.get('SERVER_PROTOCOL', 'HTTP/1.1').split('/')[-1],
|
|
58
|
+
"server.address": request.host.split(':')[0],
|
|
59
|
+
"server.port": request.host.split(':')[1] if ':' in request.host else (443 if request.scheme == 'https' else 80),
|
|
60
|
+
"client.address": request.remote_addr,
|
|
61
|
+
},
|
|
62
|
+
kind=SpanKind.SERVER
|
|
63
|
+
)
|
|
64
|
+
# Store both the span context manager and the span itself
|
|
65
|
+
request.span_context = span
|
|
66
|
+
request.span = span.__enter__() # This activates the span and returns the span object
|
|
67
|
+
|
|
68
|
+
# Log request start
|
|
69
|
+
otel.logger.logger.info(f"Request started: {request.method} {request.path}",
|
|
70
|
+
extra={"http.method": request.method, "http.path": request.path})
|
|
71
|
+
|
|
72
|
+
def app_after_request(otel, response):
|
|
73
|
+
"""
|
|
74
|
+
Cleanup tracing after Flask request completes.
|
|
75
|
+
To be used with Flask's after_request hook.
|
|
76
|
+
"""
|
|
77
|
+
|
|
78
|
+
# Check if we have a span and it's still recording
|
|
79
|
+
if hasattr(request, 'span') and request.span.is_recording():
|
|
80
|
+
# Update both new and old attribute names for compatibility
|
|
81
|
+
request.span.set_attribute("http.response.status_code", response.status_code)
|
|
82
|
+
|
|
83
|
+
# Set span status based on HTTP status code
|
|
84
|
+
if response.status_code >= 400:
|
|
85
|
+
request.span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
|
|
86
|
+
else:
|
|
87
|
+
request.span.set_status(Status(StatusCode.OK))
|
|
88
|
+
|
|
89
|
+
# Properly close the span context manager
|
|
90
|
+
if hasattr(request, 'span_context'):
|
|
91
|
+
request.span_context.__exit__(None, None, None)
|
|
92
|
+
else:
|
|
93
|
+
# Fallback if we don't have the context manager
|
|
94
|
+
request.span.end()
|
|
95
|
+
|
|
96
|
+
# Detach context
|
|
97
|
+
if hasattr(request, 'trace_token'):
|
|
98
|
+
otel.detach_context(request.trace_token)
|
|
99
|
+
|
|
100
|
+
# Log request completion
|
|
101
|
+
otel.logger.logger.info(f"Request completed: {response.status_code}",
|
|
102
|
+
extra={"http.status_code": response.status_code})
|
|
103
|
+
|
|
104
|
+
otel.force_flush(timeout_millis=100)
|
|
105
|
+
return response
|
|
106
|
+
|
|
107
|
+
def flask_error_handler(otel, exception):
|
|
108
|
+
"""
|
|
109
|
+
Handle Flask exceptions and record them in the current span.
|
|
110
|
+
To be used with Flask's errorhandler decorator.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
# Determine the status code
|
|
114
|
+
if isinstance(exception, HTTPException):
|
|
115
|
+
status_code = exception.code
|
|
116
|
+
elif hasattr(exception, 'status_code'):
|
|
117
|
+
status_code = exception.status_code
|
|
118
|
+
elif hasattr(exception, 'code'):
|
|
119
|
+
status_code = exception.code if isinstance(exception.code, int) else 500
|
|
120
|
+
else:
|
|
121
|
+
status_code = 500
|
|
122
|
+
|
|
123
|
+
# Record exception in span if available and still recording
|
|
124
|
+
if hasattr(request, 'span') and request.span.is_recording():
|
|
125
|
+
request.span.set_attribute("http.response.status_code", status_code)
|
|
126
|
+
request.span.set_attribute("error.type", type(exception).__name__)
|
|
127
|
+
|
|
128
|
+
request.span.record_exception(exception)
|
|
129
|
+
request.span.set_status(Status(StatusCode.ERROR, str(exception)))
|
|
130
|
+
request.span.add_event("exception", {
|
|
131
|
+
"exception.type": type(exception).__name__,
|
|
132
|
+
"exception.message": str(exception)
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
# Only close the span if it's still recording (not already ended)
|
|
136
|
+
if hasattr(request, 'span_context'):
|
|
137
|
+
request.span_context.__exit__(type(exception), exception, None)
|
|
138
|
+
else:
|
|
139
|
+
request.span.end()
|
|
140
|
+
|
|
141
|
+
# Log the error with status code
|
|
142
|
+
otel.logger.logger.error(f"Unhandled exception: {exception} (status: {status_code})",
|
|
143
|
+
exc_info=True,
|
|
144
|
+
extra={
|
|
145
|
+
"exception.type": type(exception).__name__,
|
|
146
|
+
"http.status_code": status_code
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
# Return error response with the determined status code
|
|
150
|
+
return jsonify({
|
|
151
|
+
"error": str(exception),
|
|
152
|
+
"type": type(exception).__name__
|
|
153
|
+
}), status_code
|
rebrandly_otel/metrics.py
CHANGED
|
@@ -34,51 +34,24 @@ class RebrandlyMeter:
|
|
|
34
34
|
|
|
35
35
|
# Standardized metric definitions aligned with Node.js
|
|
36
36
|
DEFAULT_METRICS = {
|
|
37
|
-
|
|
38
|
-
name='invocations',
|
|
39
|
-
description='Number of invocations',
|
|
40
|
-
unit='1',
|
|
41
|
-
type=MetricType.COUNTER
|
|
42
|
-
),
|
|
43
|
-
'successful_invocations': MetricDefinition(
|
|
44
|
-
name='successful_invocations',
|
|
45
|
-
description='Number of successful invocations',
|
|
46
|
-
unit='1',
|
|
47
|
-
type=MetricType.COUNTER
|
|
48
|
-
),
|
|
49
|
-
'error_invocations': MetricDefinition(
|
|
50
|
-
name='error_invocations',
|
|
51
|
-
description='Number of error invocations',
|
|
52
|
-
unit='1',
|
|
53
|
-
type=MetricType.COUNTER
|
|
54
|
-
),
|
|
55
|
-
'duration': MetricDefinition(
|
|
56
|
-
name='duration',
|
|
57
|
-
description='Duration of execution in milliseconds',
|
|
58
|
-
unit='ms',
|
|
59
|
-
type=MetricType.HISTOGRAM
|
|
60
|
-
),
|
|
37
|
+
## PROCESS
|
|
61
38
|
'cpu_usage_percentage': MetricDefinition(
|
|
62
|
-
name='
|
|
63
|
-
description='
|
|
64
|
-
unit='
|
|
39
|
+
name='process.cpu.utilization',
|
|
40
|
+
description='Difference in process.cpu.time since the last measurement, divided by the elapsed time and number of CPUs available to the process.',
|
|
41
|
+
unit='1',
|
|
65
42
|
type=MetricType.GAUGE
|
|
66
43
|
),
|
|
67
44
|
'memory_usage_bytes': MetricDefinition(
|
|
68
|
-
name='
|
|
69
|
-
description='
|
|
45
|
+
name='process.memory.used',
|
|
46
|
+
description='The amount of physical memory in use.',
|
|
70
47
|
unit='By',
|
|
71
48
|
type=MetricType.GAUGE
|
|
72
|
-
)
|
|
49
|
+
)
|
|
73
50
|
}
|
|
74
51
|
|
|
75
52
|
class GlobalMetrics:
|
|
76
53
|
def __init__(self, rebrandly_meter):
|
|
77
54
|
self.__rebrandly_meter = rebrandly_meter
|
|
78
|
-
self.invocations: Counter = self.__rebrandly_meter.get_metric('invocations')
|
|
79
|
-
self.successful_invocations: Counter = self.__rebrandly_meter.get_metric('successful_invocations')
|
|
80
|
-
self.error_invocations: Counter = self.__rebrandly_meter.get_metric('error_invocations')
|
|
81
|
-
self.duration: Histogram = self.__rebrandly_meter.get_metric('duration')
|
|
82
55
|
self.cpu_usage_percentage: Gauge = self.__rebrandly_meter.get_metric('cpu_usage_percentage')
|
|
83
56
|
self.memory_usage_bytes: Gauge = self.__rebrandly_meter.get_metric('memory_usage_bytes')
|
|
84
57
|
|
rebrandly_otel/otel_utils.py
CHANGED
|
@@ -25,6 +25,17 @@ def create_resource(name: str = None, version: str = None) -> Resource:
|
|
|
25
25
|
)
|
|
26
26
|
return resource
|
|
27
27
|
|
|
28
|
+
def get_package_version():
|
|
29
|
+
try:
|
|
30
|
+
from importlib.metadata import version, PackageNotFoundError # Python 3.8+
|
|
31
|
+
return version('rebrandly_otel')
|
|
32
|
+
except ImportError:
|
|
33
|
+
try:
|
|
34
|
+
from importlib_metadata import version, PackageNotFoundError
|
|
35
|
+
return version('rebrandly_otel')
|
|
36
|
+
except:
|
|
37
|
+
return '?.0.1'
|
|
38
|
+
|
|
28
39
|
|
|
29
40
|
def get_service_name(service_name: str = None) -> str:
|
|
30
41
|
if service_name is None:
|
|
@@ -34,7 +45,7 @@ def get_service_name(service_name: str = None) -> str:
|
|
|
34
45
|
|
|
35
46
|
def get_service_version(service_version: str = None) -> str:
|
|
36
47
|
if service_version is None:
|
|
37
|
-
return os.environ.get('OTEL_SERVICE_VERSION',
|
|
48
|
+
return os.environ.get('OTEL_SERVICE_VERSION', get_package_version())
|
|
38
49
|
return service_version
|
|
39
50
|
|
|
40
51
|
|
rebrandly_otel/rebrandly_otel.py
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Rebrandly OpenTelemetry SDK - Simplified instrumentation for Rebrandly services.
|
|
4
|
-
Updated for consistency with Node.js SDK
|
|
5
|
-
"""
|
|
1
|
+
|
|
2
|
+
import json
|
|
6
3
|
import time
|
|
7
4
|
import psutil
|
|
8
5
|
import functools
|
|
@@ -163,8 +160,6 @@ class RebrandlyOTEL:
|
|
|
163
160
|
|
|
164
161
|
result = None
|
|
165
162
|
try:
|
|
166
|
-
# Increment invocation counter with standardized metric name
|
|
167
|
-
self.meter.GlobalMetrics.invocations.add(1, {'function': span_name})
|
|
168
163
|
|
|
169
164
|
# Create and execute within span
|
|
170
165
|
with self.tracer.start_span(
|
|
@@ -207,17 +202,9 @@ class RebrandlyOTEL:
|
|
|
207
202
|
complete_event_attrs['success'] = success
|
|
208
203
|
span.add_event("lambda.invocation.complete", complete_event_attrs)
|
|
209
204
|
|
|
210
|
-
# Increment success counter with standardized metric
|
|
211
|
-
self.meter.GlobalMetrics.successful_invocations.add(1, {'function': span_name})
|
|
212
|
-
|
|
213
205
|
return result
|
|
214
206
|
|
|
215
207
|
except Exception as e:
|
|
216
|
-
# Increment error counter with standardized metric
|
|
217
|
-
self.meter.GlobalMetrics.error_invocations.add(1, {
|
|
218
|
-
'function': span_name,
|
|
219
|
-
'error': type(e).__name__
|
|
220
|
-
})
|
|
221
208
|
|
|
222
209
|
# Add failed completion event with error attribute (THIS IS THE KEY ADDITION)
|
|
223
210
|
span.add_event("lambda.invocation.complete", {
|
|
@@ -239,13 +226,9 @@ class RebrandlyOTEL:
|
|
|
239
226
|
from opentelemetry import context as otel_context
|
|
240
227
|
otel_context.detach(token)
|
|
241
228
|
|
|
242
|
-
# Record duration with standardized metric
|
|
243
|
-
duration = (datetime.now() - start_time).total_seconds() * 1000
|
|
244
|
-
self.meter.GlobalMetrics.duration.record(duration, {'function': span_name})
|
|
245
|
-
|
|
246
229
|
# Force flush if enabled
|
|
247
230
|
if auto_flush:
|
|
248
|
-
self.logger.logger.info(f"[Rebrandly OTEL] Lambda '{span_name}'
|
|
231
|
+
self.logger.logger.info(f"[Rebrandly OTEL] Lambda '{span_name}', flushing...")
|
|
249
232
|
flush_success = self.force_flush(timeout_millis=1000)
|
|
250
233
|
if not flush_success:
|
|
251
234
|
self.logger.logger.warning("[Rebrandly OTEL] Force flush may not have completed fully")
|
|
@@ -275,9 +258,6 @@ class RebrandlyOTEL:
|
|
|
275
258
|
|
|
276
259
|
result = None
|
|
277
260
|
try:
|
|
278
|
-
# Increment invocations counter with standardized metric
|
|
279
|
-
self.meter.GlobalMetrics.invocations.add(1, {'handler': span_name})
|
|
280
|
-
|
|
281
261
|
# Create span and execute function
|
|
282
262
|
span_function = self.span
|
|
283
263
|
if record is not None and ('MessageAttributes' in record or 'messageAttributes' in record):
|
|
@@ -321,18 +301,9 @@ class RebrandlyOTEL:
|
|
|
321
301
|
complete_event_attrs['success'] = success
|
|
322
302
|
span_context.add_event("message.processing.complete", complete_event_attrs)
|
|
323
303
|
|
|
324
|
-
# Increment success counter with standardized metric
|
|
325
|
-
self.meter.GlobalMetrics.successful_invocations.add(1, {'handler': span_name})
|
|
326
|
-
|
|
327
304
|
return result
|
|
328
305
|
|
|
329
306
|
except Exception as e:
|
|
330
|
-
# Increment error counter with standardized metric
|
|
331
|
-
self.meter.GlobalMetrics.error_invocations.add(1, {
|
|
332
|
-
'handler': span_name,
|
|
333
|
-
'error': type(e).__name__
|
|
334
|
-
})
|
|
335
|
-
|
|
336
307
|
# Record the exception in the span
|
|
337
308
|
if 'span_context' in locals():
|
|
338
309
|
span_context.record_exception(e)
|
|
@@ -348,10 +319,6 @@ class RebrandlyOTEL:
|
|
|
348
319
|
raise
|
|
349
320
|
|
|
350
321
|
finally:
|
|
351
|
-
# Record duration with standardized metric
|
|
352
|
-
duration = (datetime.now() - start_func).total_seconds() * 1000
|
|
353
|
-
self.meter.GlobalMetrics.duration.record(duration, {'handler': span_name})
|
|
354
|
-
|
|
355
322
|
if auto_flush:
|
|
356
323
|
self.force_flush(start_datetime=start_func)
|
|
357
324
|
|
|
@@ -374,16 +341,14 @@ class RebrandlyOTEL:
|
|
|
374
341
|
|
|
375
342
|
if start_datetime is not None:
|
|
376
343
|
end_func = datetime.now()
|
|
377
|
-
duration = (end_func - start_datetime).total_seconds() * 1000
|
|
378
344
|
cpu_percent = psutil.cpu_percent(interval=0.1) # Shorter interval for Lambda
|
|
379
345
|
memory = psutil.virtual_memory()
|
|
380
346
|
|
|
381
347
|
# Record metrics using standardized names
|
|
382
|
-
self.meter.GlobalMetrics.duration.record(duration, {'source': 'force_flush'})
|
|
383
348
|
self.meter.GlobalMetrics.memory_usage_bytes.set(memory.used)
|
|
384
349
|
self.meter.GlobalMetrics.cpu_usage_percentage.set(cpu_percent)
|
|
385
350
|
|
|
386
|
-
print(f"Function
|
|
351
|
+
print(f"Function Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
|
|
387
352
|
|
|
388
353
|
try:
|
|
389
354
|
# Flush traces
|
|
@@ -550,4 +515,4 @@ extract_context = otel.extract_context
|
|
|
550
515
|
attach_context = otel.attach_context
|
|
551
516
|
detach_context = otel.detach_context
|
|
552
517
|
force_flush = otel.force_flush
|
|
553
|
-
shutdown = otel.shutdown
|
|
518
|
+
shutdown = otel.shutdown
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rebrandly_otel
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.18
|
|
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,21 +102,23 @@ 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
108
|
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
### Custom Metrics
|
|
110
|
+
|
|
111
|
+
You can create the custom metrics you need using the default open telemetry metrics
|
|
116
112
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
113
|
+
```python
|
|
114
|
+
from src.rebrandly_otel import meter
|
|
115
|
+
|
|
116
|
+
sqs_counter = meter.meter.create_counter(
|
|
117
|
+
name="sqs_sender_counter",
|
|
118
|
+
description="Number of messages sent",
|
|
119
|
+
unit="1"
|
|
120
|
+
)
|
|
121
|
+
sqs_counter.add(1)
|
|
120
122
|
```
|
|
121
123
|
|
|
122
124
|
## Tracing Features
|
|
@@ -180,10 +182,8 @@ Automatically detects and labels Lambda triggers:
|
|
|
180
182
|
### Automatic Metrics
|
|
181
183
|
|
|
182
184
|
For Lambda functions, the SDK automatically captures:
|
|
183
|
-
- Function duration
|
|
184
185
|
- Memory usage
|
|
185
186
|
- CPU utilization
|
|
186
|
-
- Invocation counts by status
|
|
187
187
|
|
|
188
188
|
### Context Extraction
|
|
189
189
|
|
|
@@ -319,6 +319,47 @@ def process_message(record):
|
|
|
319
319
|
logger.info(f"Message data: {body}")
|
|
320
320
|
```
|
|
321
321
|
|
|
322
|
+
###
|
|
323
|
+
Flask
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
|
|
327
|
+
from flask import Flask, jsonify
|
|
328
|
+
from src.rebrandly_otel import otel, logger, app_before_request, app_after_request, flask_error_handler
|
|
329
|
+
from datetime import datetime
|
|
330
|
+
|
|
331
|
+
app = Flask(__name__)
|
|
332
|
+
|
|
333
|
+
# Register the centralized OTEL handlers
|
|
334
|
+
app.before_request(app_before_request)
|
|
335
|
+
app.after_request(app_after_request)
|
|
336
|
+
app.register_error_handler(Exception, flask_error_handler)
|
|
337
|
+
|
|
338
|
+
@app.route('/health')
|
|
339
|
+
def health():
|
|
340
|
+
logger.info("Health check requested")
|
|
341
|
+
return jsonify({"status": "healthy"}), 200
|
|
342
|
+
|
|
343
|
+
@app.route('/process', methods=['POST', 'GET'])
|
|
344
|
+
def process():
|
|
345
|
+
with otel.span("process_request"):
|
|
346
|
+
logger.info("Processing POST request")
|
|
347
|
+
|
|
348
|
+
# Simulate processing
|
|
349
|
+
result = {"processed": True, "timestamp": datetime.now().isoformat()}
|
|
350
|
+
|
|
351
|
+
logger.info(f"Returning result: {result}")
|
|
352
|
+
return jsonify(result), 200
|
|
353
|
+
|
|
354
|
+
@app.route('/error')
|
|
355
|
+
def error():
|
|
356
|
+
logger.error("Error endpoint called")
|
|
357
|
+
raise Exception("Simulated error")
|
|
358
|
+
|
|
359
|
+
if __name__ == '__main__':
|
|
360
|
+
app.run(debug=True)
|
|
361
|
+
```
|
|
362
|
+
|
|
322
363
|
### More examples
|
|
323
364
|
You can find More examples [here](examples)
|
|
324
365
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
rebrandly_otel/__init__.py,sha256=tkZQJo5hR4FJ4dIRc-3b_YGxGo-uq-DsiSz8shdac-k,397
|
|
2
|
+
rebrandly_otel/fastapi_support.py,sha256=RuBBZJuzr3osBDrkHZ0oQPV70pmvnqxTfBBDVFBFQlo,8019
|
|
3
|
+
rebrandly_otel/flask_support.py,sha256=cUVMGTjN6N8xZD4Zyng2LRWhNj62C5nmTH91SnYBp2A,6072
|
|
4
|
+
rebrandly_otel/logs.py,sha256=92jaxzI5hCHnKHu3lsSAa7K_SPHQgL46AlQUESsYNds,3724
|
|
5
|
+
rebrandly_otel/metrics.py,sha256=8Mgz_VcgsGQaBeYH2y6FtLGSqMqTwa2ilAa1pbYYymU,7472
|
|
6
|
+
rebrandly_otel/otel_utils.py,sha256=tKelaETEeZxxvddKDpY8ESsGS77CcBbQku4oQjmiJx0,2078
|
|
7
|
+
rebrandly_otel/rebrandly_otel.py,sha256=Es6ZQC2hr6qJ_M4Y-7nUvcAnGADj6-ofKn8hKFw7P1k,21166
|
|
8
|
+
rebrandly_otel/traces.py,sha256=v582WtJv3t4Bn92vlDyZouibHtgWNxdRo_XmQCmSOEA,7126
|
|
9
|
+
rebrandly_otel-0.1.18.dist-info/licenses/LICENSE,sha256=KMXHvTwP62S2q-SG7CFfMU_09rUwxiqlM0izaYGdcgY,1103
|
|
10
|
+
rebrandly_otel-0.1.18.dist-info/METADATA,sha256=CyoVBpLByY8eqh5C7UTmKTBH62thbZww1GxPjYKSV6Y,10410
|
|
11
|
+
rebrandly_otel-0.1.18.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
12
|
+
rebrandly_otel-0.1.18.dist-info/top_level.txt,sha256=26PSC1gjVUl8tTH5QfKbFevjVV4E2yojoukEfiTScvM,15
|
|
13
|
+
rebrandly_otel-0.1.18.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
rebrandly_otel/__init__.py,sha256=NUlroPhnHAKVC_q2BiyhtURXw23w1YY1BJd45gtc9qM,275
|
|
2
|
-
rebrandly_otel/logs.py,sha256=92jaxzI5hCHnKHu3lsSAa7K_SPHQgL46AlQUESsYNds,3724
|
|
3
|
-
rebrandly_otel/metrics.py,sha256=57jwrn8e1u66BOO7fcFrmtE3Rpt4VDixG9I78K-zrUU,8537
|
|
4
|
-
rebrandly_otel/otel_utils.py,sha256=uJmfz2NspSnTVJXGKoaLUl1CYb0ow6VHKjmg2d_rdwg,1704
|
|
5
|
-
rebrandly_otel/rebrandly_otel.py,sha256=A_TGyCQq752EeEvcsMmBWaWx2Z12J1hfmyYTFpYJFbo,23232
|
|
6
|
-
rebrandly_otel/traces.py,sha256=v582WtJv3t4Bn92vlDyZouibHtgWNxdRo_XmQCmSOEA,7126
|
|
7
|
-
rebrandly_otel-0.1.16.dist-info/licenses/LICENSE,sha256=KMXHvTwP62S2q-SG7CFfMU_09rUwxiqlM0izaYGdcgY,1103
|
|
8
|
-
rebrandly_otel-0.1.16.dist-info/METADATA,sha256=4SxmMqhoIM6EyjZc8_JWCvHHxATQvuXcKztv5gEqc18,9628
|
|
9
|
-
rebrandly_otel-0.1.16.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
rebrandly_otel-0.1.16.dist-info/top_level.txt,sha256=26PSC1gjVUl8tTH5QfKbFevjVV4E2yojoukEfiTScvM,15
|
|
11
|
-
rebrandly_otel-0.1.16.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|