otel-utils 0.1.11__tar.gz

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 otel-utils might be problematic. Click here for more details.

@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.3
2
+ Name: otel-utils
3
+ Version: 0.1.11
4
+ Summary: Utilidades simplificadas para instrumentación con OpenTelemetry
5
+ License: Proprietary
6
+ Author: Harold Portocarrero
7
+ Author-email: harold@getcometa.com
8
+ Requires-Python: >=3.8
9
+ Classifier: License :: Other/Proprietary License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.8
12
+ Classifier: Programming Language :: Python :: 3.9
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Requires-Dist: opentelemetry-api (>=1.29.0)
18
+ Requires-Dist: opentelemetry-exporter-otlp (>=1.29.0)
19
+ Requires-Dist: opentelemetry-instrumentation (>=0.50b0)
20
+ Requires-Dist: opentelemetry-instrumentation-logging (>=0.50b0)
21
+ Requires-Dist: opentelemetry-sdk (>=1.29.0)
22
+ Requires-Dist: typing-extensions (>=4.12.2)
23
+ Project-URL: Repository, https://github.com/getcometa/otel-utils
24
+ Description-Content-Type: text/markdown
25
+
26
+ # OpenTelemetry Utils
27
+
28
+ A Python library designed to simplify application instrumentation using OpenTelemetry. This library provides an abstraction layer that makes instrumentation more intuitive and less intrusive in your business logic.
29
+
30
+ ## Features
31
+
32
+ - Simplified OpenTelemetry configuration
33
+ - Intuitive API for distributed tracing
34
+ - Utilities for metrics and structured logging
35
+ - OpenTelemetry Collector integration
36
+ - Complete context propagation support
37
+ - Full compatibility with asynchronous applications
38
+
39
+ ## Installation
40
+
41
+ To install the library from the private repository:
42
+
43
+ ```bash
44
+ pip install git+ssh://git@github.com/your-organization/otel-utils.git
45
+ ```
46
+
47
+ Or add this to your requirements.txt:
48
+ ```bash
49
+ git+ssh://git@github.com/your-organization/otel-utils.git@v0.1.0
50
+ ```
51
+
52
+ # Basic Usage
53
+ ## Initial Configuration
54
+ ```python
55
+ from otel_utils import OtelConfig, OtelConfigurator
56
+
57
+ config = OtelConfig(
58
+ service_name="my-service",
59
+ environment="production"
60
+ )
61
+
62
+ OtelConfigurator(config)
63
+ ```
64
+
65
+ ## Tracing
66
+ ```python
67
+ from otel_utils import Tracer
68
+
69
+ tracer = Tracer("my-service")
70
+
71
+ # Using the decorator
72
+ @tracer.trace("my_operation")
73
+ async def my_function():
74
+ # Your code here
75
+ pass
76
+
77
+ # Using the context manager
78
+ with tracer.create_span("my_operation") as span:
79
+ span.set_attribute("key", "value")
80
+ # Your code here
81
+ ```
82
+
83
+ ## Metrics
84
+ ```python
85
+ from otel_utils import Metrics
86
+
87
+ metrics = Metrics("my-service")
88
+
89
+ # Simple counter
90
+ counter = metrics.get_counter("requests_total")
91
+ counter.add(1, {"endpoint": "/api/v1/resource"})
92
+
93
+ # Histogram for latencies
94
+ with metrics.measure_duration("request_duration"):
95
+ # Your code here
96
+ pass
97
+ ```
98
+
99
+ ## Structured Logging
100
+ ```python
101
+ from otel_utils import StructuredLogger
102
+
103
+ logger = StructuredLogger("my-service")
104
+
105
+ with logger.operation_context("process_order", order_id="123"):
106
+ logger.info("Starting processing")
107
+ # Your code here
108
+ ```
109
+
110
+ # OpenTelemetry Collector Integration
111
+ This library is designed to work seamlessly with the OpenTelemetry Collector. Telemetry data is sent using the OTLP protocol, which is the OpenTelemetry standard.
112
+ ```yaml
113
+ # collector configuration example
114
+ receivers:
115
+ otlp:
116
+ protocols:
117
+ grpc:
118
+ endpoint: 0.0.0.0:4317
119
+ http:
120
+ endpoint: 0.0.0.0:4318
121
+
122
+ exporters:
123
+ # configure your exporters here
124
+
125
+ service:
126
+ pipelines:
127
+ traces:
128
+ receivers: [otlp]
129
+ exporters: [your-exporter]
130
+ ```
131
+
132
+ # Best Practices
133
+ ## Separation of Concerns
134
+ Keep instrumentation separate from business logic by creating domain-specific abstractions. Your business code should remain clean and focused on its primary responsibilities.
135
+
136
+ ## Consistent Naming
137
+ Use coherent naming conventions for spans, metrics, and logs across your services. This makes it easier to correlate and analyze telemetry data.
138
+
139
+ ## Relevant Context
140
+ Include useful contextual information in spans and logs, but be mindful of sensitive data. Focus on information that aids debugging and monitoring.
141
+
142
+ ## Appropriate Granularity
143
+ Don't instrument everything. Focus on significant operations that provide value for monitoring and debugging. Consider the overhead and noise ratio when adding instrumentation.
144
+
145
+ # Development
146
+ To set up the development environment:
147
+
148
+ ```bash
149
+ # Create virtualenv
150
+ python -m venv venv
151
+ source venv/bin/activate # or `venv\Scripts\activate` on Windows
152
+
153
+ # Install development dependencies
154
+ pip install -e ".[dev]"
155
+
156
+ # Run tests
157
+ pytest
158
+ ```
159
+
160
+ # Contributing
161
+ 1. Create a feature branch (`git checkout -b feature/new-feature`)
162
+ 2. Commit your changes (`git commit -am 'Add new feature'`)
163
+ 3. Push to the branch (`git push origin feature/new-feature`)
164
+ 4. Create a Pull Request
165
+
166
+ I hope this documentation helps you understand and effectively use the OpenTelemetry Utils library. Each section is designed to guide you through the essential aspects of instrumenting your applications while maintaining clean and maintainable code.
167
+ Let me know if you need any clarification or have questions about specific features or use cases. We can explore any aspect of the library in more detail.
@@ -0,0 +1,142 @@
1
+ # OpenTelemetry Utils
2
+
3
+ A Python library designed to simplify application instrumentation using OpenTelemetry. This library provides an abstraction layer that makes instrumentation more intuitive and less intrusive in your business logic.
4
+
5
+ ## Features
6
+
7
+ - Simplified OpenTelemetry configuration
8
+ - Intuitive API for distributed tracing
9
+ - Utilities for metrics and structured logging
10
+ - OpenTelemetry Collector integration
11
+ - Complete context propagation support
12
+ - Full compatibility with asynchronous applications
13
+
14
+ ## Installation
15
+
16
+ To install the library from the private repository:
17
+
18
+ ```bash
19
+ pip install git+ssh://git@github.com/your-organization/otel-utils.git
20
+ ```
21
+
22
+ Or add this to your requirements.txt:
23
+ ```bash
24
+ git+ssh://git@github.com/your-organization/otel-utils.git@v0.1.0
25
+ ```
26
+
27
+ # Basic Usage
28
+ ## Initial Configuration
29
+ ```python
30
+ from otel_utils import OtelConfig, OtelConfigurator
31
+
32
+ config = OtelConfig(
33
+ service_name="my-service",
34
+ environment="production"
35
+ )
36
+
37
+ OtelConfigurator(config)
38
+ ```
39
+
40
+ ## Tracing
41
+ ```python
42
+ from otel_utils import Tracer
43
+
44
+ tracer = Tracer("my-service")
45
+
46
+ # Using the decorator
47
+ @tracer.trace("my_operation")
48
+ async def my_function():
49
+ # Your code here
50
+ pass
51
+
52
+ # Using the context manager
53
+ with tracer.create_span("my_operation") as span:
54
+ span.set_attribute("key", "value")
55
+ # Your code here
56
+ ```
57
+
58
+ ## Metrics
59
+ ```python
60
+ from otel_utils import Metrics
61
+
62
+ metrics = Metrics("my-service")
63
+
64
+ # Simple counter
65
+ counter = metrics.get_counter("requests_total")
66
+ counter.add(1, {"endpoint": "/api/v1/resource"})
67
+
68
+ # Histogram for latencies
69
+ with metrics.measure_duration("request_duration"):
70
+ # Your code here
71
+ pass
72
+ ```
73
+
74
+ ## Structured Logging
75
+ ```python
76
+ from otel_utils import StructuredLogger
77
+
78
+ logger = StructuredLogger("my-service")
79
+
80
+ with logger.operation_context("process_order", order_id="123"):
81
+ logger.info("Starting processing")
82
+ # Your code here
83
+ ```
84
+
85
+ # OpenTelemetry Collector Integration
86
+ This library is designed to work seamlessly with the OpenTelemetry Collector. Telemetry data is sent using the OTLP protocol, which is the OpenTelemetry standard.
87
+ ```yaml
88
+ # collector configuration example
89
+ receivers:
90
+ otlp:
91
+ protocols:
92
+ grpc:
93
+ endpoint: 0.0.0.0:4317
94
+ http:
95
+ endpoint: 0.0.0.0:4318
96
+
97
+ exporters:
98
+ # configure your exporters here
99
+
100
+ service:
101
+ pipelines:
102
+ traces:
103
+ receivers: [otlp]
104
+ exporters: [your-exporter]
105
+ ```
106
+
107
+ # Best Practices
108
+ ## Separation of Concerns
109
+ Keep instrumentation separate from business logic by creating domain-specific abstractions. Your business code should remain clean and focused on its primary responsibilities.
110
+
111
+ ## Consistent Naming
112
+ Use coherent naming conventions for spans, metrics, and logs across your services. This makes it easier to correlate and analyze telemetry data.
113
+
114
+ ## Relevant Context
115
+ Include useful contextual information in spans and logs, but be mindful of sensitive data. Focus on information that aids debugging and monitoring.
116
+
117
+ ## Appropriate Granularity
118
+ Don't instrument everything. Focus on significant operations that provide value for monitoring and debugging. Consider the overhead and noise ratio when adding instrumentation.
119
+
120
+ # Development
121
+ To set up the development environment:
122
+
123
+ ```bash
124
+ # Create virtualenv
125
+ python -m venv venv
126
+ source venv/bin/activate # or `venv\Scripts\activate` on Windows
127
+
128
+ # Install development dependencies
129
+ pip install -e ".[dev]"
130
+
131
+ # Run tests
132
+ pytest
133
+ ```
134
+
135
+ # Contributing
136
+ 1. Create a feature branch (`git checkout -b feature/new-feature`)
137
+ 2. Commit your changes (`git commit -am 'Add new feature'`)
138
+ 3. Push to the branch (`git push origin feature/new-feature`)
139
+ 4. Create a Pull Request
140
+
141
+ I hope this documentation helps you understand and effectively use the OpenTelemetry Utils library. Each section is designed to guide you through the essential aspects of instrumenting your applications while maintaining clean and maintainable code.
142
+ Let me know if you need any clarification or have questions about specific features or use cases. We can explore any aspect of the library in more detail.
@@ -0,0 +1,36 @@
1
+ [tool.poetry]
2
+ name = "otel-utils"
3
+ version = "0.1.11"
4
+ description = "Utilidades simplificadas para instrumentación con OpenTelemetry"
5
+ authors = ["Harold Portocarrero <harold@getcometa.com>"]
6
+ readme = "README.md"
7
+ packages = [
8
+ { include = "otel_utils", from = "src" }
9
+ ]
10
+ repository = "https://github.com/getcometa/otel-utils"
11
+ license = "Proprietary"
12
+
13
+ [tool.poetry.dependencies]
14
+ python = ">=3.8"
15
+ opentelemetry-api = ">=1.29.0"
16
+ opentelemetry-sdk = ">=1.29.0"
17
+ opentelemetry-exporter-otlp = ">=1.29.0"
18
+ opentelemetry-instrumentation = ">=0.50b0"
19
+ opentelemetry-instrumentation-logging = ">=0.50b0"
20
+ typing-extensions = ">=4.12.2"
21
+
22
+ [tool.poetry.group.dev.dependencies]
23
+ pytest = ">=8.3.4"
24
+ pytest-asyncio = ">=0.23.0"
25
+ pytest-cov = ">=4.1.0"
26
+ asyncio = ">=3.4.3"
27
+ black = "<=23.12.1"
28
+ isort = ">=5.13.2"
29
+ mypy = ">=1.14.1"
30
+ ruff = ">=0.9.2"
31
+ twine = ">=6.1.0"
32
+ docutils = "0.20.1"
33
+
34
+ [build-system]
35
+ requires = ["poetry-core"]
36
+ build-backend = "poetry.core.masonry.api"
File without changes
@@ -0,0 +1,156 @@
1
+ import logging
2
+ from dataclasses import dataclass
3
+ from typing import Optional, Dict, Any, Literal
4
+
5
+ from opentelemetry import trace, metrics
6
+ from opentelemetry._logs import set_logger_provider
7
+ from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter as GRPCLogExporter
8
+ from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter as GRPCMetricExporter
9
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GRPCSpanExporter
10
+ from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter as HTTPLogExporter
11
+ from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as HTTPMetricExporter
12
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPSpanExporter
13
+ from opentelemetry.instrumentation.logging import LoggingInstrumentor
14
+ from opentelemetry.sdk._logs import LoggerProvider
15
+ from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
16
+ from opentelemetry.sdk.metrics import MeterProvider
17
+ from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
18
+ from opentelemetry.sdk.resources import Resource
19
+ from opentelemetry.sdk.trace import TracerProvider
20
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
21
+
22
+
23
+ @dataclass
24
+ class OtelConfig:
25
+ """Configuration for OpenTelemetry instrumentation."""
26
+ service_name: str
27
+ environment: str
28
+ otlp_endpoint: Optional[str] = None
29
+ protocol: Literal["grpc", "http"] = "grpc"
30
+ additional_resources: Optional[Dict[str, Any]] = None
31
+ trace_sample_rate: float = 1.0
32
+ metric_export_interval_ms: int = 30000
33
+ log_level: int = logging.INFO
34
+ enable_console_logging: bool = True
35
+
36
+ @property
37
+ def trace_endpoint(self) -> Optional[str]:
38
+ if not self.otlp_endpoint:
39
+ return None
40
+ return f"{self.otlp_endpoint}/v1/traces" if self.protocol == "http" else self.otlp_endpoint
41
+
42
+ @property
43
+ def metric_endpoint(self) -> Optional[str]:
44
+ if not self.otlp_endpoint:
45
+ return None
46
+ return f"{self.otlp_endpoint}/v1/metrics" if self.protocol == "http" else self.otlp_endpoint
47
+
48
+ @property
49
+ def log_endpoint(self) -> Optional[str]:
50
+ if not self.otlp_endpoint:
51
+ return None
52
+ return f"{self.otlp_endpoint}/v1/logs" if self.protocol == "http" else self.otlp_endpoint
53
+
54
+
55
+ class OtelConfigurator:
56
+ """Central Configurator for OpenTelemetry."""
57
+
58
+ def __init__(self, config: OtelConfig):
59
+ self.config = config
60
+ self._setup_resource()
61
+ self._setup_tracing()
62
+ self._setup_metrics()
63
+ self._setup_logging()
64
+
65
+ def _get_exporters(self):
66
+ """Get the appropriate exporters based on the protocol."""
67
+ if not self.config.otlp_endpoint:
68
+ return None, None, None
69
+
70
+ if self.config.protocol == "grpc":
71
+ return (
72
+ GRPCSpanExporter(endpoint=self.config.trace_endpoint),
73
+ GRPCMetricExporter(endpoint=self.config.metric_endpoint),
74
+ GRPCLogExporter(endpoint=self.config.log_endpoint)
75
+ )
76
+ else:
77
+ return (
78
+ HTTPSpanExporter(endpoint=self.config.trace_endpoint),
79
+ HTTPMetricExporter(endpoint=self.config.metric_endpoint),
80
+ HTTPLogExporter(endpoint=self.config.log_endpoint)
81
+ )
82
+
83
+ def _setup_resource(self) -> None:
84
+ """Set up the base resource with the service attributes."""
85
+ resource_attributes = {
86
+ "service.name": self.config.service_name,
87
+ "deployment.environment": self.config.environment,
88
+ }
89
+ if self.config.additional_resources:
90
+ resource_attributes.update(self.config.additional_resources)
91
+
92
+ self.resource = Resource.create(resource_attributes)
93
+
94
+ def _setup_tracing(self) -> None:
95
+ """Set up the tracing system."""
96
+ tracer_provider = TracerProvider(resource=self.resource)
97
+
98
+ if self.config.otlp_endpoint:
99
+ span_exporter, _, _ = self._get_exporters()
100
+ span_processor = BatchSpanProcessor(span_exporter)
101
+ tracer_provider.add_span_processor(span_processor)
102
+
103
+ trace.set_tracer_provider(tracer_provider)
104
+
105
+ def _setup_metrics(self) -> None:
106
+ """Set up the metrics system."""
107
+ if self.config.otlp_endpoint:
108
+ _, metric_exporter, _ = self._get_exporters()
109
+ metric_reader = PeriodicExportingMetricReader(
110
+ metric_exporter,
111
+ export_interval_millis=self.config.metric_export_interval_ms
112
+ )
113
+ meter_provider = MeterProvider(resource=self.resource, metric_readers=[metric_reader])
114
+ else:
115
+ meter_provider = MeterProvider(resource=self.resource)
116
+
117
+ metrics.set_meter_provider(meter_provider)
118
+
119
+ def _setup_logging(self) -> None:
120
+ """
121
+ Set up the logging system with OpenTelemetry integration.
122
+ """
123
+ root_logger = logging.getLogger()
124
+ for handler in root_logger.handlers[:]:
125
+ root_logger.removeHandler(handler)
126
+
127
+ service_logger = logging.getLogger(self.config.service_name)
128
+ for handler in service_logger.handlers[:]:
129
+ service_logger.removeHandler(handler)
130
+
131
+ logging.getLogger('opentelemetry').setLevel(logging.ERROR)
132
+
133
+ logger_provider = LoggerProvider(resource=self.resource)
134
+ if self.config.otlp_endpoint:
135
+ _, _, log_exporter = self._get_exporters()
136
+ logger_provider.add_log_record_processor(
137
+ BatchLogRecordProcessor(log_exporter)
138
+ )
139
+
140
+ set_logger_provider(logger_provider)
141
+
142
+ console_handler = logging.StreamHandler()
143
+ formatter = logging.Formatter(
144
+ '%(asctime)s %(levelname)s [%(name)s] %(message)s'
145
+ )
146
+ console_handler.setFormatter(formatter)
147
+ root_logger.addHandler(console_handler)
148
+ root_logger.setLevel(self.config.log_level)
149
+
150
+ LoggingInstrumentor().instrument(
151
+ logger_provider=logger_provider,
152
+ set_logging_format=True,
153
+ log_level=self.config.log_level
154
+ )
155
+
156
+ self.logger = logging.getLogger(self.config.service_name)
@@ -0,0 +1,115 @@
1
+ import logging
2
+ import json
3
+ from datetime import datetime
4
+ from typing import Any, Dict, Optional
5
+ from opentelemetry import trace
6
+ from contextlib import contextmanager
7
+
8
+
9
+ class StructuredLogger:
10
+ """
11
+ Logger that produces structured logs with tracing context.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ service_name: str,
17
+ default_attributes: Optional[Dict[str, Any]] = None
18
+ ):
19
+ self.logger = logging.getLogger(service_name)
20
+ self.default_attributes = {
21
+ "service.name": service_name,
22
+ **(default_attributes or {})
23
+ }
24
+
25
+ def _get_trace_context(self) -> Dict[str, str]:
26
+ """
27
+ Gets the current tracing context if it exists.
28
+ """
29
+ span = trace.get_current_span()
30
+ if span.is_recording():
31
+ ctx = span.get_span_context()
32
+ return {
33
+ "trace_id": format(ctx.trace_id, "032x"),
34
+ "span_id": format(ctx.span_id, "016x")
35
+ }
36
+ return {}
37
+
38
+ def _log(
39
+ self,
40
+ level: int,
41
+ message: str,
42
+ *args,
43
+ **kwargs
44
+ ):
45
+ """
46
+ Create a structured log with tracing context.
47
+ """
48
+ extra = kwargs.pop("extra", {})
49
+ trace_context = self._get_trace_context()
50
+
51
+ additional_info = []
52
+ if kwargs.get("error_type"):
53
+ additional_info.append(f"type={kwargs['error_type']}")
54
+ if kwargs.get("error_message"):
55
+ additional_info.append(f"message={kwargs['error_message']}")
56
+ if kwargs.get("operation"):
57
+ additional_info.append(f"operation={kwargs['operation']}")
58
+
59
+ additional_str = f" - {', '.join(additional_info)}" if additional_info else ""
60
+ trace_str = f"[trace_id={trace_context.get('trace_id', '0')}]" if trace_context else ""
61
+
62
+ log_message = f"{trace_str} {message}{additional_str}"
63
+
64
+ self.logger.log(
65
+ level,
66
+ log_message,
67
+ extra={
68
+ "structured": True,
69
+ "trace_context": trace_context,
70
+ **extra,
71
+ **kwargs
72
+ }
73
+ )
74
+
75
+ def debug(self, message: str, *args, **kwargs):
76
+ self._log(logging.DEBUG, message, *args, **kwargs)
77
+
78
+ def info(self, message: str, *args, **kwargs):
79
+ self._log(logging.INFO, message, *args, **kwargs)
80
+
81
+ def error(self, message: str, *args, **kwargs):
82
+ self._log(logging.ERROR, message, *args, **kwargs)
83
+
84
+ @contextmanager
85
+ def operation_context(
86
+ self,
87
+ operation_name: str,
88
+ **context
89
+ ):
90
+ """
91
+ Provides context for an operation, recording its beginning and end.
92
+ """
93
+ try:
94
+ self.info(
95
+ f"Iniciando {operation_name}",
96
+ operation=operation_name,
97
+ status="started",
98
+ **context
99
+ )
100
+ yield
101
+ self.info(
102
+ f"Completado {operation_name}",
103
+ operation=operation_name,
104
+ status="completed",
105
+ **context
106
+ )
107
+ except Exception as e:
108
+ self.error(
109
+ f"Error en {operation_name}: {str(e)}",
110
+ operation=operation_name,
111
+ status="failed",
112
+ error=str(e),
113
+ **context
114
+ )
115
+ raise
@@ -0,0 +1,112 @@
1
+ from contextlib import contextmanager
2
+ from typing import Optional, Dict, Any, List
3
+ from opentelemetry import metrics
4
+ from opentelemetry.metrics import Observation, CallbackOptions
5
+ import time
6
+
7
+
8
+ class Metrics:
9
+ """
10
+ Advanced metrics utility that supports different types of measurements
11
+ and asynchronous callbacks.
12
+ """
13
+
14
+ def __init__(self, service_name: str):
15
+ self._meter = metrics.get_meter(service_name)
16
+ self._gauges = {}
17
+ self._histograms = {}
18
+ self._counters = {}
19
+
20
+ def create_histogram(
21
+ self,
22
+ name: str,
23
+ description: str = "",
24
+ unit: str = "1",
25
+ boundaries: List[float] = None
26
+ ):
27
+ """
28
+ Create a histogram with custom limits for distributions of values.
29
+ """
30
+ return self._meter.create_histogram(
31
+ name=name,
32
+ description=description,
33
+ unit=unit,
34
+ boundaries=boundaries
35
+ )
36
+
37
+ @contextmanager
38
+ def measure_duration(
39
+ self,
40
+ name: str,
41
+ attributes: Optional[Dict[str, Any]] = None
42
+ ):
43
+ """
44
+ Measures the duration of a operation and records it as a histogram.
45
+ """
46
+ start_time = time.monotonic()
47
+ try:
48
+ yield
49
+ finally:
50
+ duration = time.monotonic() - start_time
51
+ self.get_histogram(name).record(
52
+ duration,
53
+ attributes=attributes
54
+ )
55
+
56
+ def observe_async(
57
+ self,
58
+ name: str,
59
+ description: str = "",
60
+ callback: callable = None
61
+ ):
62
+ """
63
+ Log metrics asynchronously using a callback.
64
+ Useful for metrics that need to be calculated or queried.
65
+ """
66
+ def observer_callback(options: CallbackOptions) -> List[Observation]:
67
+ """
68
+ Callback function for observable gauge metrics.
69
+
70
+ Args:
71
+ options: CallbackOptions object containing timing and context information
72
+ for when the observation is being made. Though not used here,
73
+ it's part of the OpenTelemetry interface and can be useful for:
74
+ - Accessing timestamp of the observation
75
+ - Getting the current context
76
+ - Handling collection-specific parameters
77
+
78
+ Returns:
79
+ List[Observation]: A list containing the current observation value.
80
+ Returns [Observation(0)] in case of errors to maintain metric continuity.
81
+ """
82
+ try:
83
+ value = callback()
84
+ return [Observation(value)]
85
+ except Exception:
86
+ return [Observation(0)]
87
+
88
+ self._meter.create_observable_gauge(
89
+ name,
90
+ callbacks=[observer_callback],
91
+ description=description
92
+ )
93
+
94
+ def get_counter(self, name: str, description: str = "", unit: str = "1"):
95
+ """Gets or creates a counter."""
96
+ if name not in self._counters:
97
+ self._counters[name] = self._meter.create_counter(
98
+ name=name,
99
+ description=description,
100
+ unit=unit
101
+ )
102
+ return self._counters[name]
103
+
104
+ def get_histogram(self, name: str, description: str = "", unit: str = "1"):
105
+ """Gets or creates a histogram."""
106
+ if name not in self._histograms:
107
+ self._histograms[name] = self._meter.create_histogram(
108
+ name=name,
109
+ description=description,
110
+ unit=unit
111
+ )
112
+ return self._histograms[name]
@@ -0,0 +1,78 @@
1
+ import functools
2
+ from contextlib import contextmanager
3
+ from typing import Optional, Iterator, Any, Dict
4
+ from opentelemetry import trace
5
+ from opentelemetry.trace import Status, StatusCode, Span, SpanKind
6
+
7
+
8
+ class Tracer:
9
+ """
10
+ Advanced tracing utility that supports complex and nested traces.
11
+ """
12
+
13
+ def __init__(self, service_name: str):
14
+ self._tracer = trace.get_tracer(service_name)
15
+
16
+ @contextmanager
17
+ def start_as_current_span(
18
+ self,
19
+ name: str,
20
+ context: Optional[Any] = None,
21
+ kind: Optional[SpanKind] = SpanKind.INTERNAL,
22
+ attributes: Optional[Dict[str, Any]] = None,
23
+ links: Optional[list] = None
24
+ ) -> Iterator[Span]:
25
+ """
26
+ Creates a span and sets it as the current span in the context.
27
+ Properly handles the context propagation from external sources.
28
+ """
29
+ with self._tracer.start_as_current_span(
30
+ name,
31
+ context=context,
32
+ attributes=attributes,
33
+ kind=kind,
34
+ links=links
35
+ ) as span:
36
+ yield span
37
+
38
+ @contextmanager
39
+ def create_span(
40
+ self,
41
+ name: str,
42
+ attributes: Optional[Dict[str, Any]] = None,
43
+ kind: Optional[SpanKind] = SpanKind.INTERNAL,
44
+ links: Optional[list] = None
45
+ ) -> Iterator[Span]:
46
+ """
47
+ Creates a custom span that can be used in a 'with' context.
48
+ Useful for complex operations that need granular control.
49
+ """
50
+ with self._tracer.start_as_current_span(
51
+ name,
52
+ attributes=attributes,
53
+ kind=kind,
54
+ links=links
55
+ ) as span:
56
+ yield span
57
+
58
+ def trace(self, name: Optional[str] = None, attributes: Optional[Dict] = None):
59
+ def decorator(func):
60
+ @functools.wraps(func)
61
+ async def async_wrapper(*args, **kwargs):
62
+ with self._tracer.start_as_current_span(
63
+ name or func.__name__,
64
+ attributes=attributes,
65
+ kind=SpanKind.SERVER
66
+ ) as span:
67
+ try:
68
+ result = await func(*args, **kwargs)
69
+ span.set_status(Status(StatusCode.OK))
70
+ return result
71
+ except Exception as e:
72
+ span.set_status(Status(StatusCode.ERROR), str(e))
73
+ span.record_exception(e)
74
+ raise
75
+
76
+ return async_wrapper
77
+
78
+ return decorator