Skip to content

Commit 53ce71f

Browse files
Merge pull request #95 from ivanildobarauna-dev/feature/improve-otel-integration
Feature: Comprehensive OpenTelemetry Integration
2 parents 62ea858 + 70fb0f6 commit 53ce71f

File tree

11 files changed

+679
-61
lines changed

11 files changed

+679
-61
lines changed

.idea/vcs.xml

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CHANGELOG.md

+20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,26 @@ All notable changes to this project will be documented in this file. Dates are d
44

55
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
66

7+
#### [1.5.0](https://me.github.com/ivdatahub/api-to-dataframe/compare/1.4.0...1.5.0)
8+
9+
> 10 March 2025
10+
11+
- feature: Comprehensive OpenTelemetry integration with otel-wrapper:
12+
- Added detailed tracing for all operations (HTTP requests, dataframe conversions, retries)
13+
- Added metrics for performance monitoring and error tracking
14+
- Added structured logging with contextual information
15+
- Improved error handling with detailed error context
16+
- Complete observability across all components using otel-wrapper
17+
18+
#### [1.4.0](https://me.github.com/ivdatahub/api-to-dataframe/compare/1.3.11...1.4.0)
19+
20+
> 10 March 2025
21+
22+
- feature: add otel-wrapper
23+
- feature: update logging implementation and version bump
24+
- chore: deps
25+
- chore: bump version
26+
727
#### [1.3.11](https://me.github.com/ivdatahub/api-to-dataframe/compare/1.3.10...1.3.11)
828

929
> 24 September 2024

conftest.py

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Root conftest.py
2+
# This file ensures that pytest only collects tests from our tests directory
3+
# and ignores any tests in temporary directories or installed packages
4+
5+
import os
6+
import sys
7+
8+
# Add the 'src' directory to the path so imports work correctly
9+
# This ensures that the in-development code is used, not the installed version
10+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "src")))
11+
12+
def pytest_ignore_collect(collection_path, config):
13+
"""
14+
Configure pytest to ignore certain paths when collecting tests.
15+
16+
Returns:
17+
bool: True if the path should be ignored, False otherwise.
18+
"""
19+
# Skip the temp directory
20+
if "temp/" in str(collection_path):
21+
return True
22+
23+
return False

poetry.lock

+7-7
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+24-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "api-to-dataframe"
3-
version = "1.4.0"
3+
version = "1.5.0"
44
description = "A package to convert API responses to pandas dataframe"
55
authors = ["IvanildoBarauna <[email protected]>"]
66
readme = "README.md"
@@ -27,7 +27,7 @@ python = "^3.9"
2727
pandas = "^2.2.3"
2828
requests = "^2.32.3"
2929
logging = "^0.4.9.6"
30-
otel-wrapper = "^0.0.1"
30+
otel-wrapper = "^0.1.0"
3131

3232
[tool.poetry.group.dev.dependencies]
3333
poetry-dynamic-versioning = "^1.3.0"
@@ -58,3 +58,25 @@ disable = [
5858
"C0115", # missing-class-docstring
5959
"R0903", # too-few-public-methods
6060
]
61+
62+
[tool.pytest.ini_options]
63+
testpaths = ["tests"]
64+
python_files = "test_*.py"
65+
python_classes = ["Test*"]
66+
python_functions = ["test_*"]
67+
68+
[tool.coverage.run]
69+
source = ["src/api_to_dataframe"]
70+
omit = [
71+
"tests/*",
72+
"temp/*",
73+
"*/__init__.py",
74+
]
75+
76+
[tool.coverage.report]
77+
exclude_lines = [
78+
"pragma: no cover",
79+
"def __repr__",
80+
"raise NotImplementedError",
81+
"if __name__ == .__main__.:",
82+
]

src/api_to_dataframe/controller/client_builder.py

+165-17
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from api_to_dataframe.models.retainer import retry_strategies, Strategies
22
from api_to_dataframe.models.get_data import GetData
3-
from api_to_dataframe.utils.logger import logger
4-
from otel_wrapper import OpenObservability
3+
from api_to_dataframe.utils.logger import logger, telemetry
4+
import time
55

66

77
class ClientBuilder:
@@ -35,16 +35,40 @@ def __init__( # pylint: disable=too-many-positional-arguments,too-many-argument
3535
if headers is None:
3636
headers = {}
3737
if endpoint == "":
38-
logger.error("endpoint cannot be an empty string")
38+
error_msg = "endpoint cannot be an empty string"
39+
logger.error(error_msg)
40+
telemetry.logs().new_log(
41+
msg=error_msg,
42+
tags={"component": "ClientBuilder", "method": "__init__"},
43+
level=40 # ERROR level
44+
)
3945
raise ValueError
4046
if not isinstance(retries, int) or retries < 0:
41-
logger.error("retries must be a non-negative integer")
47+
error_msg = "retries must be a non-negative integer"
48+
logger.error(error_msg)
49+
telemetry.logs().new_log(
50+
msg=error_msg,
51+
tags={"component": "ClientBuilder", "method": "__init__"},
52+
level=40 # ERROR level
53+
)
4254
raise ValueError
4355
if not isinstance(initial_delay, int) or initial_delay < 0:
44-
logger.error("initial_delay must be a non-negative integer")
56+
error_msg = "initial_delay must be a non-negative integer"
57+
logger.error(error_msg)
58+
telemetry.logs().new_log(
59+
msg=error_msg,
60+
tags={"component": "ClientBuilder", "method": "__init__"},
61+
level=40 # ERROR level
62+
)
4563
raise ValueError
4664
if not isinstance(connection_timeout, int) or connection_timeout < 0:
47-
logger.error("connection_timeout must be a non-negative integer")
65+
error_msg = "connection_timeout must be a non-negative integer"
66+
logger.error(error_msg)
67+
telemetry.logs().new_log(
68+
msg=error_msg,
69+
tags={"component": "ClientBuilder", "method": "__init__"},
70+
level=40 # ERROR level
71+
)
4872
raise ValueError
4973

5074
self.endpoint = endpoint
@@ -53,9 +77,28 @@ def __init__( # pylint: disable=too-many-positional-arguments,too-many-argument
5377
self.headers = headers
5478
self.retries = retries
5579
self.delay = initial_delay
56-
self._o11y_wrapper = OpenObservability(application_name="api-to-dataframe").get_wrapper()
57-
self._traces = self._o11y_wrapper.traces()
58-
self._tracer = self._traces.get_tracer()
80+
81+
# Record client initialization metric
82+
telemetry.metrics().metric_increment(
83+
name="client.initialization",
84+
tags={
85+
"endpoint": endpoint,
86+
"retry_strategy": retry_strategy.name,
87+
"connection_timeout": str(connection_timeout)
88+
}
89+
)
90+
91+
# Log initialization
92+
telemetry.logs().new_log(
93+
msg=f"ClientBuilder initialized with endpoint {endpoint}",
94+
tags={
95+
"endpoint": endpoint,
96+
"retry_strategy": retry_strategy.name,
97+
"connection_timeout": str(connection_timeout),
98+
"component": "ClientBuilder"
99+
},
100+
level=20 # INFO level
101+
)
59102

60103
@retry_strategies
61104
def get_api_data(self):
@@ -69,16 +112,62 @@ def get_api_data(self):
69112
Returns:
70113
dict: The JSON response from the API as a dictionary.
71114
"""
72-
73-
with self._tracer.start_as_current_span("get_last_quote") as span:
115+
# Use the telemetry spans with context manager
116+
with telemetry.traces().span_in_context("get_api_data") as (span, _):
117+
# Add span attributes
74118
span.set_attribute("endpoint", self.endpoint)
75-
119+
span.set_attribute("retry_strategy", self.retry_strategy.name)
120+
span.set_attribute("connection_timeout", self.connection_timeout)
121+
122+
# Log the API request
123+
telemetry.logs().new_log(
124+
msg=f"Making API request to {self.endpoint}",
125+
tags={
126+
"endpoint": self.endpoint,
127+
"component": "ClientBuilder",
128+
"method": "get_api_data"
129+
},
130+
level=20 # INFO level
131+
)
132+
133+
# Record the start time for response time measurement
134+
start_time = time.time()
135+
136+
# Make the API request
76137
response = GetData.get_response(
77138
endpoint=self.endpoint,
78139
headers=self.headers,
79140
connection_timeout=self.connection_timeout,
80141
)
81-
142+
143+
# Calculate response time
144+
response_time = time.time() - start_time
145+
146+
# Record response time as histogram
147+
telemetry.metrics().record_histogram(
148+
name="api.response_time",
149+
tags={"endpoint": self.endpoint},
150+
value=response_time
151+
)
152+
153+
# Record successful request metric
154+
telemetry.metrics().metric_increment(
155+
name="api.request.success",
156+
tags={"endpoint": self.endpoint}
157+
)
158+
159+
# Log success
160+
telemetry.logs().new_log(
161+
msg=f"API request to {self.endpoint} successful",
162+
tags={
163+
"endpoint": self.endpoint,
164+
"response_status": response.status_code,
165+
"response_time": response_time,
166+
"component": "ClientBuilder",
167+
"method": "get_api_data"
168+
},
169+
level=20 # INFO level
170+
)
82171

83172
return response.json()
84173

@@ -97,7 +186,66 @@ def api_to_dataframe(response: dict):
97186
Returns:
98187
DataFrame: A pandas DataFrame containing the data from the API response.
99188
"""
100-
101-
df = GetData.to_dataframe(response)
102-
103-
return df
189+
# Use telemetry for this operation
190+
with telemetry.traces().span_in_context("api_to_dataframe") as (span, _):
191+
response_size = len(response) if isinstance(response, list) else 1
192+
span.set_attribute("response_size", response_size)
193+
194+
# Log conversion start
195+
telemetry.logs().new_log(
196+
msg="Converting API response to DataFrame",
197+
tags={
198+
"response_size": response_size,
199+
"response_type": type(response).__name__,
200+
"component": "ClientBuilder",
201+
"method": "api_to_dataframe"
202+
},
203+
level=20 # INFO level
204+
)
205+
206+
try:
207+
# Convert to dataframe
208+
df = GetData.to_dataframe(response)
209+
210+
# Record metrics
211+
telemetry.metrics().metric_increment(
212+
name="dataframe.conversion.success",
213+
tags={"size": len(df)}
214+
)
215+
216+
# Log success
217+
telemetry.logs().new_log(
218+
msg="Successfully converted API response to DataFrame",
219+
tags={
220+
"dataframe_rows": len(df),
221+
"dataframe_columns": len(df.columns),
222+
"component": "ClientBuilder",
223+
"method": "api_to_dataframe"
224+
},
225+
level=20 # INFO level
226+
)
227+
228+
return df
229+
230+
except Exception as e:
231+
# Record failure metric
232+
telemetry.metrics().metric_increment(
233+
name="dataframe.conversion.failure",
234+
tags={"error_type": type(e).__name__}
235+
)
236+
237+
# Log error
238+
error_msg = f"Failed to convert API response to DataFrame: {str(e)}"
239+
telemetry.logs().new_log(
240+
msg=error_msg,
241+
tags={
242+
"error": str(e),
243+
"error_type": type(e).__name__,
244+
"component": "ClientBuilder",
245+
"method": "api_to_dataframe"
246+
},
247+
level=40 # ERROR level
248+
)
249+
250+
# Re-raise the exception
251+
raise

0 commit comments

Comments
 (0)