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,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Span Attributes Processor for Rebrandly OTEL SDK
|
|
3
|
+
Automatically adds attributes from OTEL_SPAN_ATTRIBUTES environment variable to all spans
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from opentelemetry.context import Context
|
|
9
|
+
from opentelemetry.sdk.trace import ReadableSpan, Span
|
|
10
|
+
from opentelemetry.sdk.trace.export import SpanProcessor
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SpanAttributesProcessor(SpanProcessor):
|
|
14
|
+
"""
|
|
15
|
+
Span processor that automatically adds attributes from OTEL_SPAN_ATTRIBUTES
|
|
16
|
+
environment variable to all spans at creation time.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""Initialize the processor and parse OTEL_SPAN_ATTRIBUTES."""
|
|
21
|
+
self.name = 'SpanAttributesProcessor'
|
|
22
|
+
self.span_attributes = self._parse_span_attributes()
|
|
23
|
+
|
|
24
|
+
# Log parsed attributes in debug mode
|
|
25
|
+
if os.environ.get('OTEL_DEBUG', 'false').lower() == 'true' and self.span_attributes:
|
|
26
|
+
print(f'[SpanAttributesProcessor] Parsed OTEL_SPAN_ATTRIBUTES: {self.span_attributes}')
|
|
27
|
+
|
|
28
|
+
def _parse_span_attributes(self) -> dict:
|
|
29
|
+
"""
|
|
30
|
+
Parse OTEL_SPAN_ATTRIBUTES environment variable.
|
|
31
|
+
Format: key1=value1,key2=value2
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
Dictionary of parsed attributes as key-value pairs
|
|
35
|
+
"""
|
|
36
|
+
attributes = {}
|
|
37
|
+
otel_span_attrs = os.environ.get('OTEL_SPAN_ATTRIBUTES', None)
|
|
38
|
+
|
|
39
|
+
if not otel_span_attrs or otel_span_attrs.strip() == '':
|
|
40
|
+
return attributes
|
|
41
|
+
|
|
42
|
+
try:
|
|
43
|
+
pairs = otel_span_attrs.split(',')
|
|
44
|
+
for attr in pairs:
|
|
45
|
+
trimmed_attr = attr.strip()
|
|
46
|
+
if trimmed_attr and '=' in trimmed_attr:
|
|
47
|
+
# Split on first '=' only, in case value contains '='
|
|
48
|
+
key, value = trimmed_attr.split('=', 1)
|
|
49
|
+
key = key.strip()
|
|
50
|
+
value = value.strip()
|
|
51
|
+
if key:
|
|
52
|
+
attributes[key] = value
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f'[SpanAttributesProcessor] Warning: Invalid OTEL_SPAN_ATTRIBUTES value: {e}')
|
|
55
|
+
|
|
56
|
+
return attributes
|
|
57
|
+
|
|
58
|
+
def on_start(self, span: Span, parent_context: Optional[Context] = None) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Called when a span is started.
|
|
61
|
+
Adds configured attributes to the span.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
span: The span that was just started
|
|
65
|
+
parent_context: The parent context (optional)
|
|
66
|
+
"""
|
|
67
|
+
try:
|
|
68
|
+
# Add all parsed attributes to the span
|
|
69
|
+
if self.span_attributes:
|
|
70
|
+
for key, value in self.span_attributes.items():
|
|
71
|
+
span.set_attribute(key, value)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
# Fail silently to avoid breaking the entire tracing pipeline
|
|
74
|
+
# Log only in debug mode to avoid noise
|
|
75
|
+
if os.environ.get('OTEL_DEBUG', 'false').lower() == 'true':
|
|
76
|
+
print(f'[SpanAttributesProcessor] Error processing span: {e}')
|
|
77
|
+
|
|
78
|
+
def on_end(self, span: ReadableSpan) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Called when a span is ended.
|
|
81
|
+
No-op for this processor.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
span: The span that was just ended
|
|
85
|
+
"""
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
def shutdown(self) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Shutdown the processor.
|
|
91
|
+
No-op for this processor.
|
|
92
|
+
"""
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
def force_flush(self, timeout_millis: int = 30000) -> bool:
|
|
96
|
+
"""
|
|
97
|
+
Force flush the processor.
|
|
98
|
+
No-op for this processor.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
timeout_millis: Maximum time to wait for flush in milliseconds
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Always returns True as there's nothing to flush
|
|
105
|
+
"""
|
|
106
|
+
return True
|
rebrandly_otel/traces.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# traces.py
|
|
2
|
+
"""Tracing implementation for Rebrandly OTEL SDK."""
|
|
3
|
+
from typing import Optional, Dict, Any, ContextManager
|
|
4
|
+
from contextlib import contextmanager
|
|
5
|
+
from opentelemetry import trace, propagate, context
|
|
6
|
+
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
|
|
7
|
+
from opentelemetry.sdk.trace import TracerProvider, Span
|
|
8
|
+
from opentelemetry.sdk.trace.export import (
|
|
9
|
+
ConsoleSpanExporter,
|
|
10
|
+
BatchSpanProcessor,
|
|
11
|
+
SimpleSpanProcessor
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .otel_utils import *
|
|
15
|
+
from .span_attributes_processor import SpanAttributesProcessor
|
|
16
|
+
|
|
17
|
+
class RebrandlyTracer:
|
|
18
|
+
"""Wrapper for OpenTelemetry tracing with Rebrandly-specific features."""
|
|
19
|
+
|
|
20
|
+
def __init__(self):
|
|
21
|
+
self._tracer: Optional[trace.Tracer] = None
|
|
22
|
+
self._provider: Optional[TracerProvider] = None
|
|
23
|
+
self._setup_tracing()
|
|
24
|
+
|
|
25
|
+
def _setup_tracing(self):
|
|
26
|
+
|
|
27
|
+
# Create provider with resource
|
|
28
|
+
self._provider = TracerProvider(resource=create_resource())
|
|
29
|
+
|
|
30
|
+
# Add span attributes processor to automatically add OTEL_SPAN_ATTRIBUTES to all spans
|
|
31
|
+
self._provider.add_span_processor(SpanAttributesProcessor())
|
|
32
|
+
|
|
33
|
+
# Add console exporter for local debugging
|
|
34
|
+
if is_otel_debug():
|
|
35
|
+
console_exporter = ConsoleSpanExporter()
|
|
36
|
+
self._provider.add_span_processor(SimpleSpanProcessor(console_exporter))
|
|
37
|
+
|
|
38
|
+
# Add OTLP exporter if configured
|
|
39
|
+
otel_endpoint = get_otlp_endpoint()
|
|
40
|
+
if otel_endpoint is not None:
|
|
41
|
+
otlp_exporter = OTLPSpanExporter(
|
|
42
|
+
endpoint=otel_endpoint,
|
|
43
|
+
timeout=5
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
# Use batch processor for production
|
|
47
|
+
batch_processor = BatchSpanProcessor(otlp_exporter, export_timeout_millis=get_millis_batch_time())
|
|
48
|
+
self._provider.add_span_processor(batch_processor)
|
|
49
|
+
|
|
50
|
+
# Set as global provider
|
|
51
|
+
trace.set_tracer_provider(self._provider)
|
|
52
|
+
|
|
53
|
+
# Get tracer
|
|
54
|
+
self._tracer = trace.get_tracer(get_service_name(), get_service_version())
|
|
55
|
+
|
|
56
|
+
def force_flush(self, timeout_millis: int = 5000) -> bool:
|
|
57
|
+
"""
|
|
58
|
+
Force flush all pending spans.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
timeout_millis: Maximum time to wait for flush in milliseconds
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
True if flush succeeded, False otherwise
|
|
65
|
+
"""
|
|
66
|
+
if not self._provider:
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
try:
|
|
70
|
+
# ForceFlush on the TracerProvider will flush all processors
|
|
71
|
+
success = self._provider.force_flush(timeout_millis)
|
|
72
|
+
|
|
73
|
+
if not success:
|
|
74
|
+
print(f"[Tracer] Force flush timed out after {timeout_millis}ms")
|
|
75
|
+
|
|
76
|
+
return success
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"[Tracer] Error during force flush: {e}")
|
|
79
|
+
return False
|
|
80
|
+
|
|
81
|
+
def shutdown(self):
|
|
82
|
+
"""Shutdown the tracer provider and all processors."""
|
|
83
|
+
if self._provider:
|
|
84
|
+
try:
|
|
85
|
+
self._provider.shutdown()
|
|
86
|
+
print("[Tracer] Shutdown completed")
|
|
87
|
+
except Exception as e:
|
|
88
|
+
print(f"[Tracer] Error during shutdown: {e}")
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def tracer(self) -> trace.Tracer:
|
|
92
|
+
"""Get the underlying OpenTelemetry tracer."""
|
|
93
|
+
if not self._tracer:
|
|
94
|
+
# Return no-op tracer if tracing is disabled
|
|
95
|
+
return trace.get_tracer(__name__)
|
|
96
|
+
return self._tracer
|
|
97
|
+
|
|
98
|
+
@contextmanager
|
|
99
|
+
def start_span(self,
|
|
100
|
+
name: str,
|
|
101
|
+
attributes: Optional[Dict[str, Any]] = None,
|
|
102
|
+
kind: trace.SpanKind = trace.SpanKind.INTERNAL) -> ContextManager[Span]:
|
|
103
|
+
"""Start a new span as the current span."""
|
|
104
|
+
# Ensure we use the tracer to create a child span of the current span
|
|
105
|
+
with self.tracer.start_as_current_span(
|
|
106
|
+
name,
|
|
107
|
+
attributes=attributes,
|
|
108
|
+
kind=kind
|
|
109
|
+
) as span:
|
|
110
|
+
yield span
|
|
111
|
+
|
|
112
|
+
def start_span_with_context(self,
|
|
113
|
+
name: str,
|
|
114
|
+
attributes: Dict[str, str],
|
|
115
|
+
context_attributes: Optional[Dict[str, Any]] = None):
|
|
116
|
+
"""Start a span with extracted context (e.g., from message headers)."""
|
|
117
|
+
# Extract context from carrier
|
|
118
|
+
|
|
119
|
+
carrier, extracted_context = self.__get_aws_message_context_attributes(context_attributes)
|
|
120
|
+
ctx = propagate.extract(extracted_context)
|
|
121
|
+
|
|
122
|
+
if context_attributes is not None:
|
|
123
|
+
# Start span with extracted context
|
|
124
|
+
with self.tracer.start_as_current_span(
|
|
125
|
+
name,
|
|
126
|
+
context=ctx,
|
|
127
|
+
attributes=attributes
|
|
128
|
+
) as span:
|
|
129
|
+
yield span
|
|
130
|
+
else:
|
|
131
|
+
# Start span with current context
|
|
132
|
+
with self.tracer.start_as_current_span(
|
|
133
|
+
name,
|
|
134
|
+
context=context.get_current(),
|
|
135
|
+
attributes=attributes
|
|
136
|
+
) as span:
|
|
137
|
+
yield span
|
|
138
|
+
|
|
139
|
+
def get_current_span(self) -> Span:
|
|
140
|
+
"""Get the currently active span."""
|
|
141
|
+
return trace.get_current_span()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def record_span_exception(self, exception: Exception=None, span: Optional[Span] = None, msg: Optional[str] = None):
|
|
145
|
+
"""Record an exception on a span."""
|
|
146
|
+
target_span = span or self.get_current_span()
|
|
147
|
+
if target_span and hasattr(target_span, 'record_exception'):
|
|
148
|
+
if exception is not None:
|
|
149
|
+
target_span.record_exception(exception)
|
|
150
|
+
target_span.set_status(trace.Status(trace.StatusCode.ERROR, str(exception)))
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def record_span_success(self, span: Optional[Span] = None, msg: Optional[str] = None):
|
|
154
|
+
"""Record success on a span."""
|
|
155
|
+
target_span = span or self.get_current_span()
|
|
156
|
+
if target_span and hasattr(target_span, 'set_status'):
|
|
157
|
+
target_span.set_status(trace.Status(trace.StatusCode.OK))
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def add_event(self, name: str, attributes: Optional[Dict[str, Any]] = None, span: Optional[Span] = None):
|
|
161
|
+
"""Add an event to a span."""
|
|
162
|
+
target_span = span or self.get_current_span()
|
|
163
|
+
if target_span and hasattr(target_span, 'add_event'):
|
|
164
|
+
target_span.add_event(name, attributes=attributes or {})
|
|
165
|
+
|
|
166
|
+
# AWS-specific helpers
|
|
167
|
+
def __get_aws_message_context_attributes(self, msg: dict):
|
|
168
|
+
"""
|
|
169
|
+
Get trace context as AWS message attributes format.
|
|
170
|
+
Used for SQS/SNS message propagation.
|
|
171
|
+
"""
|
|
172
|
+
carrier = {}
|
|
173
|
+
# Convert to AWS message attributes format
|
|
174
|
+
message_attributes = {}
|
|
175
|
+
if msg is not None and 'MessageAttributes' in msg:
|
|
176
|
+
for key, value in msg['MessageAttributes'].items():
|
|
177
|
+
carrier[key] = {
|
|
178
|
+
'StringValue': value,
|
|
179
|
+
'DataType': 'String'
|
|
180
|
+
}
|
|
181
|
+
context_extracted = propagate.extract(carrier)
|
|
182
|
+
return carrier, context_extracted
|
|
183
|
+
|
|
184
|
+
def get_attributes_for_aws_from_context(self):
|
|
185
|
+
# Create carrier for message attributes
|
|
186
|
+
carrier = {}
|
|
187
|
+
|
|
188
|
+
# Inject trace context into carrier
|
|
189
|
+
propagate.inject(carrier)
|
|
190
|
+
|
|
191
|
+
# Convert carrier to SQS message attributes format
|
|
192
|
+
message_attributes = {}
|
|
193
|
+
for key, value in carrier.items():
|
|
194
|
+
message_attributes[key] = {
|
|
195
|
+
'StringValue': value,
|
|
196
|
+
'DataType': 'String'
|
|
197
|
+
}
|
|
198
|
+
return message_attributes
|