rebrandly-otel 0.3.1__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.
- rebrandly_otel/__init__.py +32 -0
- rebrandly_otel/api_gateway_utils.py +230 -0
- rebrandly_otel/fastapi_support.py +278 -0
- rebrandly_otel/flask_support.py +222 -0
- rebrandly_otel/http_constants.py +38 -0
- rebrandly_otel/http_utils.py +505 -0
- rebrandly_otel/logs.py +154 -0
- rebrandly_otel/metrics.py +212 -0
- rebrandly_otel/otel_utils.py +169 -0
- rebrandly_otel/pymysql_instrumentation.py +219 -0
- rebrandly_otel/rebrandly_otel.py +614 -0
- rebrandly_otel/span_attributes_processor.py +106 -0
- rebrandly_otel/traces.py +198 -0
- rebrandly_otel-0.3.1.dist-info/METADATA +1926 -0
- rebrandly_otel-0.3.1.dist-info/RECORD +18 -0
- rebrandly_otel-0.3.1.dist-info/WHEEL +5 -0
- rebrandly_otel-0.3.1.dist-info/licenses/LICENSE +19 -0
- rebrandly_otel-0.3.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
# flask_integration.py
|
|
2
|
+
"""Flask integration for Rebrandly OTEL SDK."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from opentelemetry.trace import Status, StatusCode, SpanKind
|
|
6
|
+
from .http_utils import filter_important_headers, capture_request_body, auto_detect_route_pattern
|
|
7
|
+
from .http_constants import (
|
|
8
|
+
HTTP_REQUEST_METHOD,
|
|
9
|
+
HTTP_REQUEST_HEADERS,
|
|
10
|
+
HTTP_REQUEST_HEADER_TRACEPARENT,
|
|
11
|
+
HTTP_REQUEST_BODY,
|
|
12
|
+
HTTP_RESPONSE_STATUS_CODE,
|
|
13
|
+
HTTP_ROUTE,
|
|
14
|
+
URL_FULL,
|
|
15
|
+
URL_SCHEME,
|
|
16
|
+
URL_PATH,
|
|
17
|
+
URL_QUERY,
|
|
18
|
+
USER_AGENT_ORIGINAL,
|
|
19
|
+
NETWORK_PROTOCOL_VERSION,
|
|
20
|
+
SERVER_ADDRESS,
|
|
21
|
+
SERVER_PORT,
|
|
22
|
+
CLIENT_ADDRESS,
|
|
23
|
+
ERROR_TYPE
|
|
24
|
+
)
|
|
25
|
+
from .api_gateway_utils import extract_api_gateway_authorizer_attributes
|
|
26
|
+
|
|
27
|
+
from flask import request, jsonify, g
|
|
28
|
+
from werkzeug.exceptions import HTTPException
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def setup_flask(otel, app):
|
|
32
|
+
"""
|
|
33
|
+
Setup Flask application with OTEL instrumentation.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
from flask import Flask
|
|
37
|
+
from rebrandly_otel import otel
|
|
38
|
+
from rebrandly_otel.flask_integration import setup_flask
|
|
39
|
+
|
|
40
|
+
app = Flask(__name__)
|
|
41
|
+
setup_flask(otel, app)
|
|
42
|
+
"""
|
|
43
|
+
app.before_request(lambda: app_before_request(otel))
|
|
44
|
+
app.after_request(lambda response: app_after_request(otel, response))
|
|
45
|
+
app.register_error_handler(Exception, lambda e: flask_error_handler(otel, e))
|
|
46
|
+
return app
|
|
47
|
+
|
|
48
|
+
def app_before_request(otel):
|
|
49
|
+
"""
|
|
50
|
+
Setup tracing for incoming Flask request.
|
|
51
|
+
To be used with Flask's before_request hook.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
# Extract trace context from headers
|
|
55
|
+
headers = dict(request.headers)
|
|
56
|
+
token = otel.attach_context(headers)
|
|
57
|
+
request.trace_token = token
|
|
58
|
+
|
|
59
|
+
# Initial span name - will be updated with route pattern in after_request
|
|
60
|
+
# Using just method initially since route is not available before routing completes
|
|
61
|
+
span_name = f"{request.method}"
|
|
62
|
+
|
|
63
|
+
# Filter headers to keep only important ones
|
|
64
|
+
filtered_headers = filter_important_headers(headers)
|
|
65
|
+
|
|
66
|
+
# Capture request body if available (before span creation)
|
|
67
|
+
request_body = None
|
|
68
|
+
try:
|
|
69
|
+
content_type = request.headers.get('Content-Type', '')
|
|
70
|
+
# Try to get JSON body first, fallback to raw data
|
|
71
|
+
body = request.get_json(silent=True) or request.data
|
|
72
|
+
request_body = capture_request_body(body, content_type)
|
|
73
|
+
except Exception:
|
|
74
|
+
# Silently skip body capture if it fails
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
# Build attributes dict, excluding None values
|
|
78
|
+
# Note: HTTP_ROUTE will be set in after_request when route pattern is available
|
|
79
|
+
attributes = {
|
|
80
|
+
# Required HTTP attributes per semantic conventions
|
|
81
|
+
HTTP_REQUEST_METHOD: request.method,
|
|
82
|
+
HTTP_REQUEST_HEADERS: json.dumps(filtered_headers, default=str),
|
|
83
|
+
HTTP_REQUEST_HEADER_TRACEPARENT: headers.get('traceparent'),
|
|
84
|
+
HTTP_REQUEST_BODY: request_body,
|
|
85
|
+
URL_FULL: request.url,
|
|
86
|
+
URL_SCHEME: request.scheme,
|
|
87
|
+
URL_PATH: request.path,
|
|
88
|
+
# HTTP_ROUTE will be set in after_request after routing completes
|
|
89
|
+
NETWORK_PROTOCOL_VERSION: request.environ.get('SERVER_PROTOCOL', 'HTTP/1.1').split('/')[-1],
|
|
90
|
+
SERVER_ADDRESS: request.host.split(':')[0],
|
|
91
|
+
SERVER_PORT: request.host.split(':')[1] if ':' in request.host else (443 if request.scheme == 'https' else 80),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Add optional attributes only if they have values
|
|
95
|
+
if request.query_string:
|
|
96
|
+
attributes[URL_QUERY] = request.query_string.decode('utf-8')
|
|
97
|
+
|
|
98
|
+
if request.user_agent and request.user_agent.string:
|
|
99
|
+
attributes[USER_AGENT_ORIGINAL] = request.user_agent.string
|
|
100
|
+
|
|
101
|
+
if request.remote_addr:
|
|
102
|
+
attributes[CLIENT_ADDRESS] = request.remote_addr
|
|
103
|
+
|
|
104
|
+
# Extract authorizer context if available (when running behind API Gateway via Lambda)
|
|
105
|
+
# The Lambda handler should store the event in flask.g.lambda_event
|
|
106
|
+
if hasattr(g, 'lambda_event') and g.lambda_event:
|
|
107
|
+
authorizer_attrs = extract_api_gateway_authorizer_attributes(g.lambda_event)
|
|
108
|
+
attributes.update(authorizer_attrs)
|
|
109
|
+
|
|
110
|
+
# Start span for request using start_as_current_span to make it the active span
|
|
111
|
+
span = otel.tracer.tracer.start_as_current_span(
|
|
112
|
+
span_name,
|
|
113
|
+
attributes=attributes,
|
|
114
|
+
kind=SpanKind.SERVER
|
|
115
|
+
)
|
|
116
|
+
# Store both the span context manager and the span itself
|
|
117
|
+
request.span_context = span
|
|
118
|
+
request.span = span.__enter__() # This activates the span and returns the span object
|
|
119
|
+
|
|
120
|
+
# Log request start
|
|
121
|
+
otel.logger.logger.info(f"Request started: {request.method} {request.path}",
|
|
122
|
+
extra={"http.method": request.method, "http.path": request.path})
|
|
123
|
+
|
|
124
|
+
def app_after_request(otel, response):
|
|
125
|
+
"""
|
|
126
|
+
Cleanup tracing after Flask request completes.
|
|
127
|
+
To be used with Flask's after_request hook.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
# Check if we have a span and it's still recording
|
|
131
|
+
if hasattr(request, 'span') and request.span.is_recording():
|
|
132
|
+
# Extract route template from Flask's url_rule (available after routing)
|
|
133
|
+
# This is critical for OpenTelemetry compliance - http.route must be low cardinality
|
|
134
|
+
route_pattern = None
|
|
135
|
+
if hasattr(request, 'url_rule') and request.url_rule is not None:
|
|
136
|
+
# Flask's url_rule.rule contains the route template (e.g., '/users/<user_id>')
|
|
137
|
+
route_pattern = request.url_rule.rule
|
|
138
|
+
else:
|
|
139
|
+
# Fallback: auto-detect pattern from path using heuristics
|
|
140
|
+
route_pattern = auto_detect_route_pattern(request.path)
|
|
141
|
+
|
|
142
|
+
# Update span name with route pattern (REQUIRED by OTEL spec)
|
|
143
|
+
# Per spec: span name should be "{method} {http.route}" for low cardinality
|
|
144
|
+
if route_pattern:
|
|
145
|
+
request.span.update_name(f"{request.method} {route_pattern}")
|
|
146
|
+
request.span.set_attribute(HTTP_ROUTE, route_pattern)
|
|
147
|
+
|
|
148
|
+
# Update response status code
|
|
149
|
+
request.span.set_attribute(HTTP_RESPONSE_STATUS_CODE, response.status_code)
|
|
150
|
+
|
|
151
|
+
# Set span status based on HTTP status code following OpenTelemetry semantic conventions
|
|
152
|
+
# For SERVER spans: only 5xx codes are marked as ERROR, all others left UNSET
|
|
153
|
+
# Per spec: https://opentelemetry.io/docs/specs/semconv/http/http-spans/
|
|
154
|
+
if response.status_code >= 500:
|
|
155
|
+
request.span.set_status(Status(StatusCode.ERROR))
|
|
156
|
+
# For all other codes (1xx, 2xx, 3xx, 4xx), leave status unset
|
|
157
|
+
|
|
158
|
+
# Properly close the span context manager
|
|
159
|
+
if hasattr(request, 'span_context'):
|
|
160
|
+
request.span_context.__exit__(None, None, None)
|
|
161
|
+
else:
|
|
162
|
+
# Fallback if we don't have the context manager
|
|
163
|
+
request.span.end()
|
|
164
|
+
|
|
165
|
+
# Detach context
|
|
166
|
+
if hasattr(request, 'trace_token'):
|
|
167
|
+
otel.detach_context(request.trace_token)
|
|
168
|
+
|
|
169
|
+
# Log request completion
|
|
170
|
+
otel.logger.logger.info(f"Request completed: {response.status_code}",
|
|
171
|
+
extra={"http.response.status_code": response.status_code})
|
|
172
|
+
|
|
173
|
+
otel.force_flush(timeout_millis=100)
|
|
174
|
+
return response
|
|
175
|
+
|
|
176
|
+
def flask_error_handler(otel, exception):
|
|
177
|
+
"""
|
|
178
|
+
Handle Flask exceptions and record them in the current span.
|
|
179
|
+
To be used with Flask's errorhandler decorator.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
# Determine the status code
|
|
183
|
+
if isinstance(exception, HTTPException):
|
|
184
|
+
status_code = exception.code
|
|
185
|
+
elif hasattr(exception, 'status_code'):
|
|
186
|
+
status_code = exception.status_code
|
|
187
|
+
elif hasattr(exception, 'code'):
|
|
188
|
+
status_code = exception.code if isinstance(exception.code, int) else 500
|
|
189
|
+
else:
|
|
190
|
+
status_code = 500
|
|
191
|
+
|
|
192
|
+
# Record exception in span if available and still recording
|
|
193
|
+
if hasattr(request, 'span') and request.span.is_recording():
|
|
194
|
+
request.span.set_attribute(HTTP_RESPONSE_STATUS_CODE, status_code)
|
|
195
|
+
request.span.set_attribute(ERROR_TYPE, type(exception).__name__)
|
|
196
|
+
|
|
197
|
+
request.span.record_exception(exception)
|
|
198
|
+
request.span.set_status(Status(StatusCode.ERROR, str(exception)))
|
|
199
|
+
request.span.add_event("exception", {
|
|
200
|
+
"exception.type": type(exception).__name__,
|
|
201
|
+
"exception.message": str(exception)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
# Only close the span if it's still recording (not already ended)
|
|
205
|
+
if hasattr(request, 'span_context'):
|
|
206
|
+
request.span_context.__exit__(type(exception), exception, None)
|
|
207
|
+
else:
|
|
208
|
+
request.span.end()
|
|
209
|
+
|
|
210
|
+
# Log the error with status code
|
|
211
|
+
otel.logger.logger.error(f"Unhandled exception: {exception} (status: {status_code})",
|
|
212
|
+
exc_info=True,
|
|
213
|
+
extra={
|
|
214
|
+
"exception.type": type(exception).__name__,
|
|
215
|
+
"http.response.status_code": status_code
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
# Return error response with the determined status code
|
|
219
|
+
return jsonify({
|
|
220
|
+
"error": str(exception),
|
|
221
|
+
"type": type(exception).__name__
|
|
222
|
+
}), status_code
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""HTTP semantic attribute constants for OpenTelemetry instrumentation."""
|
|
2
|
+
|
|
3
|
+
# HTTP Semantic Attributes following OpenTelemetry conventions
|
|
4
|
+
# https://opentelemetry.io/docs/specs/semconv/http/http-spans/
|
|
5
|
+
|
|
6
|
+
HTTP_REQUEST_METHOD = "http.request.method"
|
|
7
|
+
HTTP_REQUEST_HEADERS = "http.request.headers"
|
|
8
|
+
HTTP_REQUEST_HEADER_TRACEPARENT = "http.request.header.traceparent"
|
|
9
|
+
HTTP_REQUEST_BODY = "http.request.body.content"
|
|
10
|
+
HTTP_RESPONSE_STATUS_CODE = "http.response.status_code"
|
|
11
|
+
HTTP_ROUTE = "http.route"
|
|
12
|
+
|
|
13
|
+
# URL attributes
|
|
14
|
+
URL_FULL = "url.full"
|
|
15
|
+
URL_SCHEME = "url.scheme"
|
|
16
|
+
URL_PATH = "url.path"
|
|
17
|
+
URL_QUERY = "url.query"
|
|
18
|
+
|
|
19
|
+
# Network attributes
|
|
20
|
+
NETWORK_PROTOCOL_VERSION = "network.protocol.version"
|
|
21
|
+
|
|
22
|
+
# Server attributes
|
|
23
|
+
SERVER_ADDRESS = "server.address"
|
|
24
|
+
SERVER_PORT = "server.port"
|
|
25
|
+
|
|
26
|
+
# Client attributes
|
|
27
|
+
CLIENT_ADDRESS = "client.address"
|
|
28
|
+
|
|
29
|
+
# User agent
|
|
30
|
+
USER_AGENT_ORIGINAL = "user_agent.original"
|
|
31
|
+
|
|
32
|
+
# Error attributes
|
|
33
|
+
ERROR_TYPE = "error.type"
|
|
34
|
+
|
|
35
|
+
# User and organization attributes (Rebrandly-specific)
|
|
36
|
+
USER_ID = "user.id"
|
|
37
|
+
REBRANDLY_WORKSPACE = "rebrandly.workspace"
|
|
38
|
+
REBRANDLY_ORGANIZATION = "rebrandly.organization"
|