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.
- otel_utils-0.1.11/PKG-INFO +167 -0
- otel_utils-0.1.11/README.md +142 -0
- otel_utils-0.1.11/pyproject.toml +36 -0
- otel_utils-0.1.11/src/otel_utils/__init__.py +0 -0
- otel_utils-0.1.11/src/otel_utils/configurator.py +156 -0
- otel_utils-0.1.11/src/otel_utils/logging.py +115 -0
- otel_utils-0.1.11/src/otel_utils/metrics.py +112 -0
- otel_utils-0.1.11/src/otel_utils/tracing.py +78 -0
|
@@ -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
|