rebrandly-otel 0.1.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.
Potentially problematic release.
This version of rebrandly-otel might be problematic. Click here for more details.
- rebrandly_otel-0.1.1.dist-info/METADATA +327 -0
- rebrandly_otel-0.1.1.dist-info/RECORD +11 -0
- rebrandly_otel-0.1.1.dist-info/WHEEL +5 -0
- rebrandly_otel-0.1.1.dist-info/licenses/LICENSE +19 -0
- rebrandly_otel-0.1.1.dist-info/top_level.txt +1 -0
- src/__init__.py +0 -0
- src/logs.py +112 -0
- src/metrics.py +229 -0
- src/otel_utils.py +54 -0
- src/rebrandly_otel.py +492 -0
- src/traces.py +189 -0
src/metrics.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# metrics.py
|
|
2
|
+
"""Metrics implementation for Rebrandly OTEL SDK."""
|
|
3
|
+
from typing import Optional, Dict, List
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from enum import Enum
|
|
6
|
+
from opentelemetry import metrics
|
|
7
|
+
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter
|
|
8
|
+
from opentelemetry.metrics import Meter, Histogram, Instrument, Counter
|
|
9
|
+
from opentelemetry.metrics._internal import Gauge
|
|
10
|
+
from opentelemetry.sdk.metrics import MeterProvider
|
|
11
|
+
from opentelemetry.sdk.metrics.export import (PeriodicExportingMetricReader, ConsoleMetricExporter)
|
|
12
|
+
from opentelemetry.sdk.metrics._internal.aggregation import (ExplicitBucketHistogramAggregation)
|
|
13
|
+
from opentelemetry.sdk.metrics.view import View
|
|
14
|
+
|
|
15
|
+
from src.otel_utils import *
|
|
16
|
+
|
|
17
|
+
class MetricType(Enum):
|
|
18
|
+
"""Supported metric types."""
|
|
19
|
+
COUNTER = "counter"
|
|
20
|
+
GAUGE = "gauge"
|
|
21
|
+
HISTOGRAM = "histogram"
|
|
22
|
+
UP_DOWN_COUNTER = "up_down_counter"
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class MetricDefinition:
|
|
26
|
+
"""Definition of a metric."""
|
|
27
|
+
name: str
|
|
28
|
+
description: str
|
|
29
|
+
unit: str = "1"
|
|
30
|
+
type: MetricType = MetricType.COUNTER
|
|
31
|
+
|
|
32
|
+
class RebrandlyMeter:
|
|
33
|
+
"""Wrapper for OpenTelemetry metrics with Rebrandly-specific features."""
|
|
34
|
+
|
|
35
|
+
# Standardized metric definitions aligned with Node.js
|
|
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
|
+
),
|
|
61
|
+
'cpu_usage_percentage': MetricDefinition(
|
|
62
|
+
name='cpu_usage_percentage',
|
|
63
|
+
description='CPU usage percentage',
|
|
64
|
+
unit='%',
|
|
65
|
+
type=MetricType.GAUGE
|
|
66
|
+
),
|
|
67
|
+
'memory_usage_bytes': MetricDefinition(
|
|
68
|
+
name='memory_usage_bytes',
|
|
69
|
+
description='Memory usage in bytes',
|
|
70
|
+
unit='By',
|
|
71
|
+
type=MetricType.GAUGE
|
|
72
|
+
),
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
class GlobalMetrics:
|
|
76
|
+
def __init__(self, rebrandly_meter):
|
|
77
|
+
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
|
+
self.cpu_usage_percentage: Gauge = self.__rebrandly_meter.get_metric('cpu_usage_percentage')
|
|
83
|
+
self.memory_usage_bytes: Gauge = self.__rebrandly_meter.get_metric('memory_usage_bytes')
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def __init__(self):
|
|
87
|
+
self._meter: Optional[Meter] = None
|
|
88
|
+
self._provider: Optional[MeterProvider] = None
|
|
89
|
+
self._metrics: Dict[str, Instrument] = {}
|
|
90
|
+
self.__setup_metrics()
|
|
91
|
+
self.__register_default_metrics()
|
|
92
|
+
self.GlobalMetrics = RebrandlyMeter.GlobalMetrics(self)
|
|
93
|
+
|
|
94
|
+
def __setup_metrics(self):
|
|
95
|
+
"""Initialize metrics with configured exporters."""
|
|
96
|
+
|
|
97
|
+
readers = []
|
|
98
|
+
|
|
99
|
+
# Add console exporter for local debugging
|
|
100
|
+
if is_otel_debug():
|
|
101
|
+
console_reader = PeriodicExportingMetricReader(
|
|
102
|
+
ConsoleMetricExporter(),
|
|
103
|
+
export_interval_millis=1000 # 10 seconds for debugging
|
|
104
|
+
)
|
|
105
|
+
readers.append(console_reader)
|
|
106
|
+
|
|
107
|
+
# Add OTLP exporter if configured
|
|
108
|
+
if get_otlp_endpoint() is not None:
|
|
109
|
+
otlp_exporter = OTLPMetricExporter(endpoint=get_otlp_endpoint())
|
|
110
|
+
otlp_reader = PeriodicExportingMetricReader(otlp_exporter, export_interval_millis=get_millis_batch_time())
|
|
111
|
+
readers.append(otlp_reader)
|
|
112
|
+
|
|
113
|
+
# Create views
|
|
114
|
+
views = self.__create_views()
|
|
115
|
+
|
|
116
|
+
# Create provider
|
|
117
|
+
self._provider = MeterProvider(
|
|
118
|
+
resource=create_resource(),
|
|
119
|
+
metric_readers=readers,
|
|
120
|
+
views=views
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Set as global provider
|
|
124
|
+
metrics.set_meter_provider(self._provider)
|
|
125
|
+
|
|
126
|
+
# Get meter
|
|
127
|
+
self._meter = metrics.get_meter(get_service_name(), get_service_version())
|
|
128
|
+
|
|
129
|
+
def __create_views(self) -> List[View]:
|
|
130
|
+
"""Create metric views for customization."""
|
|
131
|
+
views = []
|
|
132
|
+
|
|
133
|
+
# Histogram view with custom buckets
|
|
134
|
+
histogram_view = View(
|
|
135
|
+
instrument_type=Histogram,
|
|
136
|
+
instrument_name="*",
|
|
137
|
+
aggregation=ExplicitBucketHistogramAggregation((0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1, 2.5, 5, 7.5, 10)) # todo <-- define buckets
|
|
138
|
+
)
|
|
139
|
+
views.append(histogram_view)
|
|
140
|
+
|
|
141
|
+
return views
|
|
142
|
+
|
|
143
|
+
def __register_default_metrics(self):
|
|
144
|
+
"""Register default metrics."""
|
|
145
|
+
for name, definition in self.DEFAULT_METRICS.items():
|
|
146
|
+
self.register_metric(definition)
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def meter(self) -> Meter:
|
|
150
|
+
"""Get the underlying OpenTelemetry meter."""
|
|
151
|
+
if not self._meter:
|
|
152
|
+
# Return no-op meter if metrics are disabled
|
|
153
|
+
return metrics.get_meter(__name__)
|
|
154
|
+
return self._meter
|
|
155
|
+
|
|
156
|
+
def force_flush(self, timeout_millis: int = 5000) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Force flush all pending metrics.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
timeout_millis: Maximum time to wait for flush in milliseconds
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
True if flush succeeded, False otherwise
|
|
165
|
+
"""
|
|
166
|
+
if not hasattr(self, '_provider') or not self._provider:
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
try:
|
|
170
|
+
# Get the internal provider (MeterProvider doesn't have direct flush)
|
|
171
|
+
# We need to flush through the metric readers
|
|
172
|
+
success = self._provider.force_flush(timeout_millis)
|
|
173
|
+
return success
|
|
174
|
+
except Exception as e:
|
|
175
|
+
print(f"[Meter] Error during force flush: {e}")
|
|
176
|
+
# For metrics, we might not have a flush method, so we return True
|
|
177
|
+
return True
|
|
178
|
+
|
|
179
|
+
def shutdown(self):
|
|
180
|
+
"""Shutdown the meter provider."""
|
|
181
|
+
if hasattr(self, '_provider') and self._provider:
|
|
182
|
+
try:
|
|
183
|
+
self._provider.shutdown()
|
|
184
|
+
print("[Meter] Shutdown completed")
|
|
185
|
+
except Exception as e:
|
|
186
|
+
print(f"[Meter] Error during shutdown: {e}")
|
|
187
|
+
|
|
188
|
+
def register_metric(self, definition: MetricDefinition) -> Instrument:
|
|
189
|
+
"""Register a new metric."""
|
|
190
|
+
if definition.name in self._metrics:
|
|
191
|
+
return self._metrics[definition.name]
|
|
192
|
+
|
|
193
|
+
metric = self.__create_metric(definition)
|
|
194
|
+
self._metrics[definition.name] = metric
|
|
195
|
+
return metric
|
|
196
|
+
|
|
197
|
+
def __create_metric(self, definition: MetricDefinition) -> Instrument:
|
|
198
|
+
"""Create a metric instrument based on definition."""
|
|
199
|
+
if definition.type == MetricType.COUNTER:
|
|
200
|
+
return self.meter.create_counter(
|
|
201
|
+
name=definition.name,
|
|
202
|
+
unit=definition.unit,
|
|
203
|
+
description=definition.description
|
|
204
|
+
)
|
|
205
|
+
elif definition.type == MetricType.HISTOGRAM:
|
|
206
|
+
return self.meter.create_histogram(
|
|
207
|
+
name=definition.name,
|
|
208
|
+
unit=definition.unit,
|
|
209
|
+
description=definition.description
|
|
210
|
+
)
|
|
211
|
+
elif definition.type == MetricType.UP_DOWN_COUNTER:
|
|
212
|
+
return self.meter.create_up_down_counter(
|
|
213
|
+
name=definition.name,
|
|
214
|
+
unit=definition.unit,
|
|
215
|
+
description=definition.description
|
|
216
|
+
)
|
|
217
|
+
elif definition.type == MetricType.GAUGE:
|
|
218
|
+
# For gauges, we'll create them when needed with callbacks
|
|
219
|
+
return self.meter.create_gauge(
|
|
220
|
+
name=definition.name,
|
|
221
|
+
unit=definition.unit,
|
|
222
|
+
description=definition.description
|
|
223
|
+
)
|
|
224
|
+
else:
|
|
225
|
+
raise ValueError(f"Unknown metric type: {definition.type}")
|
|
226
|
+
|
|
227
|
+
def get_metric(self, name: str) -> Optional[Instrument]:
|
|
228
|
+
"""Get a registered metric by name."""
|
|
229
|
+
return self._metrics.get(name)
|
src/otel_utils.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
|
|
2
|
+
# otel_utils.py
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from opentelemetry.sdk.resources import Resource
|
|
8
|
+
from opentelemetry.semconv.attributes import service_attributes
|
|
9
|
+
from opentelemetry.semconv._incubating.attributes import process_attributes, deployment_attributes
|
|
10
|
+
|
|
11
|
+
def create_resource(name: str = None, version: str = None) -> Resource:
|
|
12
|
+
|
|
13
|
+
if name is None:
|
|
14
|
+
name = get_service_name()
|
|
15
|
+
if version is None:
|
|
16
|
+
version = get_service_version()
|
|
17
|
+
|
|
18
|
+
resource = Resource.create(
|
|
19
|
+
{
|
|
20
|
+
service_attributes.SERVICE_NAME: name,
|
|
21
|
+
service_attributes.SERVICE_VERSION: version,
|
|
22
|
+
process_attributes.PROCESS_RUNTIME_VERSION: f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
|
23
|
+
deployment_attributes.DEPLOYMENT_ENVIRONMENT: os.environ.get('ENV', os.environ.get('ENVIRONMENT', os.environ.get('NODE_ENV', 'local')))
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
return resource
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def get_service_name(service_name: str = None) -> str:
|
|
30
|
+
if service_name is None:
|
|
31
|
+
return os.environ.get('OTEL_SERVICE_NAME', 'default-service-python')
|
|
32
|
+
return service_name
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_service_version(service_version: str = None) -> str:
|
|
36
|
+
if service_version is None:
|
|
37
|
+
return os.environ.get('OTEL_SERVICE_VERSION', '1.0.0')
|
|
38
|
+
return service_version
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_otlp_endpoint(otlp_endpoint: str = None) -> str:
|
|
42
|
+
if otlp_endpoint is None:
|
|
43
|
+
return os.environ.get('OTEL_EXPORTER_OTLP_ENDPOINT', None)
|
|
44
|
+
return otlp_endpoint
|
|
45
|
+
|
|
46
|
+
def is_otel_debug() -> bool:
|
|
47
|
+
return os.environ.get('OTEL_DEBUG', 'false').lower() == 'true'
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_millis_batch_time():
|
|
51
|
+
try:
|
|
52
|
+
return int(os.environ.get('BATCH_EXPORT_TIME_MILLIS', 100))
|
|
53
|
+
except:
|
|
54
|
+
return 5000
|