Skip to content

Commit

Permalink
Merge branch 'main' into db-drivers-db-statement-comment-opt-in
Browse files Browse the repository at this point in the history
  • Loading branch information
tammy-baylis-swi authored Jan 10, 2025
2 parents 6969dbe + 3ebdb63 commit 0b5dda0
Show file tree
Hide file tree
Showing 16 changed files with 662 additions and 79 deletions.
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3100](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3100))
- Add support to database stability opt-in in `_semconv` utilities and add tests
([#3111](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3111))
- `opentelemetry-opentelemetry-pymongo` Add `py.typed` file to enable PEP 561
([#3136](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3136))
- `opentelemetry-opentelemetry-requests` Add `py.typed` file to enable PEP 561
([#3135](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3135))
- `opentelemetry-instrumentation-system-metrics` Add `py.typed` file to enable PEP 561
([#3132](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3132))
- `opentelemetry-opentelemetry-sqlite3` Add `py.typed` file to enable PEP 561
Expand All @@ -27,6 +31,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#3148](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3148))
- add support to Python 3.13
([#3134](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3134))
- `opentelemetry-util-http` Add `py.typed` file to enable PEP 561
([#3127](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3127))

### Fixed

Expand All @@ -37,6 +43,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `opentelemetry-instrumentation` Fix `get_dist_dependency_conflicts` if no distribution requires
([#3168](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3168))

### Breaking changes

- `opentelemetry-instrumentation-sqlalchemy` including sqlcomment in `db.statement` span attribute value is now opt-in
([#3112](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3112))

### Breaking changes

- `opentelemetry-instrumentation-dbapi` including sqlcomment in `db.statement` span attribute value is now opt-in
([#3115](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3115))


### Breaking changes

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ The Python auto-instrumentation libraries for [OpenTelemetry](https://openteleme
* [Installation](#installation)
* [Releasing](#releasing)
* [Releasing a package as `1.0` stable](#releasing-a-package-as-10-stable)
* [Semantic Convention status of instrumentations](#semantic-convention-status-of-instrumentations)
* [Contributing](#contributing)
* [Thanks to all the people who already contributed](#thanks-to-all-the-people-who-already-contributed)

Expand Down Expand Up @@ -100,7 +101,7 @@ To release a package as `1.0` stable, the package:

## Semantic Convention status of instrumentations

In our efforts to maintain optimal user experience and prevent breaking changes for transitioning into stable semantic conventions, OpenTelemetry Python is adopting the [semantic convention migration plan](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/http/migration-guide.md) for several instrumentations. Currently this plan is only being adopted for HTTP-related instrumentations, but will eventually cover all types. Please refer to the `semconv status` column of the [instrumentation README](instrumentation/README.md) of the current status of instrumentations' semantic conventions. The possible values are `experimental`, `stable` and `migration` referring to [status](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.31.0/specification/document-status.md#lifecycle-status) of that particular semantic convention. `Migration` refers to an instrumentation that currently supports the migration plan.
In our efforts to maintain optimal user experience and prevent breaking changes for transitioning into stable semantic conventions, OpenTelemetry Python is adopting the semantic convention migration plan for several instrumentations. Currently this plan is only being adopted for [HTTP-related instrumentations](https://github.com/open-telemetry/semantic-conventions/blob/main/docs/non-normative/http-migration.md), but will eventually cover all types. Please refer to the `semconv status` column of the [instrumentation README](instrumentation/README.md) of the current status of instrumentations' semantic conventions. The possible values are `experimental`, `stable` and `migration` referring to [status](https://github.com/open-telemetry/opentelemetry-specification/blob/v1.31.0/specification/document-status.md#lifecycle-status) of that particular semantic convention. `Migration` refers to an instrumentation that currently supports the migration plan.

## Contributing

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from asyncpg import Connection, Record, cursor
from wrapt import ObjectProxy

from opentelemetry import trace as trace_api
from opentelemetry.instrumentation.asyncpg import AsyncPGInstrumentor
from opentelemetry.test.test_base import TestBase

Expand Down Expand Up @@ -105,3 +106,36 @@ async def exec_mock(*args, **kwargs):
spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 2)
self.assertEqual([span.status.is_ok for span in spans], [True, True])

def test_no_op_tracer_provider(self):
AsyncPGInstrumentor().uninstrument()
AsyncPGInstrumentor().instrument(
tracer_provider=trace_api.NoOpTracerProvider()
)

# Mock out all interaction with postgres
async def bind_mock(*args, **kwargs):
return []

async def exec_mock(*args, **kwargs):
return [], None, True

conn = mock.Mock()
conn.is_closed = lambda: False

conn._protocol = mock.Mock()
conn._protocol.bind = bind_mock
conn._protocol.execute = exec_mock
conn._protocol.bind_execute = exec_mock
conn._protocol.close_portal = bind_mock

state = mock.Mock()
state.closed = False

# init the cursor and fetch a single record
crs = cursor.Cursor(conn, "SELECT * FROM test", state, [], Record)
asyncio.run(crs._init(1))
asyncio.run(crs.fetch(1))

spans = self.memory_exporter.get_finished_spans()
self.assertEqual(len(spans), 0)
Original file line number Diff line number Diff line change
Expand Up @@ -39,30 +39,49 @@
"""
# pylint: disable=no-value-for-parameter

from __future__ import annotations

import logging
from typing import Collection
from types import CodeType
from typing import Any, Callable, Collection, TypeVar

import jinja2
from jinja2.environment import Template
from wrapt import wrap_function_wrapper as _wrap

from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.jinja2.package import _instruments
from opentelemetry.instrumentation.jinja2.version import __version__
from opentelemetry.instrumentation.utils import unwrap
from opentelemetry.trace import SpanKind, get_tracer
from opentelemetry.trace import SpanKind, Tracer, get_tracer

logger = logging.getLogger(__name__)

ATTRIBUTE_JINJA2_TEMPLATE_NAME = "jinja2.template_name"
ATTRIBUTE_JINJA2_TEMPLATE_PATH = "jinja2.template_path"
DEFAULT_TEMPLATE_NAME = "<memory>"

R = TypeVar("R")


def _with_tracer_wrapper(func):
def _with_tracer_wrapper(
func: Callable[
[Tracer, Callable[..., R], Any, list[Any], dict[str, Any]], R
],
) -> Callable[
[Tracer], Callable[[Callable[..., R], Any, list[Any], dict[str, Any]], R]
]:
"""Helper for providing tracer for wrapper functions."""

def _with_tracer(tracer):
def wrapper(wrapped, instance, args, kwargs):
def _with_tracer(
tracer: Tracer,
) -> Callable[[Callable[..., R], Any, list[Any], dict[str, Any]], R]:
def wrapper(
wrapped: Callable[..., R],
instance: Any,
args: list[Any],
kwargs: dict[str, Any],
) -> R:
return func(tracer, wrapped, instance, args, kwargs)

return wrapper
Expand All @@ -71,7 +90,13 @@ def wrapper(wrapped, instance, args, kwargs):


@_with_tracer_wrapper
def _wrap_render(tracer, wrapped, instance, args, kwargs):
def _wrap_render(
tracer: Tracer,
wrapped: Callable[..., Any],
instance: Template,
args: list[Any],
kwargs: dict[str, Any],
):
"""Wrap `Template.render()` or `Template.generate()`"""
with tracer.start_as_current_span(
"jinja2.render",
Expand All @@ -84,7 +109,13 @@ def _wrap_render(tracer, wrapped, instance, args, kwargs):


@_with_tracer_wrapper
def _wrap_compile(tracer, wrapped, _, args, kwargs):
def _wrap_compile(
tracer: Tracer,
wrapped: Callable[..., CodeType],
_,
args: list[Any],
kwargs: dict[str, Any],
) -> CodeType:
with tracer.start_as_current_span(
"jinja2.compile",
kind=SpanKind.INTERNAL,
Expand All @@ -100,7 +131,13 @@ def _wrap_compile(tracer, wrapped, _, args, kwargs):


@_with_tracer_wrapper
def _wrap_load_template(tracer, wrapped, _, args, kwargs):
def _wrap_load_template(
tracer: Tracer,
wrapped: Callable[..., Template],
_,
args: list[Any],
kwargs: dict[str, Any],
) -> Template:
with tracer.start_as_current_span(
"jinja2.load",
kind=SpanKind.INTERNAL,
Expand Down Expand Up @@ -128,7 +165,7 @@ class Jinja2Instrumentor(BaseInstrumentor):
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments

def _instrument(self, **kwargs):
def _instrument(self, **kwargs: Any):
tracer_provider = kwargs.get("tracer_provider")
tracer = get_tracer(
__name__,
Expand All @@ -146,7 +183,7 @@ def _instrument(self, **kwargs):
_wrap_load_template(tracer),
)

def _uninstrument(self, **kwargs):
def _uninstrument(self, **kwargs: Any):
unwrap(jinja2.Template, "render")
unwrap(jinja2.Template, "generate")
unwrap(jinja2.Environment, "compile")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ def failed_hook(span, event):
"""

from __future__ import annotations

from logging import getLogger
from typing import Callable, Collection
from typing import Any, Callable, Collection, TypeVar

from pymongo import monitoring

Expand All @@ -88,7 +90,7 @@ def failed_hook(span, event):
from opentelemetry.instrumentation.pymongo.version import __version__
from opentelemetry.instrumentation.utils import is_instrumentation_enabled
from opentelemetry.semconv.trace import DbSystemValues, SpanAttributes
from opentelemetry.trace import SpanKind, get_tracer
from opentelemetry.trace import SpanKind, Tracer, get_tracer
from opentelemetry.trace.span import Span
from opentelemetry.trace.status import Status, StatusCode

Expand All @@ -98,14 +100,21 @@ def failed_hook(span, event):
ResponseHookT = Callable[[Span, monitoring.CommandSucceededEvent], None]
FailedHookT = Callable[[Span, monitoring.CommandFailedEvent], None]

CommandEvent = TypeVar(
"CommandEvent",
monitoring.CommandStartedEvent,
monitoring.CommandSucceededEvent,
monitoring.CommandFailedEvent,
)


def dummy_callback(span, event): ...
def dummy_callback(span: Span, event: CommandEvent): ...


class CommandTracer(monitoring.CommandListener):
def __init__(
self,
tracer,
tracer: Tracer,
request_hook: RequestHookT = dummy_callback,
response_hook: ResponseHookT = dummy_callback,
failed_hook: FailedHookT = dummy_callback,
Expand Down Expand Up @@ -195,10 +204,12 @@ def failed(self, event: monitoring.CommandFailedEvent):
_LOG.exception(hook_exception)
span.end()

def _pop_span(self, event):
def _pop_span(self, event: CommandEvent) -> Span | None:
return self._span_dict.pop(_get_span_dict_key(event), None)

def _get_statement_by_command_name(self, command_name, event):
def _get_statement_by_command_name(
self, command_name: str, event: CommandEvent
) -> str:
statement = command_name
command_attribute = COMMAND_TO_ATTRIBUTE_MAPPING.get(command_name)
command = event.command.get(command_attribute)
Expand All @@ -207,14 +218,16 @@ def _get_statement_by_command_name(self, command_name, event):
return statement


def _get_span_dict_key(event):
def _get_span_dict_key(
event: CommandEvent,
) -> int | tuple[int, tuple[str, int | None]]:
if event.connection_id is not None:
return event.request_id, event.connection_id
return event.request_id


class PymongoInstrumentor(BaseInstrumentor):
_commandtracer_instance = None # type CommandTracer
_commandtracer_instance: CommandTracer | None = None
# The instrumentation for PyMongo is based on the event listener interface
# https://api.mongodb.com/python/current/api/pymongo/monitoring.html.
# This interface only allows to register listeners and does not provide
Expand All @@ -225,7 +238,7 @@ class PymongoInstrumentor(BaseInstrumentor):
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments

def _instrument(self, **kwargs):
def _instrument(self, **kwargs: Any):
"""Integrate with pymongo to trace it using event listener.
https://api.mongodb.com/python/current/api/pymongo/monitoring.html
Expand Down Expand Up @@ -259,6 +272,6 @@ def _instrument(self, **kwargs):
# If already created, just enable it
self._commandtracer_instance.is_enabled = True

def _uninstrument(self, **kwargs):
def _uninstrument(self, **kwargs: Any):
if self._commandtracer_instance is not None:
self._commandtracer_instance.is_enabled = False
Original file line number Diff line number Diff line change
Expand Up @@ -72,10 +72,12 @@ def response_hook(span, request_obj, response)
---
"""

from __future__ import annotations

import functools
import types
from timeit import default_timer
from typing import Callable, Collection, Optional
from typing import Any, Callable, Collection, Optional
from urllib.parse import urlparse

from requests.models import PreparedRequest, Response
Expand Down Expand Up @@ -146,7 +148,7 @@ def _instrument(
duration_histogram_new: Histogram,
request_hook: _RequestHookT = None,
response_hook: _ResponseHookT = None,
excluded_urls: ExcludeList = None,
excluded_urls: ExcludeList | None = None,
sem_conv_opt_in_mode: _StabilityMode = _StabilityMode.DEFAULT,
):
"""Enables tracing of all requests calls that go through
Expand All @@ -164,7 +166,9 @@ def _instrument(

# pylint: disable-msg=too-many-locals,too-many-branches
@functools.wraps(wrapped_send)
def instrumented_send(self, request, **kwargs):
def instrumented_send(
self: Session, request: PreparedRequest, **kwargs: Any
):
if excluded_urls and excluded_urls.url_disabled(request.url):
return wrapped_send(self, request, **kwargs)

Expand Down Expand Up @@ -345,7 +349,7 @@ def _uninstrument():
_uninstrument_from(Session)


def _uninstrument_from(instr_root, restore_as_bound_func=False):
def _uninstrument_from(instr_root, restore_as_bound_func: bool = False):
for instr_func_name in ("request", "send"):
instr_func = getattr(instr_root, instr_func_name)
if not getattr(
Expand All @@ -361,7 +365,7 @@ def _uninstrument_from(instr_root, restore_as_bound_func=False):
setattr(instr_root, instr_func_name, original)


def get_default_span_name(method):
def get_default_span_name(method: str) -> str:
"""
Default implementation for name_callback, returns HTTP {method_name}.
https://opentelemetry.io/docs/reference/specification/trace/semantic_conventions/http/#name
Expand All @@ -385,7 +389,7 @@ class RequestsInstrumentor(BaseInstrumentor):
def instrumentation_dependencies(self) -> Collection[str]:
return _instruments

def _instrument(self, **kwargs):
def _instrument(self, **kwargs: Any):
"""Instruments requests module
Args:
Expand Down Expand Up @@ -443,10 +447,10 @@ def _instrument(self, **kwargs):
sem_conv_opt_in_mode=semconv_opt_in_mode,
)

def _uninstrument(self, **kwargs):
def _uninstrument(self, **kwargs: Any):
_uninstrument()

@staticmethod
def uninstrument_session(session):
def uninstrument_session(session: Session):
"""Disables instrumentation on the session object."""
_uninstrument_from(session, restore_as_bound_func=True)
Loading

0 comments on commit 0b5dda0

Please sign in to comment.