Skip to content

Commit

Permalink
Merge branch 'main' into issue_2478
Browse files Browse the repository at this point in the history
  • Loading branch information
ocelotl committed May 24, 2024
2 parents 7c5ad67 + 66a107f commit e56584b
Show file tree
Hide file tree
Showing 33 changed files with 628 additions and 137 deletions.
8 changes: 6 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- `opentelemetry-instrumentation-dbapi` Fix compatibility with Psycopg3 to extract libpq build version (#2500)[https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2500]
- `opentelemetry-instrumentation-grpc` AioClientInterceptor should propagate with a Metadata object
([#2363](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2363))
- `opentelemetry-instrumentation-boto3sqs` Instrument Session and resource
Expand All @@ -51,6 +52,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#2461](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2461))
- Remove SDK dependency from opentelemetry-instrumentation-grpc
([#2474](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2474))
- `opentelemetry-instrumentation-elasticsearch` Improved support for version 8
([#2420](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2420))
- `opentelemetry-instrumentation-asyncio` Check for __name__ attribute in the coroutine
([#2521](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2521))

## Version 1.24.0/0.45b0 (2024-03-28)

Expand Down Expand Up @@ -122,7 +127,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#1959](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/1959))
- `opentelemetry-resource-detector-azure` Added dependency for Cloud Resource ID attribute
([#2072](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2072))

## Version 1.21.0/0.42b0 (2023-11-01)

### Added
Expand Down Expand Up @@ -1536,4 +1541,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-resource-detector-azure` Suppress instrumentation for `urllib` call
([#2178](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2178))
- AwsLambdaInstrumentor handles and re-raises function exception ([#2245](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/2245))

4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ You can run `tox` with the following arguments:
`black` and `isort` are executed when `tox -e lint` is run. The reported errors can be tedious to fix manually.
An easier way to do so is:

1. Run `.tox/lint-some-package/bin/black .`
2. Run `.tox/lint-some-package/bin/isort .`
1. Run `.tox/lint/bin/black .`
2. Run `.tox/lint/bin/isort .`

Or you can call formatting and linting in one command by [pre-commit](https://pre-commit.com/):

Expand Down
2 changes: 1 addition & 1 deletion gen-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-c dev-requirements.txt
astor==0.8.1
jinja2==3.1.3
jinja2==3.1.4
markupsafe==2.0.1
isort
black
Expand Down
2 changes: 1 addition & 1 deletion instrumentation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
| [opentelemetry-instrumentation-confluent-kafka](./opentelemetry-instrumentation-confluent-kafka) | confluent-kafka >= 1.8.2, <= 2.3.0 | No | experimental
| [opentelemetry-instrumentation-dbapi](./opentelemetry-instrumentation-dbapi) | dbapi | No | experimental
| [opentelemetry-instrumentation-django](./opentelemetry-instrumentation-django) | django >= 1.10 | Yes | experimental
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 2.0 | No | experimental
| [opentelemetry-instrumentation-elasticsearch](./opentelemetry-instrumentation-elasticsearch) | elasticsearch >= 6.0 | No | experimental
| [opentelemetry-instrumentation-falcon](./opentelemetry-instrumentation-falcon) | falcon >= 1.4.1, < 4.0.0 | Yes | experimental
| [opentelemetry-instrumentation-fastapi](./opentelemetry-instrumentation-fastapi) | fastapi ~= 0.58 | Yes | experimental
| [opentelemetry-instrumentation-flask](./opentelemetry-instrumentation-flask) | flask >= 1.0 | Yes | migration
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,8 @@ def trace_item(self, coro_or_future):
return coro_or_future

async def trace_coroutine(self, coro):
if not hasattr(coro, "__name__"):
return coro
start = default_timer()
attr = {
"type": "coroutine",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import asyncio
from unittest.mock import patch

# pylint: disable=no-name-in-module
from opentelemetry.instrumentation.asyncio import AsyncioInstrumentor
from opentelemetry.instrumentation.asyncio.environment_variables import (
OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE,
)
from opentelemetry.test.test_base import TestBase
from opentelemetry.trace import get_tracer


class TestAsyncioAnext(TestBase):
@patch.dict(
"os.environ",
{OTEL_PYTHON_ASYNCIO_COROUTINE_NAMES_TO_TRACE: "async_func"},
)
def setUp(self):
super().setUp()
AsyncioInstrumentor().instrument()
self._tracer = get_tracer(
__name__,
)

def tearDown(self):
super().tearDown()
AsyncioInstrumentor().uninstrument()

# Asyncio anext() does not have __name__ attribute, which is used to determine if the coroutine should be traced.
# This test is to ensure that the instrumentation does not break when the coroutine does not have __name__ attribute.
def test_asyncio_anext(self):
async def main():
async def async_gen():
for it in range(2):
yield it

async_gen_instance = async_gen()
agen = anext(async_gen_instance)
await asyncio.create_task(agen)

asyncio.run(main())
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 0)
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
request_hook (Callable) - a function with extra user-defined logic to be performed before performing the request
this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, api_params: dict) -> None
response_hook (Callable) - a function with extra user-defined logic to be performed after performing the request
this function signature is: def request_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None
this function signature is: def response_hook(span: Span, service_name: str, operation_name: str, result: dict) -> None
for example:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -427,12 +427,19 @@ def traced_execution(
if args and self._commenter_enabled:
try:
args_list = list(args)
if hasattr(self._connect_module, "__libpq_version__"):
libpq_version = self._connect_module.__libpq_version__
else:
libpq_version = (
self._connect_module.pq.__build_version__
)

commenter_data = {
# Psycopg2/framework information
"db_driver": f"psycopg2:{self._connect_module.__version__.split(' ')[0]}",
"dbapi_threadsafety": self._connect_module.threadsafety,
"dbapi_level": self._connect_module.apilevel,
"libpq_version": self._connect_module.__libpq_version__,
"libpq_version": libpq_version,
"driver_paramstyle": self._connect_module.paramstyle,
}
if self._commenter_options.get(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,32 @@ def test_executemany_comment(self):
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
)

def test_compatible_build_version_psycopg_psycopg2_libpq(self):
connect_module = mock.MagicMock()
connect_module.__version__ = mock.MagicMock()
connect_module.pq = mock.MagicMock()
connect_module.pq.__build_version__ = 123
connect_module.apilevel = 123
connect_module.threadsafety = 123
connect_module.paramstyle = "test"

db_integration = dbapi.DatabaseApiIntegration(
"testname",
"testcomponent",
enable_commenter=True,
commenter_options={"db_driver": False, "dbapi_level": False},
connect_module=connect_module,
)
mock_connection = db_integration.wrapped_connection(
mock_connect, {}, {}
)
cursor = mock_connection.cursor()
cursor.executemany("Select 1;")
self.assertRegex(
cursor.query,
r"Select 1 /\*dbapi_threadsafety=123,driver_paramstyle='test',libpq_version=123,traceparent='\d{1,2}-[a-zA-Z0-9_]{32}-[a-zA-Z0-9_]{16}-\d{1,2}'\*/;",
)

def test_executemany_flask_integration_comment(self):
connect_module = mock.MagicMock()
connect_module.__version__ = mock.MagicMock()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def response_hook(span, response):
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.semconv.trace import SpanAttributes
from opentelemetry.trace import SpanKind, get_tracer
from opentelemetry.trace import SpanKind, Status, StatusCode, get_tracer

from .utils import sanitize_body

Expand All @@ -103,6 +103,7 @@ def response_hook(span, response):
es_transport_split = elasticsearch.VERSION[0] > 7
if es_transport_split:
import elastic_transport
from elastic_transport._models import DefaultType

logger = getLogger(__name__)

Expand Down Expand Up @@ -173,7 +174,12 @@ def _instrument(self, **kwargs):

def _uninstrument(self, **kwargs):
# pylint: disable=no-member
unwrap(elasticsearch.Transport, "perform_request")
transport_class = (
elastic_transport.Transport
if es_transport_split
else elasticsearch.Transport
)
unwrap(transport_class, "perform_request")


_regex_doc_url = re.compile(r"/_doc/([^/]+)")
Expand All @@ -182,6 +188,7 @@ def _uninstrument(self, **kwargs):
_regex_search_url = re.compile(r"/([^/]+)/_search[/]?")


# pylint: disable=too-many-statements
def _wrap_perform_request(
tracer,
span_name_prefix,
Expand Down Expand Up @@ -234,7 +241,22 @@ def wrapper(wrapped, _, args, kwargs):
kind=SpanKind.CLIENT,
) as span:
if callable(request_hook):
request_hook(span, method, url, kwargs)
# elasticsearch 8 changed the parameters quite a bit
if es_transport_split:

def normalize_kwargs(k, v):
if isinstance(v, DefaultType):
v = str(v)
elif isinstance(v, elastic_transport.HttpHeaders):
v = dict(v)
return (k, v)

hook_kwargs = dict(
normalize_kwargs(k, v) for k, v in kwargs.items()
)
else:
hook_kwargs = kwargs
request_hook(span, method, url, hook_kwargs)

if span.is_recording():
attributes = {
Expand All @@ -260,16 +282,41 @@ def wrapper(wrapped, _, args, kwargs):
span.set_attribute(key, value)

rv = wrapped(*args, **kwargs)
if isinstance(rv, dict) and span.is_recording():

body = rv.body if es_transport_split else rv
if isinstance(body, dict) and span.is_recording():
for member in _ATTRIBUTES_FROM_RESULT:
if member in rv:
if member in body:
span.set_attribute(
f"elasticsearch.{member}",
str(rv[member]),
str(body[member]),
)

# since the transport split the raising of exceptions that set the error status
# are called after this code so need to set error status manually
if es_transport_split and span.is_recording():
if not (method == "HEAD" and rv.meta.status == 404) and (
not 200 <= rv.meta.status < 299
):
exception = elasticsearch.exceptions.HTTP_EXCEPTIONS.get(
rv.meta.status, elasticsearch.exceptions.ApiError
)
message = str(body)
if isinstance(body, dict):
error = body.get("error", message)
if isinstance(error, dict) and "type" in error:
error = error["type"]
message = error

span.set_status(
Status(
status_code=StatusCode.ERROR,
description=f"{exception.__name__}: {message}",
)
)

if callable(response_hook):
response_hook(span, rv)
response_hook(span, body)
return rv

return wrapper
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@
# limitations under the License.


_instruments = ("elasticsearch >= 2.0",)
_instruments = ("elasticsearch >= 6.0",)
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
asgiref==3.7.2
attrs==23.2.0
Deprecated==1.2.14
elasticsearch==8.12.1
elasticsearch-dsl==8.12.0
elastic-transport==8.12.0
importlib-metadata==6.11.0
iniconfig==2.0.0
packaging==23.2
pluggy==1.4.0
py==1.11.0
py-cpuinfo==9.0.0
pytest==7.1.3
pytest-benchmark==4.0.0
python-dateutil==2.8.2
six==1.16.0
tomli==2.0.1
typing_extensions==4.10.0
urllib3==2.2.1
wrapt==1.16.0
zipp==3.17.0
-e opentelemetry-instrumentation
-e instrumentation/opentelemetry-instrumentation-elasticsearch
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ class Index:
dsl_index_span_name = "Elasticsearch/test-index/doc/2"
dsl_index_url = "/test-index/doc/2"
dsl_search_method = "GET"

perform_request_mock_path = "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request"


def mock_response(body: str, status_code: int = 200):
return (status_code, {}, body)
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,9 @@ class Index:
dsl_index_span_name = "Elasticsearch/test-index/_doc/:id"
dsl_index_url = "/test-index/_doc/2"
dsl_search_method = "POST"

perform_request_mock_path = "elasticsearch.connection.http_urllib3.Urllib3HttpConnection.perform_request"


def mock_response(body: str, status_code: int = 200):
return (status_code, {}, body)
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from elastic_transport import ApiResponseMeta, HttpHeaders
from elastic_transport._node import NodeApiResponse
from elasticsearch_dsl import Document, Keyword, Text


Expand All @@ -36,6 +38,23 @@ class Index:
}
}
dsl_index_result = (1, {}, '{"result": "created"}')
dsl_index_span_name = "Elasticsearch/test-index/_doc/2"
dsl_index_span_name = "Elasticsearch/test-index/_doc/:id"
dsl_index_url = "/test-index/_doc/2"
dsl_search_method = "POST"

perform_request_mock_path = (
"elastic_transport._node._http_urllib3.Urllib3HttpNode.perform_request"
)


def mock_response(body: str, status_code: int = 200):
return NodeApiResponse(
ApiResponseMeta(
status=status_code,
headers=HttpHeaders({}),
duration=100,
http_version="1.1",
node="node",
),
body.encode(),
)
Loading

0 comments on commit e56584b

Please sign in to comment.