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.

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