Skip to content

Port requests #23

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions instrumentors/requests/setup.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# 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.
#
[metadata]
name = opentelemetry-ext-requests
description = Requests tracing for OpenTelemetry (based on opentelemetry-ext-wsgi)
long_description = file: README.rst
long_description_content_type = text/x-rst
author = OpenTelemetry Authors
author_email = [email protected]
url = https://github.com/open-telemetry/opentelemetry-auto-instr-python/instrumentors/requests/
platforms = any
license = Apache-2.0
classifiers =
Development Status :: 4 - Beta
Intended Audience :: Developers
License :: OSI Approved :: Apache Software License
Programming Language :: Python
Programming Language :: Python :: 3
Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8

[options]
python_requires = >=3.4
package_dir=
=src
packages=find_namespace:
install_requires =
opentelemetry-api
opentelemetry-auto_instrumentation
opentelemetry-ext-wsgi
requests ~= 2.0
ddtrace @ git+ssh://[email protected]/DataDog/dd-trace-py.git@1e87c9bdf7769032982349c4ccc0e1c2e6866a16

[options.extras_require]
test =
opentelemetry-sdk

[options.packages.find]
where = src
33 changes: 33 additions & 0 deletions instrumentors/requests/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# 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 os

import setuptools

BASE_DIR = os.path.dirname(__file__)
VERSION_FILENAME = os.path.join(
BASE_DIR, "src", "opentelemetry", "ext", "requests", "version.py"
)
PACKAGE_INFO = {}
with open(VERSION_FILENAME) as f:
exec(f.read(), PACKAGE_INFO)

setuptools.setup(
version=PACKAGE_INFO["__version__"],
entry_points={
"opentelemetry_instrumentor": [
"requests = opentelemetry.ext.requests:RequestsInstrumentor"
]
},
)
56 changes: 56 additions & 0 deletions instrumentors/requests/src/opentelemetry/ext/requests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"""
The ``requests`` integration traces all HTTP calls to internal or external services.
Auto instrumentation is available using the ``patch`` function that **must be called
before** importing the ``requests`` library. The following is an example::

from ddtrace import patch
patch(requests=True)

import requests
requests.get("https://www.datadoghq.com")

If you would prefer finer grained control, use a ``TracedSession`` object as you would a
``requests.Session``::

from ddtrace.contrib.requests import TracedSession

session = TracedSession()
session.get("https://www.datadoghq.com")

The library can be configured globally and per instance, using the Configuration API::

from ddtrace import config

# disable distributed tracing globally
config.requests['distributed_tracing'] = False

# enable trace analytics globally
config.requests['analytics_enabled'] = True

# change the service name/distributed tracing only for this session
session = Session()
cfg = config.get_from(session)
cfg['service_name'] = 'auth-api'
cfg['analytics_enabled'] = True

:ref:`Headers tracing <http-headers-tracing>` is supported for this integration.
"""

from opentelemetry.auto_instrumentation.instrumentor import BaseInstrumentor

from .patch import patch, unpatch

class RequestsInstrumentor(BaseInstrumentor):
"""A instrumentor for flask.Flask

See `BaseInstrumentor`
"""

def __init__(self):
super().__init__()

def _instrument(self):
patch()

def _uninstrument(self):
unpatch()
112 changes: 112 additions & 0 deletions instrumentors/requests/src/opentelemetry/ext/requests/connection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import ddtrace
from ddtrace import config
from ddtrace.http import store_request_headers, store_response_headers

from ddtrace.compat import parse
from ddtrace.constants import ANALYTICS_SAMPLE_RATE_KEY
from ddtrace.ext import SpanTypes, http
from ddtrace.internal.logger import get_logger
from ddtrace.propagation.http import HTTPPropagator
from .constants import DEFAULT_SERVICE

log = get_logger(__name__)

# opentelemetry related
from opentelemetry import context, propagators, trace
from opentelemetry.trace.status import StatusCanonicalCode
from opentelemetry.ext.requests.version import __version__

def _extract_service_name(session, span, hostname=None):
"""Extracts the right service name based on the following logic:
- `requests` is the default service name
- users can change it via `session.service_name = 'clients'`
- if the Span doesn't have a parent, use the set service name or fallback to the default
parent service value if the set service name is the default
- if `split_by_domain` is used, always override users settings
and use the network location as a service name

The priority can be represented as:
Updated service name > parent service name > default to `requests`.
"""
cfg = config.get_from(session)
if cfg["split_by_domain"] and hostname:
return hostname

return cfg["service_name"]

def _wrap_send(func, instance, args, kwargs):
"""Trace the `Session.send` instance method"""
# avoid instrumenting request towards an exporter
if context.get_value("suppress_instrumentation"):
return func(*args, **kwargs)

tracer = trace.get_tracer(__name__, __version__)

request = kwargs.get("request") or args[0]
if not request:
return func(*args, **kwargs)

# sanitize url of query
parsed_uri = parse.urlparse(request.url)
hostname = parsed_uri.hostname
if parsed_uri.port:
hostname = "{}:{}".format(hostname, parsed_uri.port)
sanitized_url = parse.urlunparse(
(
parsed_uri.scheme,
parsed_uri.netloc,
parsed_uri.path,
parsed_uri.params,
None, # drop parsed_uri.query
parsed_uri.fragment,
)
)

with tracer.start_as_current_span("requests.request", kind=trace.SpanKind.CLIENT) as span:
# update the span service name before doing any action
span.set_attribute("service", _extract_service_name(instance, span, hostname=hostname))

# Configure trace search sample rate
# DEV: analytics enabled on per-session basis
cfg = config.get_from(instance)
analytics_enabled = cfg.get("analytics_enabled")
if analytics_enabled:
span.set_attribute(ANALYTICS_SAMPLE_RATE_KEY, cfg.get("analytics_sample_rate", True))

# propagate distributed tracing headers
if cfg.get("distributed_tracing"):
propagators.inject(type(request.headers).__setitem__, request.headers)

# TODO(Mauricio): Re-enable it
# Storing request headers in the span
#store_request_headers(request.headers, span, config.requests)

response = None
try:
response = func(*args, **kwargs)

# TODO(Mauricio): Reenable it
# Storing response headers in the span. Note that response.headers is not a dict, but an iterable
# requests custom structure, that we convert to a dict
#if hasattr(response, "headers"):
# store_response_headers(dict(response.headers), span, config.requests)
return response
finally:
try:
span.set_attribute(http.METHOD, request.method.upper())
span.set_attribute(http.URL, sanitized_url)
if config.requests.trace_query_string:
span.set_attribute(http.QUERY_STRING, parsed_uri.query)
if response is not None:
span.set_attribute(http.STATUS_CODE, response.status_code)
if 400 <= response.status_code:
status = trace.Status(StatusCanonicalCode.UNKNOWN, str(response.status_code))
span.set_status(status)
# Storing response headers in the span.
# Note that response.headers is not a dict, but an iterable
# requests custom structure, that we convert to a dict
# TODO(Mauricio): renable it
#response_headers = dict(getattr(response, "headers", {}))
#store_response_headers(response_headers, span, config.requests)
except Exception:
log.debug("requests: error adding tags", exc_info=True)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DEFAULT_SERVICE = 'requests'
40 changes: 40 additions & 0 deletions instrumentors/requests/src/opentelemetry/ext/requests/patch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import requests

from ddtrace.vendor.wrapt import wrap_function_wrapper as _w

from ddtrace import config

from ddtrace.pin import Pin
from ddtrace.utils.formats import asbool, get_env
from ddtrace.utils.wrappers import unwrap as _u
from .constants import DEFAULT_SERVICE
from .connection import _wrap_send

# requests default settings
config._add('requests', {
'service_name': get_env('requests', 'service_name', DEFAULT_SERVICE),
'distributed_tracing': asbool(get_env('requests', 'distributed_tracing', True)),
'split_by_domain': asbool(get_env('requests', 'split_by_domain', False)),
})


def patch():
"""Activate http calls tracing"""
if getattr(requests, '__datadog_patch', False):
return
setattr(requests, '__datadog_patch', True)

_w('requests', 'Session.send', _wrap_send)
Pin(
service=config.requests['service_name'],
app='requests',
_config=config.requests,
).onto(requests.Session)

def unpatch():
"""Disable traced sessions"""
if not getattr(requests, '__datadog_patch', False):
return
setattr(requests, '__datadog_patch', False)

_u(requests.Session, 'send')
17 changes: 17 additions & 0 deletions instrumentors/requests/src/opentelemetry/ext/requests/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import requests

from ddtrace.vendor.wrapt import wrap_function_wrapper as _w

from .connection import _wrap_send


class TracedSession(requests.Session):
"""TracedSession is a requests' Session that is already traced.
You can use it if you want a finer grained control for your
HTTP clients.
"""
pass


# always patch our `TracedSession` when imported
_w(TracedSession, 'send', _wrap_send)
15 changes: 15 additions & 0 deletions instrumentors/requests/src/opentelemetry/ext/requests/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 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.

__version__ = "0.7.dev0"
Empty file.
Loading