Skip to content

Commit 05d8402

Browse files
authored
Merge pull request #790 from DataDog/0.20-dev
Merge 0.20-dev
2 parents e8b1070 + 18518d6 commit 05d8402

File tree

21 files changed

+493
-15
lines changed

21 files changed

+493
-15
lines changed

.circleci/config.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,20 @@ jobs:
190190
- test_utils.results
191191
- *save_cache_step
192192

193+
test_logging:
194+
docker:
195+
- *test_runner
196+
resource_class: *resource_class
197+
steps:
198+
- checkout
199+
- *restore_cache_step
200+
- run: tox -e '{py27,py34,py35,py36}-test_logging' --result-json /tmp/test_logging.results
201+
- persist_to_workspace:
202+
root: /tmp
203+
paths:
204+
- test_logging.results
205+
- *save_cache_step
206+
193207
asyncio:
194208
docker:
195209
- *test_runner
@@ -884,6 +898,20 @@ jobs:
884898
- jinja2.results
885899
- *save_cache_step
886900

901+
mako:
902+
docker:
903+
- *test_runner
904+
resource_class: *resource_class
905+
steps:
906+
- checkout
907+
- *restore_cache_step
908+
- run: tox -e 'mako_contrib-{py27,py34,py35,py36}-mako{010,100}' --result-json /tmp/mako.results
909+
- persist_to_workspace:
910+
root: /tmp
911+
paths:
912+
- mako.results
913+
- *save_cache_step
914+
887915
build_docs:
888916
# deploy official documentation
889917
docker:
@@ -1018,6 +1046,9 @@ workflows:
10181046
- kombu:
10191047
requires:
10201048
- flake8
1049+
- mako:
1050+
requires:
1051+
- flake8
10211052
- molten:
10221053
requires:
10231054
- flake8
@@ -1081,6 +1112,9 @@ workflows:
10811112
- test_utils:
10821113
requires:
10831114
- flake8
1115+
- test_logging:
1116+
requires:
1117+
- flake8
10841118
- tornado:
10851119
requires:
10861120
- flake8
@@ -1122,6 +1156,7 @@ workflows:
11221156
- integration
11231157
- jinja2
11241158
- kombu
1159+
- mako
11251160
- molten
11261161
- mongoengine
11271162
- msgpack
@@ -1143,6 +1178,7 @@ workflows:
11431178
- sqlalchemy
11441179
- sqlite3
11451180
- test_utils
1181+
- test_logging
11461182
- tornado
11471183
- tracer
11481184
- unit_tests

ddtrace/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from .tracer import Tracer
55
from .settings import config
66

7-
__version__ = '0.19.0'
7+
__version__ = '0.20.0'
88

99
# a global tracer instance with integration settings
1010
tracer = Tracer()

ddtrace/bootstrap/sitecustomize.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,25 @@
88
import sys
99
import logging
1010

11-
from ddtrace.utils.formats import asbool
11+
from ddtrace.utils.formats import asbool, get_env
1212

13+
logs_injection = asbool(get_env('logs', 'injection'))
14+
DD_LOG_FORMAT = '%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] {}- %(message)s'.format(
15+
'[dd.trace_id=%(dd.trace_id)s dd.span_id=%(dd.span_id)s] ' if logs_injection else ''
16+
)
1317

1418
debug = os.environ.get("DATADOG_TRACE_DEBUG")
19+
20+
# Set here a default logging format for basicConfig
21+
22+
# DEV: Once basicConfig is called here, future calls to it cannot be used to
23+
# change the formatter since it applies the formatter to the root handler only
24+
# upon initializing it the first time.
25+
# See https://github.com/python/cpython/blob/112e4afd582515fcdcc0cde5012a4866e5cfda12/Lib/logging/__init__.py#L1550
1526
if debug and debug.lower() == "true":
16-
logging.basicConfig(level=logging.DEBUG)
27+
logging.basicConfig(level=logging.DEBUG, format=DD_LOG_FORMAT)
1728
else:
18-
logging.basicConfig()
29+
logging.basicConfig(format=DD_LOG_FORMAT)
1930

2031
log = logging.getLogger(__name__)
2132

@@ -82,6 +93,9 @@ def add_global_tags(tracer):
8293
if opts:
8394
tracer.configure(**opts)
8495

96+
if logs_injection:
97+
EXTRA_PATCHED_MODULES.update({'logging': True})
98+
8599
if patch:
86100
update_patched_modules()
87101
from ddtrace import patch_all; patch_all(**EXTRA_PATCHED_MODULES) # noqa
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""
2+
Datadog APM traces can be integrated with Logs by first having the tracing
3+
library patch the standard library ``logging`` module and updating the log
4+
formatter used by an application. This feature enables you to inject the current
5+
trace information into a log entry.
6+
7+
Before the trace information can be injected into logs, the formatter has to be
8+
updated to include ``dd.trace_id`` and ``dd.span_id`` attributes from the log
9+
record. The integration with Logs occurs as long as the log entry includes
10+
``dd.trace_id=%(dd.trace_id)s`` and ``dd.span_id=%(dd.span_id)s``.
11+
12+
ddtrace-run
13+
-----------
14+
15+
When using ``ddtrace-run``, enable patching by setting the environment variable
16+
``DD_LOGS_INJECTION=true``. The logger by default will have a format that
17+
includes trace information::
18+
19+
import logging
20+
from ddtrace import tracer
21+
22+
log = logging.getLogger()
23+
log.level = logging.INFO
24+
25+
26+
@tracer.wrap()
27+
def hello():
28+
log.info('Hello, World!')
29+
30+
hello()
31+
32+
Manual Instrumentation
33+
----------------------
34+
35+
If you prefer to instrument manually, patch the logging library then update the
36+
log formatter as in the following example::
37+
38+
from ddtrace import patch_all; patch_all(logging=True)
39+
import logging
40+
from ddtrace import tracer
41+
42+
FORMAT = ('%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] '
43+
'[dd.trace_id=%(dd.trace_id)s dd.span_id=%(dd.span_id)s] '
44+
'- %(message)s')
45+
logging.basicConfig(format=FORMAT)
46+
log = logging.getLogger()
47+
log.level = logging.INFO
48+
49+
50+
@tracer.wrap()
51+
def hello():
52+
log.info('Hello, World!')
53+
54+
hello()
55+
"""
56+
57+
from ...utils.importlib import require_modules
58+
59+
60+
required_modules = ['logging']
61+
62+
with require_modules(required_modules) as missing_modules:
63+
if not missing_modules:
64+
from .patch import patch, unpatch
65+
66+
__all__ = ['patch', 'unpatch']

ddtrace/contrib/logging/patch.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import logging
2+
from wrapt import wrap_function_wrapper as _w
3+
4+
from ddtrace import config
5+
6+
from ...helpers import get_correlation_ids
7+
from ...utils.wrappers import unwrap as _u
8+
9+
RECORD_ATTR_TRACE_ID = 'dd.trace_id'
10+
RECORD_ATTR_SPAN_ID = 'dd.span_id'
11+
RECORD_ATTR_VALUE_NULL = 0
12+
13+
config._add('logging', dict(
14+
tracer=None, # by default, override here for custom tracer
15+
))
16+
17+
18+
def _w_makeRecord(func, instance, args, kwargs):
19+
record = func(*args, **kwargs)
20+
21+
# add correlation identifiers to LogRecord
22+
trace_id, span_id = get_correlation_ids(tracer=config.logging.tracer)
23+
if trace_id and span_id:
24+
setattr(record, RECORD_ATTR_TRACE_ID, trace_id)
25+
setattr(record, RECORD_ATTR_SPAN_ID, span_id)
26+
else:
27+
setattr(record, RECORD_ATTR_TRACE_ID, RECORD_ATTR_VALUE_NULL)
28+
setattr(record, RECORD_ATTR_SPAN_ID, RECORD_ATTR_VALUE_NULL)
29+
30+
return record
31+
32+
33+
def patch():
34+
"""
35+
Patch ``logging`` module in the Python Standard Library for injection of
36+
tracer information by wrapping the base factory method ``Logger.makeRecord``
37+
"""
38+
if getattr(logging, '_datadog_patch', False):
39+
return
40+
setattr(logging, '_datadog_patch', True)
41+
42+
_w(logging.Logger, 'makeRecord', _w_makeRecord)
43+
44+
45+
def unpatch():
46+
if getattr(logging, '_datadog_patch', False):
47+
setattr(logging, '_datadog_patch', False)
48+
49+
_u(logging.Logger, 'makeRecord')

ddtrace/contrib/mako/__init__.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""
2+
The ``mako`` integration traces templates rendering.
3+
Auto instrumentation is available using the ``patch``. The following is an example::
4+
5+
from ddtrace import patch
6+
from mako.template import Template
7+
8+
patch(mako=True)
9+
10+
t = Template(filename="index.html")
11+
12+
"""
13+
from ..util import require_modules
14+
15+
required_modules = ['mako']
16+
17+
with require_modules(required_modules) as missing_modules:
18+
if not missing_modules:
19+
from .patch import patch, unpatch
20+
21+
__all__ = [
22+
'patch',
23+
'unpatch',
24+
]

ddtrace/contrib/mako/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
DEFAULT_TEMPLATE_NAME = '<memory>'

ddtrace/contrib/mako/patch.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import mako
2+
from mako.template import Template
3+
from wrapt import wrap_function_wrapper as _w
4+
5+
from ...ext import http
6+
from ...pin import Pin
7+
from ...utils.importlib import func_name
8+
from ...utils.wrappers import unwrap as _u
9+
from .constants import DEFAULT_TEMPLATE_NAME
10+
11+
12+
def patch():
13+
if getattr(mako, '__datadog_patch', False):
14+
# already patched
15+
return
16+
setattr(mako, '__datadog_patch', True)
17+
18+
Pin(service='mako', app='mako', app_type=http.TEMPLATE).onto(Template)
19+
20+
_w(mako, 'template.Template.render', _wrap_render)
21+
_w(mako, 'template.Template.render_unicode', _wrap_render)
22+
_w(mako, 'template.Template.render_context', _wrap_render)
23+
24+
25+
def unpatch():
26+
if not getattr(mako, '__datadog_patch', False):
27+
return
28+
setattr(mako, '__datadog_patch', False)
29+
30+
_u(mako.template.Template, 'render')
31+
_u(mako.template.Template, 'render_unicode')
32+
_u(mako.template.Template, 'render_context')
33+
34+
35+
def _wrap_render(wrapped, instance, args, kwargs):
36+
pin = Pin.get_from(instance)
37+
if not pin or not pin.enabled():
38+
return wrapped(*args, **kwargs)
39+
40+
template_name = instance.filename or DEFAULT_TEMPLATE_NAME
41+
with pin.tracer.trace(func_name(wrapped), pin.service, span_type=http.TEMPLATE) as span:
42+
try:
43+
template = wrapped(*args, **kwargs)
44+
return template
45+
finally:
46+
span.resource = template_name
47+
span.set_tag('mako.template_name', template_name)

ddtrace/helpers.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ddtrace
22

33

4-
def get_correlation_ids():
4+
def get_correlation_ids(tracer=None):
55
"""Retrieves the Correlation Identifiers for the current active ``Trace``.
66
This helper method can be achieved manually and should be considered
77
only a shortcut. The main reason is to abstract the current ``Tracer``
@@ -11,9 +11,9 @@ def get_correlation_ids():
1111
OpenTracing users can still extract these values using the ``ScopeManager``
1212
API, though this shortcut is a simple one-liner. The usage is:
1313
14-
from ddtrace import correlation
14+
from ddtrace import helpers
1515
16-
trace_id, span_id = correlation.get_correlation_ids()
16+
trace_id, span_id = helpers.get_correlation_ids()
1717
1818
:returns: a tuple containing the trace_id and span_id
1919
"""
@@ -22,7 +22,15 @@ def get_correlation_ids():
2222
# and we're doing the same here for ``ddtrace.tracer``. Because this helper
2323
# must work also with OpenTracing, we should take the right used ``Tracer``.
2424
# At the time of writing, it's enough to support our Datadog Tracer.
25-
tracer = ddtrace.tracer
25+
26+
# If no tracer passed in, use global tracer
27+
if not tracer:
28+
tracer = ddtrace.tracer
29+
30+
# If tracer is disabled, skip
31+
if not tracer.enabled:
32+
return None, None
33+
2634
span = tracer.current_span()
2735
if span is None:
2836
return None, None

ddtrace/monkey.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
'vertica': True,
4747
'molten': True,
4848
'jinja2': True,
49+
'mako': True,
4950
'flask': True,
5051
'kombu': False,
5152

@@ -54,6 +55,9 @@
5455
"falcon": False,
5556
"pylons": False,
5657
"pyramid": False,
58+
59+
# Standard library modules off by default
60+
'logging': False,
5761
}
5862

5963
_LOCK = threading.Lock()

0 commit comments

Comments
 (0)