rebrandly-otel 0.1.17__py3-none-any.whl → 0.1.19__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.

@@ -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
@@ -1,15 +1,17 @@
1
1
  # flask_integration.py
2
2
  """Flask integration for Rebrandly OTEL SDK."""
3
- from typing import TYPE_CHECKING
4
- from opentelemetry.trace import Status, StatusCode, SpanKind
5
3
 
6
- if TYPE_CHECKING:
7
- from .rebrandly_otel import RebrandlyOTEL
4
+ import json
5
+ from rebrandly_otel import Status, StatusCode, SpanKind
8
6
 
9
- def setup_flask(otel: 'RebrandlyOTEL', app):
7
+ from flask import request, jsonify
8
+ from werkzeug.exceptions import HTTPException
9
+
10
+
11
+ def setup_flask(otel, app):
10
12
  """
11
13
  Setup Flask application with OTEL instrumentation.
12
-
14
+
13
15
  Example:
14
16
  from flask import Flask
15
17
  from rebrandly_otel import otel
@@ -23,31 +25,39 @@ def setup_flask(otel: 'RebrandlyOTEL', app):
23
25
  app.register_error_handler(Exception, lambda e: flask_error_handler(otel, e))
24
26
  return app
25
27
 
26
- def app_before_request(otel: 'RebrandlyOTEL'):
28
+ def app_before_request(otel):
27
29
  """
28
30
  Setup tracing for incoming Flask request.
29
31
  To be used with Flask's before_request hook.
30
32
  """
31
- from flask import request
32
33
 
33
34
  # Extract trace context from headers
34
35
  headers = dict(request.headers)
35
36
  token = otel.attach_context(headers)
36
37
  request.trace_token = token
37
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
+
38
43
  # Start span for request using start_as_current_span to make it the active span
39
44
  span = otel.tracer.tracer.start_as_current_span(
40
- f"{request.method} {request.path}",
45
+ span_name,
41
46
  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"
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,
51
61
  },
52
62
  kind=SpanKind.SERVER
53
63
  )
@@ -59,33 +69,22 @@ def app_before_request(otel: 'RebrandlyOTEL'):
59
69
  otel.logger.logger.info(f"Request started: {request.method} {request.path}",
60
70
  extra={"http.method": request.method, "http.path": request.path})
61
71
 
62
- def app_after_request(otel: 'RebrandlyOTEL', response):
72
+ def app_after_request(otel, response):
63
73
  """
64
74
  Cleanup tracing after Flask request completes.
65
75
  To be used with Flask's after_request hook.
66
76
  """
67
- from flask import request
68
77
 
69
78
  # Check if we have a span and it's still recording
70
79
  if hasattr(request, 'span') and request.span.is_recording():
71
- request.span.set_attribute("http.status_code", response.status_code)
80
+ # Update both new and old attribute names for compatibility
81
+ request.span.set_attribute("http.response.status_code", response.status_code)
72
82
 
73
83
  # Set span status based on HTTP status code
74
84
  if response.status_code >= 400:
75
85
  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
86
  else:
83
87
  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
88
 
90
89
  # Properly close the span context manager
91
90
  if hasattr(request, 'span_context'):
@@ -102,21 +101,14 @@ def app_after_request(otel: 'RebrandlyOTEL', response):
102
101
  otel.logger.logger.info(f"Request completed: {response.status_code}",
103
102
  extra={"http.status_code": response.status_code})
104
103
 
105
- # Record general invocation metric
106
- otel.meter.GlobalMetrics.invocations.add(1, {
107
- "endpoint": request.path,
108
- "method": request.method
109
- })
110
-
104
+ otel.force_flush(timeout_millis=100)
111
105
  return response
112
106
 
113
- def flask_error_handler(otel: 'RebrandlyOTEL', exception):
107
+ def flask_error_handler(otel, exception):
114
108
  """
115
109
  Handle Flask exceptions and record them in the current span.
116
110
  To be used with Flask's errorhandler decorator.
117
111
  """
118
- from flask import request, jsonify
119
- from werkzeug.exceptions import HTTPException
120
112
 
121
113
  # Determine the status code
122
114
  if isinstance(exception, HTTPException):
@@ -130,15 +122,14 @@ def flask_error_handler(otel: 'RebrandlyOTEL', exception):
130
122
 
131
123
  # Record exception in span if available and still recording
132
124
  if hasattr(request, 'span') and request.span.is_recording():
133
- request.span.set_attribute("http.status_code", status_code)
125
+ request.span.set_attribute("http.response.status_code", status_code)
134
126
  request.span.set_attribute("error.type", type(exception).__name__)
135
127
 
136
128
  request.span.record_exception(exception)
137
129
  request.span.set_status(Status(StatusCode.ERROR, str(exception)))
138
130
  request.span.add_event("exception", {
139
131
  "exception.type": type(exception).__name__,
140
- "exception.message": str(exception),
141
- "http.status_code": status_code
132
+ "exception.message": str(exception)
142
133
  })
143
134
 
144
135
  # Only close the span if it's still recording (not already ended)
@@ -155,14 +146,6 @@ def flask_error_handler(otel: 'RebrandlyOTEL', exception):
155
146
  "http.status_code": status_code
156
147
  })
157
148
 
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
149
  # Return error response with the determined status code
167
150
  return jsonify({
168
151
  "error": str(exception),
rebrandly_otel/logs.py CHANGED
@@ -1,6 +1,7 @@
1
1
  # logs.py
2
2
  """Logging implementation for Rebrandly OTEL SDK."""
3
3
  import logging
4
+ import sys
4
5
  from typing import Optional
5
6
  from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
6
7
  from opentelemetry.sdk._logs.export import (
@@ -34,12 +35,15 @@ class RebrandlyLogger:
34
35
  self._provider.add_log_record_processor(SimpleLogRecordProcessor(console_exporter))
35
36
 
36
37
  # Add OTLP exporter if configured
37
- if get_otlp_endpoint():
38
- otlp_exporter = OTLPLogExporter(endpoint=get_otlp_endpoint())
38
+ otel_endpoint = get_otlp_endpoint()
39
+ if otel_endpoint:
40
+ otlp_exporter = OTLPLogExporter(
41
+ timeout=5,
42
+ endpoint=otel_endpoint
43
+ )
39
44
  batch_processor = BatchLogRecordProcessor(otlp_exporter, export_timeout_millis=get_millis_batch_time())
40
45
  self._provider.add_log_record_processor(batch_processor)
41
46
 
42
- # Set as global provider
43
47
  set_logger_provider(self._provider)
44
48
 
45
49
  # Configure standard logging
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
- 'invocations': MetricDefinition(
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='cpu_usage_percentage',
63
- description='CPU usage percentage',
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='memory_usage_bytes',
69
- description='Memory usage in bytes',
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
 
@@ -105,8 +78,12 @@ class RebrandlyMeter:
105
78
  readers.append(console_reader)
106
79
 
107
80
  # Add OTLP exporter if configured
108
- if get_otlp_endpoint() is not None:
109
- otlp_exporter = OTLPMetricExporter(endpoint=get_otlp_endpoint())
81
+ otel_endpoint = get_otlp_endpoint()
82
+ if otel_endpoint is not None:
83
+ otlp_exporter = OTLPMetricExporter(
84
+ endpoint=otel_endpoint,
85
+ timeout=5
86
+ )
110
87
  otlp_reader = PeriodicExportingMetricReader(otlp_exporter, export_interval_millis=get_millis_batch_time())
111
88
  readers.append(otlp_reader)
112
89
 
@@ -3,6 +3,7 @@
3
3
 
4
4
  import os
5
5
  import sys
6
+ import grpc
6
7
 
7
8
  from opentelemetry.sdk.resources import Resource
8
9
  from opentelemetry.semconv.attributes import service_attributes
@@ -33,8 +34,9 @@ def get_package_version():
33
34
  try:
34
35
  from importlib_metadata import version, PackageNotFoundError
35
36
  return version('rebrandly_otel')
36
- except:
37
- return '?.0.1'
37
+ except Exception as e:
38
+ print(f"[OTEL Utils] Warning: Could not get package version: {e}")
39
+ return '0.1.0'
38
40
 
39
41
 
40
42
  def get_service_name(service_name: str = None) -> str:
@@ -49,10 +51,31 @@ def get_service_version(service_version: str = None) -> str:
49
51
  return service_version
50
52
 
51
53
 
52
- def get_otlp_endpoint(otlp_endpoint: str = None) -> str:
53
- if otlp_endpoint is None:
54
- return os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT', None)
55
- return otlp_endpoint
54
+ def get_otlp_endpoint(otlp_endpoint: str = None) -> str | None:
55
+ endpoint = otlp_endpoint or os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT', None)
56
+
57
+ if endpoint is not None:
58
+ try:
59
+ from urllib.parse import urlparse
60
+
61
+ # Parse the endpoint
62
+ parsed = urlparse(endpoint if '://' in endpoint else f'http://{endpoint}')
63
+ host = parsed.hostname
64
+ port = parsed.port
65
+
66
+ # Test gRPC connection
67
+ channel = grpc.insecure_channel(f'{host}:{port}')
68
+ try:
69
+ # Wait for the channel to be ready
70
+ grpc.channel_ready_future(channel).result(timeout=3)
71
+ return endpoint
72
+ finally:
73
+ channel.close()
74
+
75
+ except Exception as e:
76
+ print(f"[OTEL] Failed to connect to OTLP endpoint {endpoint}: {e}")
77
+ return None
78
+ return endpoint
56
79
 
57
80
  def is_otel_debug() -> bool:
58
81
  return os.environ.get('OTEL_DEBUG', 'false').lower() == 'true'
@@ -61,5 +84,6 @@ def is_otel_debug() -> bool:
61
84
  def get_millis_batch_time():
62
85
  try:
63
86
  return int(os.environ.get('BATCH_EXPORT_TIME_MILLIS', 100))
64
- except:
87
+ except Exception as e:
88
+ print(f"[OTEL Utils] Warning: Invalid BATCH_EXPORT_TIME_MILLIS value, using default 5000ms: {e}")
65
89
  return 5000
@@ -160,8 +160,6 @@ class RebrandlyOTEL:
160
160
 
161
161
  result = None
162
162
  try:
163
- # Increment invocation counter with standardized metric name
164
- self.meter.GlobalMetrics.invocations.add(1, {'function': span_name})
165
163
 
166
164
  # Create and execute within span
167
165
  with self.tracer.start_span(
@@ -204,17 +202,9 @@ class RebrandlyOTEL:
204
202
  complete_event_attrs['success'] = success
205
203
  span.add_event("lambda.invocation.complete", complete_event_attrs)
206
204
 
207
- # Increment success counter with standardized metric
208
- self.meter.GlobalMetrics.successful_invocations.add(1, {'function': span_name})
209
-
210
205
  return result
211
206
 
212
207
  except Exception as e:
213
- # Increment error counter with standardized metric
214
- self.meter.GlobalMetrics.error_invocations.add(1, {
215
- 'function': span_name,
216
- 'error': type(e).__name__
217
- })
218
208
 
219
209
  # Add failed completion event with error attribute (THIS IS THE KEY ADDITION)
220
210
  span.add_event("lambda.invocation.complete", {
@@ -236,13 +226,9 @@ class RebrandlyOTEL:
236
226
  from opentelemetry import context as otel_context
237
227
  otel_context.detach(token)
238
228
 
239
- # Record duration with standardized metric
240
- duration = (datetime.now() - start_time).total_seconds() * 1000
241
- self.meter.GlobalMetrics.duration.record(duration, {'function': span_name})
242
-
243
229
  # Force flush if enabled
244
230
  if auto_flush:
245
- self.logger.logger.info(f"[Rebrandly OTEL] Lambda '{span_name}' completed in {duration:.2f}ms, flushing...")
231
+ self.logger.logger.info(f"[Rebrandly OTEL] Lambda '{span_name}', flushing...")
246
232
  flush_success = self.force_flush(timeout_millis=1000)
247
233
  if not flush_success:
248
234
  self.logger.logger.warning("[Rebrandly OTEL] Force flush may not have completed fully")
@@ -272,12 +258,9 @@ class RebrandlyOTEL:
272
258
 
273
259
  result = None
274
260
  try:
275
- # Increment invocations counter with standardized metric
276
- self.meter.GlobalMetrics.invocations.add(1, {'handler': span_name})
277
-
278
261
  # Create span and execute function
279
262
  span_function = self.span
280
- if record is not None and ('MessageAttributes' in record or 'messageAttributes' in record):
263
+ if record is not None and (('MessageAttributes' in record or 'messageAttributes' in record) or ('Sns' in record and 'MessageAttributes' in record['Sns'])):
281
264
  span_function = self.aws_message_span
282
265
 
283
266
  with span_function(span_name, message=record, attributes=span_attributes, kind=kind) as span_context:
@@ -318,18 +301,9 @@ class RebrandlyOTEL:
318
301
  complete_event_attrs['success'] = success
319
302
  span_context.add_event("message.processing.complete", complete_event_attrs)
320
303
 
321
- # Increment success counter with standardized metric
322
- self.meter.GlobalMetrics.successful_invocations.add(1, {'handler': span_name})
323
-
324
304
  return result
325
305
 
326
306
  except Exception as e:
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
- })
332
-
333
307
  # Record the exception in the span
334
308
  if 'span_context' in locals():
335
309
  span_context.record_exception(e)
@@ -345,10 +319,6 @@ class RebrandlyOTEL:
345
319
  raise
346
320
 
347
321
  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
-
352
322
  if auto_flush:
353
323
  self.force_flush(start_datetime=start_func)
354
324
 
@@ -371,16 +341,14 @@ class RebrandlyOTEL:
371
341
 
372
342
  if start_datetime is not None:
373
343
  end_func = datetime.now()
374
- duration = (end_func - start_datetime).total_seconds() * 1000
375
344
  cpu_percent = psutil.cpu_percent(interval=0.1) # Shorter interval for Lambda
376
345
  memory = psutil.virtual_memory()
377
346
 
378
347
  # Record metrics using standardized names
379
- self.meter.GlobalMetrics.duration.record(duration, {'source': 'force_flush'})
380
348
  self.meter.GlobalMetrics.memory_usage_bytes.set(memory.used)
381
349
  self.meter.GlobalMetrics.cpu_usage_percentage.set(cpu_percent)
382
350
 
383
- print(f"Function duration: {duration}ms, Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
351
+ print(f"Function Memory usage: {memory.percent}%, CPU usage: {cpu_percent}%")
384
352
 
385
353
  try:
386
354
  # Flush traces
@@ -402,6 +370,7 @@ class RebrandlyOTEL:
402
370
  time.sleep(0.1)
403
371
 
404
372
  except Exception as e:
373
+ print(f"[Rebrandly OTEL] Error during force flush: {e}")
405
374
  success = False
406
375
 
407
376
  return success
rebrandly_otel/traces.py CHANGED
@@ -32,8 +32,12 @@ class RebrandlyTracer:
32
32
  self._provider.add_span_processor(SimpleSpanProcessor(console_exporter))
33
33
 
34
34
  # Add OTLP exporter if configured
35
- if get_otlp_endpoint() is not None:
36
- otlp_exporter = OTLPSpanExporter(endpoint=get_otlp_endpoint())
35
+ otel_endpoint = get_otlp_endpoint()
36
+ if otel_endpoint is not None:
37
+ otlp_exporter = OTLPSpanExporter(
38
+ endpoint=otel_endpoint,
39
+ timeout=5
40
+ )
37
41
 
38
42
  # Use batch processor for production
39
43
  batch_processor = BatchSpanProcessor(otlp_exporter, export_timeout_millis=get_millis_batch_time())
@@ -139,14 +143,14 @@ class RebrandlyTracer:
139
143
  if target_span and hasattr(target_span, 'record_exception'):
140
144
  if exception is not None:
141
145
  target_span.record_exception(exception)
142
- target_span.set_status(trace.Status(trace.StatusCode.ERROR, str(exception)), description=msg)
146
+ target_span.set_status(trace.Status(trace.StatusCode.ERROR, str(exception)))
143
147
 
144
148
 
145
149
  def record_span_success(self, span: Optional[Span] = None, msg: Optional[str] = None):
146
- """Record an exception on a span."""
150
+ """Record success on a span."""
147
151
  target_span = span or self.get_current_span()
148
- if target_span and hasattr(target_span, 'record_exception'):
149
- target_span.set_status(trace.Status(trace.StatusCode.OK), description=msg)
152
+ if target_span and hasattr(target_span, 'set_status'):
153
+ target_span.set_status(trace.Status(trace.StatusCode.OK))
150
154
 
151
155
 
152
156
  def add_event(self, name: str, attributes: Optional[Dict[str, Any]] = None, span: Optional[Span] = None):
@@ -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
 
@@ -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=5byeN-cDmBRpeZDw9IBz_vuiJm3wsGEbcAk5pwYHNAU,3791
5
+ rebrandly_otel/metrics.py,sha256=8aAqdr3SAcX_rVivTl_aHeD_BRByt-Qnfij_51Y7Fn0,7561
6
+ rebrandly_otel/otel_utils.py,sha256=ej6ayM1e9muHx1RNaEBZJTBIL_G0QAtDkqkS1VsoTyo,3026
7
+ rebrandly_otel/rebrandly_otel.py,sha256=hyNMFQzjZdP6oaXd0x1VKSKZg4-IuCtO6XeAiRZc7oo,21299
8
+ rebrandly_otel/traces.py,sha256=JY_3RWbzpUxzEx3GqTVgggsyA2DB4oR-zDftIFFJha4,7174
9
+ rebrandly_otel-0.1.19.dist-info/licenses/LICENSE,sha256=KMXHvTwP62S2q-SG7CFfMU_09rUwxiqlM0izaYGdcgY,1103
10
+ rebrandly_otel-0.1.19.dist-info/METADATA,sha256=DUACwLo6mJSEjir0pfY8kszp-G36JDnz5lh3uivRECg,12854
11
+ rebrandly_otel-0.1.19.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
12
+ rebrandly_otel-0.1.19.dist-info/top_level.txt,sha256=26PSC1gjVUl8tTH5QfKbFevjVV4E2yojoukEfiTScvM,15
13
+ rebrandly_otel-0.1.19.dist-info/RECORD,,
@@ -1,12 +0,0 @@
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,,