rebrandly-otel 0.1.14__py3-none-any.whl → 0.1.17__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 +3 -1
- rebrandly_otel/flask_support.py +170 -0
- rebrandly_otel/otel_utils.py +12 -1
- rebrandly_otel/rebrandly_otel.py +108 -81
- {rebrandly_otel-0.1.14.dist-info → rebrandly_otel-0.1.17.dist-info}/METADATA +57 -1
- rebrandly_otel-0.1.17.dist-info/RECORD +12 -0
- rebrandly_otel-0.1.14.dist-info/RECORD +0 -11
- {rebrandly_otel-0.1.14.dist-info → rebrandly_otel-0.1.17.dist-info}/WHEEL +0 -0
- {rebrandly_otel-0.1.14.dist-info → rebrandly_otel-0.1.17.dist-info}/licenses/LICENSE +0 -0
- {rebrandly_otel-0.1.14.dist-info → rebrandly_otel-0.1.17.dist-info}/top_level.txt +0 -0
rebrandly_otel/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# src/__init__.py
|
|
2
2
|
from .rebrandly_otel import *
|
|
3
|
+
from .flask_support import *
|
|
3
4
|
|
|
4
5
|
# Explicitly define what's available
|
|
5
6
|
__all__ = [
|
|
@@ -12,5 +13,6 @@ __all__ = [
|
|
|
12
13
|
'logger',
|
|
13
14
|
'force_flush',
|
|
14
15
|
'aws_message_handler',
|
|
15
|
-
'shutdown'
|
|
16
|
+
'shutdown',
|
|
17
|
+
'setup_flask'
|
|
16
18
|
]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# flask_integration.py
|
|
2
|
+
"""Flask integration for Rebrandly OTEL SDK."""
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
from opentelemetry.trace import Status, StatusCode, SpanKind
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from .rebrandly_otel import RebrandlyOTEL
|
|
8
|
+
|
|
9
|
+
def setup_flask(otel: 'RebrandlyOTEL', app):
|
|
10
|
+
"""
|
|
11
|
+
Setup Flask application with OTEL instrumentation.
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
from flask import Flask
|
|
15
|
+
from rebrandly_otel import otel
|
|
16
|
+
from rebrandly_otel.flask_integration import setup_flask
|
|
17
|
+
|
|
18
|
+
app = Flask(__name__)
|
|
19
|
+
setup_flask(otel, app)
|
|
20
|
+
"""
|
|
21
|
+
app.before_request(lambda: app_before_request(otel))
|
|
22
|
+
app.after_request(lambda response: app_after_request(otel, response))
|
|
23
|
+
app.register_error_handler(Exception, lambda e: flask_error_handler(otel, e))
|
|
24
|
+
return app
|
|
25
|
+
|
|
26
|
+
def app_before_request(otel: 'RebrandlyOTEL'):
|
|
27
|
+
"""
|
|
28
|
+
Setup tracing for incoming Flask request.
|
|
29
|
+
To be used with Flask's before_request hook.
|
|
30
|
+
"""
|
|
31
|
+
from flask import request
|
|
32
|
+
|
|
33
|
+
# Extract trace context from headers
|
|
34
|
+
headers = dict(request.headers)
|
|
35
|
+
token = otel.attach_context(headers)
|
|
36
|
+
request.trace_token = token
|
|
37
|
+
|
|
38
|
+
# Start span for request using start_as_current_span to make it the active span
|
|
39
|
+
span = otel.tracer.tracer.start_as_current_span(
|
|
40
|
+
f"{request.method} {request.path}",
|
|
41
|
+
attributes={
|
|
42
|
+
"http.method": request.method,
|
|
43
|
+
"http.url": request.url,
|
|
44
|
+
"http.path": request.path,
|
|
45
|
+
"http.scheme": request.scheme,
|
|
46
|
+
"http.host": request.host,
|
|
47
|
+
"http.user_agent": request.user_agent.string if request.user_agent else '',
|
|
48
|
+
"http.remote_addr": request.remote_addr,
|
|
49
|
+
"http.target": request.path,
|
|
50
|
+
"span.kind": "server"
|
|
51
|
+
},
|
|
52
|
+
kind=SpanKind.SERVER
|
|
53
|
+
)
|
|
54
|
+
# Store both the span context manager and the span itself
|
|
55
|
+
request.span_context = span
|
|
56
|
+
request.span = span.__enter__() # This activates the span and returns the span object
|
|
57
|
+
|
|
58
|
+
# Log request start
|
|
59
|
+
otel.logger.logger.info(f"Request started: {request.method} {request.path}",
|
|
60
|
+
extra={"http.method": request.method, "http.path": request.path})
|
|
61
|
+
|
|
62
|
+
def app_after_request(otel: 'RebrandlyOTEL', response):
|
|
63
|
+
"""
|
|
64
|
+
Cleanup tracing after Flask request completes.
|
|
65
|
+
To be used with Flask's after_request hook.
|
|
66
|
+
"""
|
|
67
|
+
from flask import request
|
|
68
|
+
|
|
69
|
+
# Check if we have a span and it's still recording
|
|
70
|
+
if hasattr(request, 'span') and request.span.is_recording():
|
|
71
|
+
request.span.set_attribute("http.status_code", response.status_code)
|
|
72
|
+
|
|
73
|
+
# Set span status based on HTTP status code
|
|
74
|
+
if response.status_code >= 400:
|
|
75
|
+
request.span.set_status(Status(StatusCode.ERROR, f"HTTP {response.status_code}"))
|
|
76
|
+
# Record error metric
|
|
77
|
+
otel.meter.GlobalMetrics.error_invocations.add(1, {
|
|
78
|
+
"endpoint": request.path,
|
|
79
|
+
"method": request.method,
|
|
80
|
+
"status_code": response.status_code
|
|
81
|
+
})
|
|
82
|
+
else:
|
|
83
|
+
request.span.set_status(Status(StatusCode.OK))
|
|
84
|
+
# Record success metric
|
|
85
|
+
otel.meter.GlobalMetrics.successful_invocations.add(1, {
|
|
86
|
+
"endpoint": request.path,
|
|
87
|
+
"method": request.method
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
# Properly close the span context manager
|
|
91
|
+
if hasattr(request, 'span_context'):
|
|
92
|
+
request.span_context.__exit__(None, None, None)
|
|
93
|
+
else:
|
|
94
|
+
# Fallback if we don't have the context manager
|
|
95
|
+
request.span.end()
|
|
96
|
+
|
|
97
|
+
# Detach context
|
|
98
|
+
if hasattr(request, 'trace_token'):
|
|
99
|
+
otel.detach_context(request.trace_token)
|
|
100
|
+
|
|
101
|
+
# Log request completion
|
|
102
|
+
otel.logger.logger.info(f"Request completed: {response.status_code}",
|
|
103
|
+
extra={"http.status_code": response.status_code})
|
|
104
|
+
|
|
105
|
+
# Record general invocation metric
|
|
106
|
+
otel.meter.GlobalMetrics.invocations.add(1, {
|
|
107
|
+
"endpoint": request.path,
|
|
108
|
+
"method": request.method
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return response
|
|
112
|
+
|
|
113
|
+
def flask_error_handler(otel: 'RebrandlyOTEL', exception):
|
|
114
|
+
"""
|
|
115
|
+
Handle Flask exceptions and record them in the current span.
|
|
116
|
+
To be used with Flask's errorhandler decorator.
|
|
117
|
+
"""
|
|
118
|
+
from flask import request, jsonify
|
|
119
|
+
from werkzeug.exceptions import HTTPException
|
|
120
|
+
|
|
121
|
+
# Determine the status code
|
|
122
|
+
if isinstance(exception, HTTPException):
|
|
123
|
+
status_code = exception.code
|
|
124
|
+
elif hasattr(exception, 'status_code'):
|
|
125
|
+
status_code = exception.status_code
|
|
126
|
+
elif hasattr(exception, 'code'):
|
|
127
|
+
status_code = exception.code if isinstance(exception.code, int) else 500
|
|
128
|
+
else:
|
|
129
|
+
status_code = 500
|
|
130
|
+
|
|
131
|
+
# Record exception in span if available and still recording
|
|
132
|
+
if hasattr(request, 'span') and request.span.is_recording():
|
|
133
|
+
request.span.set_attribute("http.status_code", status_code)
|
|
134
|
+
request.span.set_attribute("error.type", type(exception).__name__)
|
|
135
|
+
|
|
136
|
+
request.span.record_exception(exception)
|
|
137
|
+
request.span.set_status(Status(StatusCode.ERROR, str(exception)))
|
|
138
|
+
request.span.add_event("exception", {
|
|
139
|
+
"exception.type": type(exception).__name__,
|
|
140
|
+
"exception.message": str(exception),
|
|
141
|
+
"http.status_code": status_code
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
# Only close the span if it's still recording (not already ended)
|
|
145
|
+
if hasattr(request, 'span_context'):
|
|
146
|
+
request.span_context.__exit__(type(exception), exception, None)
|
|
147
|
+
else:
|
|
148
|
+
request.span.end()
|
|
149
|
+
|
|
150
|
+
# Log the error with status code
|
|
151
|
+
otel.logger.logger.error(f"Unhandled exception: {exception} (status: {status_code})",
|
|
152
|
+
exc_info=True,
|
|
153
|
+
extra={
|
|
154
|
+
"exception.type": type(exception).__name__,
|
|
155
|
+
"http.status_code": status_code
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
# Record error metric with status code
|
|
159
|
+
otel.meter.GlobalMetrics.error_invocations.add(1, {
|
|
160
|
+
"endpoint": request.path if hasattr(request, 'path') else 'unknown',
|
|
161
|
+
"method": request.method if hasattr(request, 'method') else 'unknown',
|
|
162
|
+
"error": type(exception).__name__,
|
|
163
|
+
"status_code": status_code
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
# Return error response with the determined status code
|
|
167
|
+
return jsonify({
|
|
168
|
+
"error": str(exception),
|
|
169
|
+
"type": type(exception).__name__
|
|
170
|
+
}), status_code
|
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,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Rebrandly OpenTelemetry SDK - Simplified instrumentation for Rebrandly services.
|
|
4
|
-
"""
|
|
1
|
+
|
|
2
|
+
import json
|
|
5
3
|
import time
|
|
6
4
|
import psutil
|
|
7
5
|
import functools
|
|
@@ -99,8 +97,6 @@ class RebrandlyOTEL:
|
|
|
99
97
|
return wrapper
|
|
100
98
|
return decorator
|
|
101
99
|
|
|
102
|
-
# Fix for the lambda_handler method in rebrandly_otel.py
|
|
103
|
-
# Replace the lambda_handler method (around line 132) with this fixed version:
|
|
104
100
|
def lambda_handler(self,
|
|
105
101
|
name: Optional[str] = None,
|
|
106
102
|
attributes: Optional[Dict[str, Any]] = None,
|
|
@@ -132,6 +128,7 @@ class RebrandlyOTEL:
|
|
|
132
128
|
|
|
133
129
|
# Handle context extraction from AWS events
|
|
134
130
|
token = None
|
|
131
|
+
|
|
135
132
|
if not skip_aws_link and event and isinstance(event, dict) and 'Records' in event:
|
|
136
133
|
first_record = event['Records'][0] if event['Records'] else None
|
|
137
134
|
if first_record:
|
|
@@ -160,11 +157,10 @@ class RebrandlyOTEL:
|
|
|
160
157
|
from opentelemetry import propagate, context as otel_context
|
|
161
158
|
extracted_context = propagate.extract(carrier)
|
|
162
159
|
token = otel_context.attach(extracted_context)
|
|
163
|
-
span_attributes['message.has_trace_context'] = True
|
|
164
160
|
|
|
165
161
|
result = None
|
|
166
162
|
try:
|
|
167
|
-
# Increment invocation counter
|
|
163
|
+
# Increment invocation counter with standardized metric name
|
|
168
164
|
self.meter.GlobalMetrics.invocations.add(1, {'function': span_name})
|
|
169
165
|
|
|
170
166
|
# Create and execute within span
|
|
@@ -173,26 +169,63 @@ class RebrandlyOTEL:
|
|
|
173
169
|
attributes=span_attributes,
|
|
174
170
|
kind=kind
|
|
175
171
|
) as span:
|
|
176
|
-
# Add invocation start event
|
|
177
|
-
|
|
172
|
+
# Add invocation start event with standardized attributes
|
|
173
|
+
start_event_attrs = {
|
|
178
174
|
'event.type': type(event).__name__ if event else 'None'
|
|
179
|
-
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
# Add records count if present
|
|
178
|
+
if event and isinstance(event, dict) and 'Records' in event:
|
|
179
|
+
start_event_attrs['event.records'] = f"{len(event['Records'])}"
|
|
180
|
+
|
|
181
|
+
span.add_event("lambda.invocation.start", start_event_attrs)
|
|
180
182
|
|
|
181
183
|
# Execute handler
|
|
182
184
|
result = func(event, lambda_context)
|
|
183
185
|
|
|
184
|
-
# Process result
|
|
185
|
-
|
|
186
|
+
# Process result with standardized attributes
|
|
187
|
+
success = True
|
|
188
|
+
complete_event_attrs = {}
|
|
189
|
+
|
|
190
|
+
if isinstance(result, dict) and 'statusCode' in result:
|
|
191
|
+
span.set_attribute("http.status_code", result['statusCode'])
|
|
192
|
+
complete_event_attrs['status_code'] = result['statusCode']
|
|
193
|
+
|
|
194
|
+
# Set span status based on HTTP status code
|
|
195
|
+
if result['statusCode'] >= 400:
|
|
196
|
+
success = False
|
|
197
|
+
span.set_status(Status(StatusCode.ERROR, f"HTTP {result['statusCode']}"))
|
|
198
|
+
else:
|
|
199
|
+
span.set_status(Status(StatusCode.OK))
|
|
200
|
+
else:
|
|
201
|
+
span.set_status(Status(StatusCode.OK))
|
|
202
|
+
|
|
203
|
+
# Add completion event with success indicator
|
|
204
|
+
complete_event_attrs['success'] = success
|
|
205
|
+
span.add_event("lambda.invocation.complete", complete_event_attrs)
|
|
206
|
+
|
|
207
|
+
# Increment success counter with standardized metric
|
|
208
|
+
self.meter.GlobalMetrics.successful_invocations.add(1, {'function': span_name})
|
|
186
209
|
|
|
187
210
|
return result
|
|
188
211
|
|
|
189
212
|
except Exception as e:
|
|
190
|
-
# Increment error counter
|
|
213
|
+
# Increment error counter with standardized metric
|
|
191
214
|
self.meter.GlobalMetrics.error_invocations.add(1, {
|
|
192
215
|
'function': span_name,
|
|
193
216
|
'error': type(e).__name__
|
|
194
217
|
})
|
|
195
218
|
|
|
219
|
+
# Add failed completion event with error attribute (THIS IS THE KEY ADDITION)
|
|
220
|
+
span.add_event("lambda.invocation.complete", {
|
|
221
|
+
'success': False,
|
|
222
|
+
'error': type(e).__name__
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
# Record the exception in the span
|
|
226
|
+
span.record_exception(e)
|
|
227
|
+
span.set_status(Status(StatusCode.ERROR, str(e)))
|
|
228
|
+
|
|
196
229
|
# Log error
|
|
197
230
|
self.logger.logger.error(f"Lambda execution failed: {e}", exc_info=True)
|
|
198
231
|
raise
|
|
@@ -203,81 +236,68 @@ class RebrandlyOTEL:
|
|
|
203
236
|
from opentelemetry import context as otel_context
|
|
204
237
|
otel_context.detach(token)
|
|
205
238
|
|
|
206
|
-
# Record duration
|
|
239
|
+
# Record duration with standardized metric
|
|
207
240
|
duration = (datetime.now() - start_time).total_seconds() * 1000
|
|
208
241
|
self.meter.GlobalMetrics.duration.record(duration, {'function': span_name})
|
|
209
242
|
|
|
210
243
|
# Force flush if enabled
|
|
211
244
|
if auto_flush:
|
|
212
|
-
self.logger.logger.info(f"[OTEL] Lambda '{span_name}' completed in {duration:.2f}ms, flushing...")
|
|
245
|
+
self.logger.logger.info(f"[Rebrandly OTEL] Lambda '{span_name}' completed in {duration:.2f}ms, flushing...")
|
|
213
246
|
flush_success = self.force_flush(timeout_millis=1000)
|
|
214
247
|
if not flush_success:
|
|
215
|
-
self.logger.logger.warning("[OTEL] Force flush may not have completed fully")
|
|
248
|
+
self.logger.logger.warning("[Rebrandly OTEL] Force flush may not have completed fully")
|
|
216
249
|
|
|
217
250
|
return wrapper
|
|
218
251
|
return decorator
|
|
219
252
|
|
|
220
|
-
def _process_lambda_result(self, result, span_context, span_name):
|
|
221
|
-
"""Helper method to process Lambda result and update span accordingly"""
|
|
222
|
-
if isinstance(result, dict):
|
|
223
|
-
if 'statusCode' in result:
|
|
224
|
-
span_context.set_attribute("http.status_code", result['statusCode'])
|
|
225
|
-
# Set span status based on HTTP status code
|
|
226
|
-
if result['statusCode'] >= 400:
|
|
227
|
-
span_context.set_status(Status(StatusCode.ERROR, f"HTTP {result['statusCode']}"))
|
|
228
|
-
else:
|
|
229
|
-
span_context.set_status(Status(StatusCode.OK))
|
|
230
|
-
|
|
231
|
-
# Add completion event
|
|
232
|
-
span_context.add_event("lambda.invocation.complete",
|
|
233
|
-
attributes={"success": result.get('statusCode', 200) < 400})
|
|
234
|
-
else:
|
|
235
|
-
span_context.set_status(Status(StatusCode.OK))
|
|
236
|
-
span_context.add_event("lambda.invocation.complete", attributes={"success": True})
|
|
237
|
-
|
|
238
|
-
# Increment success counter
|
|
239
|
-
self.meter.GlobalMetrics.successful_invocations.add(1, {'function': span_name})
|
|
240
|
-
|
|
241
253
|
def aws_message_handler(self,
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
254
|
+
name: Optional[str] = None,
|
|
255
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
256
|
+
kind: SpanKind = SpanKind.CONSUMER,
|
|
257
|
+
auto_flush: bool = True):
|
|
246
258
|
"""
|
|
247
|
-
|
|
259
|
+
Decorator for AWS message handlers (SQS/SNS record processing).
|
|
260
|
+
Requires a record object parameter to the function.
|
|
248
261
|
"""
|
|
249
262
|
def decorator(func):
|
|
250
263
|
@functools.wraps(func)
|
|
251
264
|
def wrapper(record=None, *args, **kwargs):
|
|
252
265
|
# Determine span name
|
|
253
|
-
span_name = name or f"
|
|
266
|
+
span_name = name or f"message.{func.__name__}"
|
|
254
267
|
start_func = datetime.now()
|
|
255
268
|
|
|
256
269
|
# Build span attributes
|
|
257
270
|
span_attributes = attributes or {}
|
|
271
|
+
span_attributes['messaging.operation'] = 'process'
|
|
258
272
|
|
|
259
273
|
result = None
|
|
260
274
|
try:
|
|
261
|
-
# Increment invocations counter
|
|
262
|
-
print('XXX 2')
|
|
275
|
+
# Increment invocations counter with standardized metric
|
|
263
276
|
self.meter.GlobalMetrics.invocations.add(1, {'handler': span_name})
|
|
264
277
|
|
|
265
278
|
# Create span and execute function
|
|
266
279
|
span_function = self.span
|
|
267
|
-
if record is not None and 'MessageAttributes' in record:
|
|
280
|
+
if record is not None and ('MessageAttributes' in record or 'messageAttributes' in record):
|
|
268
281
|
span_function = self.aws_message_span
|
|
269
282
|
|
|
270
283
|
with span_function(span_name, message=record, attributes=span_attributes, kind=kind) as span_context:
|
|
284
|
+
# Add processing start event with standardized name
|
|
285
|
+
span_context.add_event("message.processing.start", {})
|
|
286
|
+
|
|
271
287
|
# Execute the actual handler function
|
|
272
288
|
result = func(record, *args, **kwargs)
|
|
273
289
|
|
|
274
|
-
#
|
|
290
|
+
# Process result
|
|
291
|
+
success = True
|
|
292
|
+
complete_event_attrs = {}
|
|
293
|
+
|
|
275
294
|
if result and isinstance(result, dict):
|
|
276
295
|
if 'statusCode' in result:
|
|
277
|
-
span_context.set_attribute("
|
|
296
|
+
span_context.set_attribute("http.status_code", result['statusCode'])
|
|
278
297
|
|
|
279
298
|
# Set span status based on status code
|
|
280
299
|
if result['statusCode'] >= 400:
|
|
300
|
+
success = False
|
|
281
301
|
span_context.set_status(
|
|
282
302
|
Status(StatusCode.ERROR, f"Handler returned {result['statusCode']}")
|
|
283
303
|
)
|
|
@@ -286,32 +306,49 @@ class RebrandlyOTEL:
|
|
|
286
306
|
|
|
287
307
|
# Add custom result attributes if present
|
|
288
308
|
if 'processed' in result:
|
|
289
|
-
|
|
309
|
+
complete_event_attrs['processed'] = result['processed']
|
|
310
|
+
span_context.set_attribute("message.processed", result['processed'])
|
|
290
311
|
if 'skipped' in result:
|
|
291
|
-
|
|
312
|
+
complete_event_attrs['skipped'] = result['skipped']
|
|
313
|
+
span_context.set_attribute("message.skipped", result['skipped'])
|
|
314
|
+
else:
|
|
315
|
+
span_context.set_status(Status(StatusCode.OK))
|
|
292
316
|
|
|
293
|
-
# Add completion event
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
})
|
|
317
|
+
# Add completion event with standardized name
|
|
318
|
+
complete_event_attrs['success'] = success
|
|
319
|
+
span_context.add_event("message.processing.complete", complete_event_attrs)
|
|
297
320
|
|
|
298
|
-
# Increment success counter
|
|
321
|
+
# Increment success counter with standardized metric
|
|
299
322
|
self.meter.GlobalMetrics.successful_invocations.add(1, {'handler': span_name})
|
|
300
323
|
|
|
301
324
|
return result
|
|
302
325
|
|
|
303
326
|
except Exception as e:
|
|
304
|
-
# Increment error counter
|
|
305
|
-
self.meter.GlobalMetrics.error_invocations.add(1, {
|
|
327
|
+
# Increment error counter with standardized metric
|
|
328
|
+
self.meter.GlobalMetrics.error_invocations.add(1, {
|
|
329
|
+
'handler': span_name,
|
|
330
|
+
'error': type(e).__name__
|
|
331
|
+
})
|
|
306
332
|
|
|
307
333
|
# Record the exception in the span
|
|
308
|
-
span_context
|
|
309
|
-
|
|
334
|
+
if 'span_context' in locals():
|
|
335
|
+
span_context.record_exception(e)
|
|
336
|
+
span_context.set_status(Status(StatusCode.ERROR, str(e)))
|
|
337
|
+
|
|
338
|
+
# Add failed processing event
|
|
339
|
+
span_context.add_event("message.processing.complete", {
|
|
340
|
+
'success': False,
|
|
341
|
+
'error': type(e).__name__
|
|
342
|
+
})
|
|
310
343
|
|
|
311
344
|
# Re-raise the exception
|
|
312
345
|
raise
|
|
313
346
|
|
|
314
347
|
finally:
|
|
348
|
+
# Record duration with standardized metric
|
|
349
|
+
duration = (datetime.now() - start_func).total_seconds() * 1000
|
|
350
|
+
self.meter.GlobalMetrics.duration.record(duration, {'handler': span_name})
|
|
351
|
+
|
|
315
352
|
if auto_flush:
|
|
316
353
|
self.force_flush(start_datetime=start_func)
|
|
317
354
|
|
|
@@ -324,6 +361,7 @@ class RebrandlyOTEL:
|
|
|
324
361
|
This is CRITICAL for Lambda functions to ensure data is sent before function freezes.
|
|
325
362
|
|
|
326
363
|
Args:
|
|
364
|
+
start_datetime: Optional start time for system metrics capture
|
|
327
365
|
timeout_millis: Maximum time to wait for flush in milliseconds
|
|
328
366
|
|
|
329
367
|
Returns:
|
|
@@ -334,14 +372,15 @@ class RebrandlyOTEL:
|
|
|
334
372
|
if start_datetime is not None:
|
|
335
373
|
end_func = datetime.now()
|
|
336
374
|
duration = (end_func - start_datetime).total_seconds() * 1000
|
|
337
|
-
cpu_percent = psutil.cpu_percent(interval=1)
|
|
375
|
+
cpu_percent = psutil.cpu_percent(interval=0.1) # Shorter interval for Lambda
|
|
338
376
|
memory = psutil.virtual_memory()
|
|
339
377
|
|
|
340
378
|
# Record metrics using standardized names
|
|
341
379
|
self.meter.GlobalMetrics.duration.record(duration, {'source': 'force_flush'})
|
|
342
380
|
self.meter.GlobalMetrics.memory_usage_bytes.set(memory.used)
|
|
343
381
|
self.meter.GlobalMetrics.cpu_usage_percentage.set(cpu_percent)
|
|
344
|
-
|
|
382
|
+
|
|
383
|
+
print(f"Function duration: {duration}ms, Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
|
|
345
384
|
|
|
346
385
|
try:
|
|
347
386
|
# Flush traces
|
|
@@ -379,9 +418,8 @@ class RebrandlyOTEL:
|
|
|
379
418
|
self._meter.shutdown()
|
|
380
419
|
if self._logger:
|
|
381
420
|
self._logger.shutdown()
|
|
382
|
-
print("[OTEL] Shutdown completed")
|
|
383
421
|
except Exception as e:
|
|
384
|
-
print(f"[OTEL] Error during shutdown: {e}")
|
|
422
|
+
print(f"[Rebrandly OTEL] Error during shutdown: {e}")
|
|
385
423
|
|
|
386
424
|
def _detect_lambda_trigger(self, event: Any) -> str:
|
|
387
425
|
"""Detect Lambda trigger type from event."""
|
|
@@ -455,6 +493,7 @@ class RebrandlyOTEL:
|
|
|
455
493
|
from opentelemetry import trace, context as otel_context
|
|
456
494
|
|
|
457
495
|
combined_attributes = attributes or {}
|
|
496
|
+
combined_attributes['messaging.operation'] = 'process'
|
|
458
497
|
|
|
459
498
|
# Extract message attributes for linking/attributes
|
|
460
499
|
if message and isinstance(message, dict):
|
|
@@ -463,16 +502,11 @@ class RebrandlyOTEL:
|
|
|
463
502
|
sns_msg = message['Sns']
|
|
464
503
|
if 'MessageId' in sns_msg:
|
|
465
504
|
combined_attributes['messaging.message_id'] = sns_msg['MessageId']
|
|
505
|
+
if 'Subject' in sns_msg:
|
|
506
|
+
combined_attributes['messaging.sns.subject'] = sns_msg['Subject']
|
|
466
507
|
if 'TopicArn' in sns_msg:
|
|
467
508
|
combined_attributes['messaging.destination'] = sns_msg['TopicArn']
|
|
468
|
-
combined_attributes['messaging.system'] = '
|
|
469
|
-
|
|
470
|
-
# Check for trace context in SNS
|
|
471
|
-
if 'MessageAttributes' in sns_msg:
|
|
472
|
-
for key, value in sns_msg['MessageAttributes'].items():
|
|
473
|
-
if key == 'traceparent' and 'Value' in value:
|
|
474
|
-
combined_attributes['message.traceparent'] = value['Value']
|
|
475
|
-
combined_attributes['message.has_trace_context'] = True
|
|
509
|
+
combined_attributes['messaging.system'] = 'sns'
|
|
476
510
|
|
|
477
511
|
elif 'messageId' in message:
|
|
478
512
|
# SQS message
|
|
@@ -480,18 +514,11 @@ class RebrandlyOTEL:
|
|
|
480
514
|
if 'eventSource' in message:
|
|
481
515
|
combined_attributes['messaging.system'] = message['eventSource']
|
|
482
516
|
|
|
483
|
-
# Check for trace context in SQS
|
|
484
|
-
if 'MessageAttributes' in message or 'messageAttributes' in message:
|
|
485
|
-
attrs = message.get('MessageAttributes') or message.get('messageAttributes', {})
|
|
486
|
-
for key, value in attrs.items():
|
|
487
|
-
if key == 'traceparent':
|
|
488
|
-
tp_value = value.get('StringValue') or value.get('stringValue', '')
|
|
489
|
-
combined_attributes['message.traceparent'] = tp_value
|
|
490
|
-
combined_attributes['message.has_trace_context'] = True
|
|
491
517
|
|
|
492
518
|
if 'awsRegion' in message:
|
|
493
519
|
combined_attributes['cloud.region'] = message['awsRegion']
|
|
494
520
|
|
|
521
|
+
|
|
495
522
|
# Use the tracer's start_span method directly to ensure it works
|
|
496
523
|
# This creates a child span of whatever is currently active
|
|
497
524
|
with self.tracer.start_span(
|
|
@@ -520,4 +547,4 @@ extract_context = otel.extract_context
|
|
|
520
547
|
attach_context = otel.attach_context
|
|
521
548
|
detach_context = otel.detach_context
|
|
522
549
|
force_flush = otel.force_flush
|
|
523
|
-
shutdown = otel.shutdown
|
|
550
|
+
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.17
|
|
4
4
|
Summary: Python OTEL wrapper by Rebrandly
|
|
5
5
|
Home-page: https://github.com/rebrandly/rebrandly-otel-python
|
|
6
6
|
Author: Antonio Romano
|
|
@@ -119,6 +119,21 @@ meter.GlobalMetrics.invocations.add(1, {'function': 'my_function'})
|
|
|
119
119
|
meter.GlobalMetrics.duration.record(150.5, {'source': 'api'})
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
+
### Custom Metrics
|
|
123
|
+
|
|
124
|
+
You can create the custom metrics you need using the default open telemetry metrics
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from src.rebrandly_otel import meter
|
|
128
|
+
|
|
129
|
+
sqs_counter = meter.meter.create_counter(
|
|
130
|
+
name="sqs_sender_counter",
|
|
131
|
+
description="Number of messages sent",
|
|
132
|
+
unit="1"
|
|
133
|
+
)
|
|
134
|
+
sqs_counter.add(1)
|
|
135
|
+
```
|
|
136
|
+
|
|
122
137
|
## Tracing Features
|
|
123
138
|
|
|
124
139
|
### Automatic Context Propagation
|
|
@@ -319,6 +334,47 @@ def process_message(record):
|
|
|
319
334
|
logger.info(f"Message data: {body}")
|
|
320
335
|
```
|
|
321
336
|
|
|
337
|
+
###
|
|
338
|
+
Flask
|
|
339
|
+
|
|
340
|
+
```python
|
|
341
|
+
|
|
342
|
+
from flask import Flask, jsonify
|
|
343
|
+
from src.rebrandly_otel import otel, logger, app_before_request, app_after_request, flask_error_handler
|
|
344
|
+
from datetime import datetime
|
|
345
|
+
|
|
346
|
+
app = Flask(__name__)
|
|
347
|
+
|
|
348
|
+
# Register the centralized OTEL handlers
|
|
349
|
+
app.before_request(app_before_request)
|
|
350
|
+
app.after_request(app_after_request)
|
|
351
|
+
app.register_error_handler(Exception, flask_error_handler)
|
|
352
|
+
|
|
353
|
+
@app.route('/health')
|
|
354
|
+
def health():
|
|
355
|
+
logger.info("Health check requested")
|
|
356
|
+
return jsonify({"status": "healthy"}), 200
|
|
357
|
+
|
|
358
|
+
@app.route('/process', methods=['POST', 'GET'])
|
|
359
|
+
def process():
|
|
360
|
+
with otel.span("process_request"):
|
|
361
|
+
logger.info("Processing POST request")
|
|
362
|
+
|
|
363
|
+
# Simulate processing
|
|
364
|
+
result = {"processed": True, "timestamp": datetime.now().isoformat()}
|
|
365
|
+
|
|
366
|
+
logger.info(f"Returning result: {result}")
|
|
367
|
+
return jsonify(result), 200
|
|
368
|
+
|
|
369
|
+
@app.route('/error')
|
|
370
|
+
def error():
|
|
371
|
+
logger.error("Error endpoint called")
|
|
372
|
+
raise Exception("Simulated error")
|
|
373
|
+
|
|
374
|
+
if __name__ == '__main__':
|
|
375
|
+
app.run(debug=True)
|
|
376
|
+
```
|
|
377
|
+
|
|
322
378
|
### More examples
|
|
323
379
|
You can find More examples [here](examples)
|
|
324
380
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
rebrandly_otel/__init__.py,sha256=MtIlcsWV1m1NAkrraS9SviDXaHybveOAV1RQi5lm_kM,323
|
|
2
|
+
rebrandly_otel/flask_support.py,sha256=BX3DPF9TYRNounfQEt0GV3oV6rMUJtHXrRRioF31TXA,6424
|
|
3
|
+
rebrandly_otel/logs.py,sha256=92jaxzI5hCHnKHu3lsSAa7K_SPHQgL46AlQUESsYNds,3724
|
|
4
|
+
rebrandly_otel/metrics.py,sha256=57jwrn8e1u66BOO7fcFrmtE3Rpt4VDixG9I78K-zrUU,8537
|
|
5
|
+
rebrandly_otel/otel_utils.py,sha256=tKelaETEeZxxvddKDpY8ESsGS77CcBbQku4oQjmiJx0,2078
|
|
6
|
+
rebrandly_otel/rebrandly_otel.py,sha256=DUhO_79jVq3rwm7HmKzw-hIx3snBW31PAxgWu14jIGo,23096
|
|
7
|
+
rebrandly_otel/traces.py,sha256=v582WtJv3t4Bn92vlDyZouibHtgWNxdRo_XmQCmSOEA,7126
|
|
8
|
+
rebrandly_otel-0.1.17.dist-info/licenses/LICENSE,sha256=KMXHvTwP62S2q-SG7CFfMU_09rUwxiqlM0izaYGdcgY,1103
|
|
9
|
+
rebrandly_otel-0.1.17.dist-info/METADATA,sha256=NieFA0UNJfoLZ2pnqbMJrQ9NY58OYrftgRG35bQE9AU,10971
|
|
10
|
+
rebrandly_otel-0.1.17.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
11
|
+
rebrandly_otel-0.1.17.dist-info/top_level.txt,sha256=26PSC1gjVUl8tTH5QfKbFevjVV4E2yojoukEfiTScvM,15
|
|
12
|
+
rebrandly_otel-0.1.17.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=KwUWW1kiWZrB9B2xwioBsA_c-Xm4YoH51_HyX6HBvQY,21778
|
|
6
|
-
rebrandly_otel/traces.py,sha256=v582WtJv3t4Bn92vlDyZouibHtgWNxdRo_XmQCmSOEA,7126
|
|
7
|
-
rebrandly_otel-0.1.14.dist-info/licenses/LICENSE,sha256=KMXHvTwP62S2q-SG7CFfMU_09rUwxiqlM0izaYGdcgY,1103
|
|
8
|
-
rebrandly_otel-0.1.14.dist-info/METADATA,sha256=tfOB74T9zcU5aXOj3Rw6bHChrd9-wexzkq7FqQUWptY,9628
|
|
9
|
-
rebrandly_otel-0.1.14.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
10
|
-
rebrandly_otel-0.1.14.dist-info/top_level.txt,sha256=26PSC1gjVUl8tTH5QfKbFevjVV4E2yojoukEfiTScvM,15
|
|
11
|
-
rebrandly_otel-0.1.14.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|