Merge in latest from datadog/dd-trace-py (#1)
This commit is contained in:
		
							parent
							
								
									67f43ea6ab
								
							
						
					
					
						commit
						f13c716af6
					
				
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							|  | @ -0,0 +1 @@ | ||||||
|  | * @DataDog/apm-python | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | Thanks for taking the time for reporting an issue! | ||||||
|  | 
 | ||||||
|  | Before reporting an issue on dd-trace-py, please be sure to provide all | ||||||
|  | necessary information. | ||||||
|  | 
 | ||||||
|  | If you're hitting a bug, make sure that you're using the latest version of this | ||||||
|  | library. | ||||||
|  | 
 | ||||||
|  | ### Which version of dd-trace-py are you using? | ||||||
|  | 
 | ||||||
|  | ### Which version of the libraries are you using? | ||||||
|  | 
 | ||||||
|  | You can copy/paste the output of `pip freeze` here. | ||||||
|  | 
 | ||||||
|  | ### How can we reproduce your problem? | ||||||
|  | 
 | ||||||
|  | ### What is the result that you get? | ||||||
|  | 
 | ||||||
|  | ### What is result that you expected? | ||||||
|  | @ -1,5 +1,7 @@ | ||||||
|  | # Byte-compiled / optimized / DLL files | ||||||
|  | __pycache__/ | ||||||
| *.py[cod] | *.py[cod] | ||||||
| *.sw[op] | *$py.class | ||||||
| 
 | 
 | ||||||
| # C extensions | # C extensions | ||||||
| *.so | *.so | ||||||
|  | @ -18,7 +20,6 @@ develop-eggs | ||||||
| .installed.cfg | .installed.cfg | ||||||
| lib | lib | ||||||
| lib64 | lib64 | ||||||
| __pycache__ |  | ||||||
| venv*/ | venv*/ | ||||||
| 
 | 
 | ||||||
| # Installer logs | # Installer logs | ||||||
|  | @ -55,3 +56,96 @@ _build/ | ||||||
| # mypy | # mypy | ||||||
| .mypy_cache/ | .mypy_cache/ | ||||||
| target | target | ||||||
|  | ======= | ||||||
|  | # Distribution / packaging | ||||||
|  | .Python | ||||||
|  | env/ | ||||||
|  | build/ | ||||||
|  | develop-eggs/ | ||||||
|  | dist/ | ||||||
|  | downloads/ | ||||||
|  | eggs/ | ||||||
|  | .eggs/ | ||||||
|  | lib64/ | ||||||
|  | parts/ | ||||||
|  | sdist/ | ||||||
|  | var/ | ||||||
|  | *.egg-info/ | ||||||
|  | .installed.cfg | ||||||
|  | *.egg | ||||||
|  | *.whl | ||||||
|  | 
 | ||||||
|  | # PyInstaller | ||||||
|  | #  Usually these files are written by a python script from a template | ||||||
|  | #  before PyInstaller builds the exe, so as to inject date/other infos into it. | ||||||
|  | *.manifest | ||||||
|  | *.spec | ||||||
|  | 
 | ||||||
|  | # Installer logs | ||||||
|  | pip-log.txt | ||||||
|  | pip-delete-this-directory.txt | ||||||
|  | 
 | ||||||
|  | # Unit test / coverage reports | ||||||
|  | htmlcov/ | ||||||
|  | .tox/ | ||||||
|  | .ddtox/ | ||||||
|  | .coverage | ||||||
|  | .coverage.* | ||||||
|  | .cache | ||||||
|  | coverage.xml | ||||||
|  | *,cover | ||||||
|  | .hypothesis/ | ||||||
|  | .pytest_cache/ | ||||||
|  | 
 | ||||||
|  | # Translations | ||||||
|  | *.mo | ||||||
|  | *.pot | ||||||
|  | 
 | ||||||
|  | # Django stuff: | ||||||
|  | *.log | ||||||
|  | local_settings.py | ||||||
|  | 
 | ||||||
|  | # Flask stuff: | ||||||
|  | instance/ | ||||||
|  | .webassets-cache | ||||||
|  | 
 | ||||||
|  | # Scrapy stuff: | ||||||
|  | .scrapy | ||||||
|  | 
 | ||||||
|  | # Sphinx documentation | ||||||
|  | docs/_build/ | ||||||
|  | 
 | ||||||
|  | # PyBuilder | ||||||
|  | target/ | ||||||
|  | 
 | ||||||
|  | # IPython Notebook | ||||||
|  | .ipynb_checkpoints | ||||||
|  | 
 | ||||||
|  | # pyenv | ||||||
|  | .python-version | ||||||
|  | 
 | ||||||
|  | # celery beat schedule file | ||||||
|  | celerybeat-schedule | ||||||
|  | 
 | ||||||
|  | # docker-compose env file | ||||||
|  | # it must be versioned to keep track of backing services defaults | ||||||
|  | !.env | ||||||
|  | 
 | ||||||
|  | # virtualenv | ||||||
|  | venv/ | ||||||
|  | ENV/ | ||||||
|  | 
 | ||||||
|  | # Spyder project settings | ||||||
|  | .spyderproject | ||||||
|  | 
 | ||||||
|  | # Rope project settings | ||||||
|  | .ropeproject | ||||||
|  | 
 | ||||||
|  | # Vim | ||||||
|  | *.swp | ||||||
|  | # IDEA | ||||||
|  | .idea/ | ||||||
|  | 
 | ||||||
|  | # VS Code | ||||||
|  | .vscode/ | ||||||
|  | >>>>>>> dd/master | ||||||
|  |  | ||||||
|  | @ -0,0 +1,200 @@ | ||||||
|  |                                  Apache License | ||||||
|  |                            Version 2.0, January 2004 | ||||||
|  |                         http://www.apache.org/licenses/ | ||||||
|  | 
 | ||||||
|  |    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION | ||||||
|  | 
 | ||||||
|  |    1. Definitions. | ||||||
|  | 
 | ||||||
|  |       "License" shall mean the terms and conditions for use, reproduction, | ||||||
|  |       and distribution as defined by Sections 1 through 9 of this document. | ||||||
|  | 
 | ||||||
|  |       "Licensor" shall mean the copyright owner or entity authorized by | ||||||
|  |       the copyright owner that is granting the License. | ||||||
|  | 
 | ||||||
|  |       "Legal Entity" shall mean the union of the acting entity and all | ||||||
|  |       other entities that control, are controlled by, or are under common | ||||||
|  |       control with that entity. For the purposes of this definition, | ||||||
|  |       "control" means (i) the power, direct or indirect, to cause the | ||||||
|  |       direction or management of such entity, whether by contract or | ||||||
|  |       otherwise, or (ii) ownership of fifty percent (50%) or more of the | ||||||
|  |       outstanding shares, or (iii) beneficial ownership of such entity. | ||||||
|  | 
 | ||||||
|  |       "You" (or "Your") shall mean an individual or Legal Entity | ||||||
|  |       exercising permissions granted by this License. | ||||||
|  | 
 | ||||||
|  |       "Source" form shall mean the preferred form for making modifications, | ||||||
|  |       including but not limited to software source code, documentation | ||||||
|  |       source, and configuration files. | ||||||
|  | 
 | ||||||
|  |       "Object" form shall mean any form resulting from mechanical | ||||||
|  |       transformation or translation of a Source form, including but | ||||||
|  |       not limited to compiled object code, generated documentation, | ||||||
|  |       and conversions to other media types. | ||||||
|  | 
 | ||||||
|  |       "Work" shall mean the work of authorship, whether in Source or | ||||||
|  |       Object form, made available under the License, as indicated by a | ||||||
|  |       copyright notice that is included in or attached to the work | ||||||
|  |       (an example is provided in the Appendix below). | ||||||
|  | 
 | ||||||
|  |       "Derivative Works" shall mean any work, whether in Source or Object | ||||||
|  |       form, that is based on (or derived from) the Work and for which the | ||||||
|  |       editorial revisions, annotations, elaborations, or other modifications | ||||||
|  |       represent, as a whole, an original work of authorship. For the purposes | ||||||
|  |       of this License, Derivative Works shall not include works that remain | ||||||
|  |       separable from, or merely link (or bind by name) to the interfaces of, | ||||||
|  |       the Work and Derivative Works thereof. | ||||||
|  | 
 | ||||||
|  |       "Contribution" shall mean any work of authorship, including | ||||||
|  |       the original version of the Work and any modifications or additions | ||||||
|  |       to that Work or Derivative Works thereof, that is intentionally | ||||||
|  |       submitted to Licensor for inclusion in the Work by the copyright owner | ||||||
|  |       or by an individual or Legal Entity authorized to submit on behalf of | ||||||
|  |       the copyright owner. For the purposes of this definition, "submitted" | ||||||
|  |       means any form of electronic, verbal, or written communication sent | ||||||
|  |       to the Licensor or its representatives, including but not limited to | ||||||
|  |       communication on electronic mailing lists, source code control systems, | ||||||
|  |       and issue tracking systems that are managed by, or on behalf of, the | ||||||
|  |       Licensor for the purpose of discussing and improving the Work, but | ||||||
|  |       excluding communication that is conspicuously marked or otherwise | ||||||
|  |       designated in writing by the copyright owner as "Not a Contribution." | ||||||
|  | 
 | ||||||
|  |       "Contributor" shall mean Licensor and any individual or Legal Entity | ||||||
|  |       on behalf of whom a Contribution has been received by Licensor and | ||||||
|  |       subsequently incorporated within the Work. | ||||||
|  | 
 | ||||||
|  |    2. Grant of Copyright License. Subject to the terms and conditions of | ||||||
|  |       this License, each Contributor hereby grants to You a perpetual, | ||||||
|  |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||||
|  |       copyright license to reproduce, prepare Derivative Works of, | ||||||
|  |       publicly display, publicly perform, sublicense, and distribute the | ||||||
|  |       Work and such Derivative Works in Source or Object form. | ||||||
|  | 
 | ||||||
|  |    3. Grant of Patent License. Subject to the terms and conditions of | ||||||
|  |       this License, each Contributor hereby grants to You a perpetual, | ||||||
|  |       worldwide, non-exclusive, no-charge, royalty-free, irrevocable | ||||||
|  |       (except as stated in this section) patent license to make, have made, | ||||||
|  |       use, offer to sell, sell, import, and otherwise transfer the Work, | ||||||
|  |       where such license applies only to those patent claims licensable | ||||||
|  |       by such Contributor that are necessarily infringed by their | ||||||
|  |       Contribution(s) alone or by combination of their Contribution(s) | ||||||
|  |       with the Work to which such Contribution(s) was submitted. If You | ||||||
|  |       institute patent litigation against any entity (including a | ||||||
|  |       cross-claim or counterclaim in a lawsuit) alleging that the Work | ||||||
|  |       or a Contribution incorporated within the Work constitutes direct | ||||||
|  |       or contributory patent infringement, then any patent licenses | ||||||
|  |       granted to You under this License for that Work shall terminate | ||||||
|  |       as of the date such litigation is filed. | ||||||
|  | 
 | ||||||
|  |    4. Redistribution. You may reproduce and distribute copies of the | ||||||
|  |       Work or Derivative Works thereof in any medium, with or without | ||||||
|  |       modifications, and in Source or Object form, provided that You | ||||||
|  |       meet the following conditions: | ||||||
|  | 
 | ||||||
|  |       (a) You must give any other recipients of the Work or | ||||||
|  |           Derivative Works a copy of this License; and | ||||||
|  | 
 | ||||||
|  |       (b) You must cause any modified files to carry prominent notices | ||||||
|  |           stating that You changed the files; and | ||||||
|  | 
 | ||||||
|  |       (c) You must retain, in the Source form of any Derivative Works | ||||||
|  |           that You distribute, all copyright, patent, trademark, and | ||||||
|  |           attribution notices from the Source form of the Work, | ||||||
|  |           excluding those notices that do not pertain to any part of | ||||||
|  |           the Derivative Works; and | ||||||
|  | 
 | ||||||
|  |       (d) If the Work includes a "NOTICE" text file as part of its | ||||||
|  |           distribution, then any Derivative Works that You distribute must | ||||||
|  |           include a readable copy of the attribution notices contained | ||||||
|  |           within such NOTICE file, excluding those notices that do not | ||||||
|  |           pertain to any part of the Derivative Works, in at least one | ||||||
|  |           of the following places: within a NOTICE text file distributed | ||||||
|  |           as part of the Derivative Works; within the Source form or | ||||||
|  |           documentation, if provided along with the Derivative Works; or, | ||||||
|  |           within a display generated by the Derivative Works, if and | ||||||
|  |           wherever such third-party notices normally appear. The contents | ||||||
|  |           of the NOTICE file are for informational purposes only and | ||||||
|  |           do not modify the License. You may add Your own attribution | ||||||
|  |           notices within Derivative Works that You distribute, alongside | ||||||
|  |           or as an addendum to the NOTICE text from the Work, provided | ||||||
|  |           that such additional attribution notices cannot be construed | ||||||
|  |           as modifying the License. | ||||||
|  | 
 | ||||||
|  |       You may add Your own copyright statement to Your modifications and | ||||||
|  |       may provide additional or different license terms and conditions | ||||||
|  |       for use, reproduction, or distribution of Your modifications, or | ||||||
|  |       for any such Derivative Works as a whole, provided Your use, | ||||||
|  |       reproduction, and distribution of the Work otherwise complies with | ||||||
|  |       the conditions stated in this License. | ||||||
|  | 
 | ||||||
|  |    5. Submission of Contributions. Unless You explicitly state otherwise, | ||||||
|  |       any Contribution intentionally submitted for inclusion in the Work | ||||||
|  |       by You to the Licensor shall be under the terms and conditions of | ||||||
|  |       this License, without any additional terms or conditions. | ||||||
|  |       Notwithstanding the above, nothing herein shall supersede or modify | ||||||
|  |       the terms of any separate license agreement you may have executed | ||||||
|  |       with Licensor regarding such Contributions. | ||||||
|  | 
 | ||||||
|  |    6. Trademarks. This License does not grant permission to use the trade | ||||||
|  |       names, trademarks, service marks, or product names of the Licensor, | ||||||
|  |       except as required for reasonable and customary use in describing the | ||||||
|  |       origin of the Work and reproducing the content of the NOTICE file. | ||||||
|  | 
 | ||||||
|  |    7. Disclaimer of Warranty. Unless required by applicable law or | ||||||
|  |       agreed to in writing, Licensor provides the Work (and each | ||||||
|  |       Contributor provides its Contributions) on an "AS IS" BASIS, | ||||||
|  |       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or | ||||||
|  |       implied, including, without limitation, any warranties or conditions | ||||||
|  |       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A | ||||||
|  |       PARTICULAR PURPOSE. You are solely responsible for determining the | ||||||
|  |       appropriateness of using or redistributing the Work and assume any | ||||||
|  |       risks associated with Your exercise of permissions under this License. | ||||||
|  | 
 | ||||||
|  |    8. Limitation of Liability. In no event and under no legal theory, | ||||||
|  |       whether in tort (including negligence), contract, or otherwise, | ||||||
|  |       unless required by applicable law (such as deliberate and grossly | ||||||
|  |       negligent acts) or agreed to in writing, shall any Contributor be | ||||||
|  |       liable to You for damages, including any direct, indirect, special, | ||||||
|  |       incidental, or consequential damages of any character arising as a | ||||||
|  |       result of this License or out of the use or inability to use the | ||||||
|  |       Work (including but not limited to damages for loss of goodwill, | ||||||
|  |       work stoppage, computer failure or malfunction, or any and all | ||||||
|  |       other commercial damages or losses), even if such Contributor | ||||||
|  |       has been advised of the possibility of such damages. | ||||||
|  | 
 | ||||||
|  |    9. Accepting Warranty or Additional Liability. While redistributing | ||||||
|  |       the Work or Derivative Works thereof, You may choose to offer, | ||||||
|  |       and charge a fee for, acceptance of support, warranty, indemnity, | ||||||
|  |       or other liability obligations and/or rights consistent with this | ||||||
|  |       License. However, in accepting such obligations, You may act only | ||||||
|  |       on Your own behalf and on Your sole responsibility, not on behalf | ||||||
|  |       of any other Contributor, and only if You agree to indemnify, | ||||||
|  |       defend, and hold each Contributor harmless for any liability | ||||||
|  |       incurred by, or claims asserted against, such Contributor by reason | ||||||
|  |       of your accepting any such warranty or additional liability. | ||||||
|  | 
 | ||||||
|  |    END OF TERMS AND CONDITIONS | ||||||
|  | 
 | ||||||
|  |    APPENDIX: How to apply the Apache License to your work. | ||||||
|  | 
 | ||||||
|  |       To apply the Apache License to your work, attach the following | ||||||
|  |       boilerplate notice, with the fields enclosed by brackets "{}" | ||||||
|  |       replaced with your own identifying information. (Don't include | ||||||
|  |       the brackets!)  The text should be enclosed in the appropriate | ||||||
|  |       comment syntax for the file format. We also recommend that a | ||||||
|  |       file or class name and description of purpose be included on the | ||||||
|  |       same "printed page" as the copyright notice for easier | ||||||
|  |       identification within third-party archives. | ||||||
|  | 
 | ||||||
|  |    Copyright 2016 Datadog, Inc. | ||||||
|  |    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. | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | Copyright (c) 2016, Datadog <info@datadoghq.com> | ||||||
|  | All rights reserved. | ||||||
|  | 
 | ||||||
|  | Redistribution and use in source and binary forms, with or without | ||||||
|  | modification, are permitted provided that the following conditions are met: | ||||||
|  |     * Redistributions of source code must retain the above copyright | ||||||
|  |       notice, this list of conditions and the following disclaimer. | ||||||
|  |     * Redistributions in binary form must reproduce the above copyright | ||||||
|  |       notice, this list of conditions and the following disclaimer in the | ||||||
|  |       documentation and/or other materials provided with the distribution. | ||||||
|  |     * Neither the name of Datadog nor the | ||||||
|  |       names of its contributors may be used to endorse or promote products | ||||||
|  |       derived from this software without specific prior written permission. | ||||||
|  | 
 | ||||||
|  | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | ||||||
|  | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | ||||||
|  | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||||||
|  | DISCLAIMED. IN NO EVENT SHALL DATADOG BE LIABLE FOR ANY | ||||||
|  | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | ||||||
|  | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | ||||||
|  | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND | ||||||
|  | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | ||||||
|  | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | ||||||
|  | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||||||
|  | @ -0,0 +1,4 @@ | ||||||
|  | Datadog dd-trace-py | ||||||
|  | Copyright 2016-Present Datadog, Inc. | ||||||
|  | 
 | ||||||
|  | This product includes software developed at Datadog, Inc. (https://www.datadoghq.com/). | ||||||
|  | @ -0,0 +1,77 @@ | ||||||
|  | desc "build the docs" | ||||||
|  | task :docs do | ||||||
|  |     sh "pip install sphinx" | ||||||
|  |   Dir.chdir 'docs' do | ||||||
|  |     sh "make html" | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | # Deploy tasks | ||||||
|  | S3_DIR = ENV['S3_DIR'] | ||||||
|  | S3_BUCKET = "pypi.datadoghq.com" | ||||||
|  | 
 | ||||||
|  | desc "release the a new wheel" | ||||||
|  | task :'release:wheel' do | ||||||
|  |   fail "Missing environment variable S3_DIR" if !S3_DIR or S3_DIR.empty? | ||||||
|  | 
 | ||||||
|  |   # Use custom `mkwheelhouse` to upload wheels and source distribution from dist/ to S3 bucket | ||||||
|  |   sh "scripts/mkwheelhouse" | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | desc "release the docs website" | ||||||
|  | task :'release:docs' => :docs do | ||||||
|  |   fail "Missing environment variable S3_DIR" if !S3_DIR or S3_DIR.empty? | ||||||
|  |   sh "aws s3 cp --recursive docs/_build/html/ s3://#{S3_BUCKET}/#{S3_DIR}/docs/" | ||||||
|  | end | ||||||
|  | 
 | ||||||
|  | namespace :pypi do | ||||||
|  |   RELEASE_DIR = './dist/' | ||||||
|  | 
 | ||||||
|  |   def get_version() | ||||||
|  |     return `python setup.py --version`.strip | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def get_branch() | ||||||
|  |     return `git name-rev --name-only HEAD`.strip | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   task :confirm do | ||||||
|  |     ddtrace_version = get_version | ||||||
|  | 
 | ||||||
|  |     if get_branch.downcase != 'tags/v#{ddtrace_version}' | ||||||
|  |       print "WARNING: Expected current commit to be tagged as 'tags/v#{ddtrace_version}, instead we are on '#{get_branch}', proceed anyways [y|N]? " | ||||||
|  |       $stdout.flush | ||||||
|  | 
 | ||||||
|  |       abort if $stdin.gets.to_s.strip.downcase != 'y' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     puts "WARNING: This task will build and release new wheels to https://pypi.org/project/ddtrace/, this action cannot be undone" | ||||||
|  |     print "         To proceed please type the version '#{ddtrace_version}': " | ||||||
|  |     $stdout.flush | ||||||
|  | 
 | ||||||
|  |     abort if $stdin.gets.to_s.strip.downcase != ddtrace_version | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   task :clean do | ||||||
|  |     FileUtils.rm_rf(RELEASE_DIR) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   task :install do | ||||||
|  |     sh 'pip install twine' | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   task :build => :clean do | ||||||
|  |     puts "building release in #{RELEASE_DIR}" | ||||||
|  |     sh "scripts/build-dist" | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   task :release => [:confirm, :install, :build] do | ||||||
|  |     builds = Dir.entries(RELEASE_DIR).reject {|f| f == '.' || f == '..'} | ||||||
|  |     if builds.length == 0 | ||||||
|  |         fail "no build found in #{RELEASE_DIR}" | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     puts "uploading #{RELEASE_DIR}/*" | ||||||
|  |     sh "twine upload #{RELEASE_DIR}/*" | ||||||
|  |   end | ||||||
|  | end | ||||||
|  | @ -0,0 +1,55 @@ | ||||||
|  | """ | ||||||
|  | This file configures a local pytest plugin, which allows us to configure plugin hooks to control the | ||||||
|  | execution of our tests. Either by loading in fixtures, configuring directories to ignore, etc | ||||||
|  | 
 | ||||||
|  | Local plugins: https://docs.pytest.org/en/3.10.1/writing_plugins.html#local-conftest-plugins | ||||||
|  | Hook reference: https://docs.pytest.org/en/3.10.1/reference.html#hook-reference | ||||||
|  | """ | ||||||
|  | import os | ||||||
|  | import re | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | import pytest | ||||||
|  | 
 | ||||||
|  | PY_DIR_PATTERN = re.compile(r"^py[23][0-9]$") | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Determine if the folder should be ignored | ||||||
|  | # https://docs.pytest.org/en/3.10.1/reference.html#_pytest.hookspec.pytest_ignore_collect | ||||||
|  | # DEV: We can only ignore folders/modules, we cannot ignore individual files | ||||||
|  | # DEV: We must wrap with `@pytest.mark.hookwrapper` to inherit from default (e.g. honor `--ignore`) | ||||||
|  | #      https://github.com/pytest-dev/pytest/issues/846#issuecomment-122129189 | ||||||
|  | @pytest.mark.hookwrapper | ||||||
|  | def pytest_ignore_collect(path, config): | ||||||
|  |     """ | ||||||
|  |     Skip directories defining a required minimum Python version | ||||||
|  | 
 | ||||||
|  |     Example:: | ||||||
|  | 
 | ||||||
|  |         File: tests/contrib/vertica/py35/test.py | ||||||
|  |         Python 2.7: Skip | ||||||
|  |         Python 3.4: Skip | ||||||
|  |         Python 3.5: Collect | ||||||
|  |         Python 3.6: Collect | ||||||
|  |     """ | ||||||
|  |     # Execute original behavior first | ||||||
|  |     # DEV: We need to set `outcome.force_result(True)` if we need to override | ||||||
|  |     #      these results and skip this directory | ||||||
|  |     outcome = yield | ||||||
|  | 
 | ||||||
|  |     # Was not ignored by default behavior | ||||||
|  |     if not outcome.get_result(): | ||||||
|  |         # DEV: `path` is a `LocalPath` | ||||||
|  |         path = str(path) | ||||||
|  |         if not os.path.isdir(path): | ||||||
|  |             path = os.path.dirname(path) | ||||||
|  |         dirname = os.path.basename(path) | ||||||
|  | 
 | ||||||
|  |         # Directory name match `py[23][0-9]` | ||||||
|  |         if PY_DIR_PATTERN.match(dirname): | ||||||
|  |             # Split out version numbers into a tuple: `py35` -> `(3, 5)` | ||||||
|  |             min_required = tuple((int(v) for v in dirname.strip("py"))) | ||||||
|  | 
 | ||||||
|  |             # If the current Python version does not meet the minimum required, skip this directory | ||||||
|  |             if sys.version_info[0:2] < min_required: | ||||||
|  |                 outcome.force_result(True) | ||||||
|  | @ -0,0 +1,51 @@ | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | import pkg_resources | ||||||
|  | 
 | ||||||
|  | from .monkey import patch, patch_all | ||||||
|  | from .pin import Pin | ||||||
|  | from .span import Span | ||||||
|  | from .tracer import Tracer | ||||||
|  | from .settings import config | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     __version__ = pkg_resources.get_distribution(__name__).version | ||||||
|  | except pkg_resources.DistributionNotFound: | ||||||
|  |     # package is not installed | ||||||
|  |     __version__ = None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # a global tracer instance with integration settings | ||||||
|  | tracer = Tracer() | ||||||
|  | 
 | ||||||
|  | __all__ = [ | ||||||
|  |     'patch', | ||||||
|  |     'patch_all', | ||||||
|  |     'Pin', | ||||||
|  |     'Span', | ||||||
|  |     'tracer', | ||||||
|  |     'Tracer', | ||||||
|  |     'config', | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _ORIGINAL_EXCEPTHOOK = sys.excepthook | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _excepthook(tp, value, traceback): | ||||||
|  |     tracer.global_excepthook(tp, value, traceback) | ||||||
|  |     if _ORIGINAL_EXCEPTHOOK: | ||||||
|  |         return _ORIGINAL_EXCEPTHOOK(tp, value, traceback) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def install_excepthook(): | ||||||
|  |     """Install a hook that intercepts unhandled exception and send metrics about them.""" | ||||||
|  |     global _ORIGINAL_EXCEPTHOOK | ||||||
|  |     _ORIGINAL_EXCEPTHOOK = sys.excepthook | ||||||
|  |     sys.excepthook = _excepthook | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def uninstall_excepthook(): | ||||||
|  |     """Uninstall the global tracer except hook.""" | ||||||
|  |     sys.excepthook = _ORIGINAL_EXCEPTHOOK | ||||||
|  | @ -0,0 +1,82 @@ | ||||||
|  | import atexit | ||||||
|  | import threading | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | from .internal.logger import get_logger | ||||||
|  | 
 | ||||||
|  | _LOG = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class PeriodicWorkerThread(object): | ||||||
|  |     """Periodic worker thread. | ||||||
|  | 
 | ||||||
|  |     This class can be used to instantiate a worker thread that will run its `run_periodic` function every `interval` | ||||||
|  |     seconds. | ||||||
|  | 
 | ||||||
|  |     The method `on_shutdown` will be called on worker shutdown. The worker will be shutdown when the program exits and | ||||||
|  |     can be waited for with the `exit_timeout` parameter. | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     _DEFAULT_INTERVAL = 1.0 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, interval=_DEFAULT_INTERVAL, exit_timeout=None, name=None, daemon=True): | ||||||
|  |         """Create a new worker thread that runs a function periodically. | ||||||
|  | 
 | ||||||
|  |         :param interval: The interval in seconds to wait between calls to `run_periodic`. | ||||||
|  |         :param exit_timeout: The timeout to use when exiting the program and waiting for the thread to finish. | ||||||
|  |         :param name: Name of the worker. | ||||||
|  |         :param daemon: Whether the worker should be a daemon. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |         self._thread = threading.Thread(target=self._target, name=name) | ||||||
|  |         self._thread.daemon = daemon | ||||||
|  |         self._stop = threading.Event() | ||||||
|  |         self.interval = interval | ||||||
|  |         self.exit_timeout = exit_timeout | ||||||
|  |         atexit.register(self._atexit) | ||||||
|  | 
 | ||||||
|  |     def _atexit(self): | ||||||
|  |         self.stop() | ||||||
|  |         if self.exit_timeout is not None: | ||||||
|  |             key = 'ctrl-break' if os.name == 'nt' else 'ctrl-c' | ||||||
|  |             _LOG.debug( | ||||||
|  |                 'Waiting %d seconds for %s to finish. Hit %s to quit.', | ||||||
|  |                 self.exit_timeout, self._thread.name, key, | ||||||
|  |             ) | ||||||
|  |             self.join(self.exit_timeout) | ||||||
|  | 
 | ||||||
|  |     def start(self): | ||||||
|  |         """Start the periodic worker.""" | ||||||
|  |         _LOG.debug('Starting %s thread', self._thread.name) | ||||||
|  |         self._thread.start() | ||||||
|  | 
 | ||||||
|  |     def stop(self): | ||||||
|  |         """Stop the worker.""" | ||||||
|  |         _LOG.debug('Stopping %s thread', self._thread.name) | ||||||
|  |         self._stop.set() | ||||||
|  | 
 | ||||||
|  |     def is_alive(self): | ||||||
|  |         return self._thread.is_alive() | ||||||
|  | 
 | ||||||
|  |     def join(self, timeout=None): | ||||||
|  |         return self._thread.join(timeout) | ||||||
|  | 
 | ||||||
|  |     def _target(self): | ||||||
|  |         while not self._stop.wait(self.interval): | ||||||
|  |             self.run_periodic() | ||||||
|  |         self._on_shutdown() | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def run_periodic(): | ||||||
|  |         """Method executed every interval.""" | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     def _on_shutdown(self): | ||||||
|  |         _LOG.debug('Shutting down %s thread', self._thread.name) | ||||||
|  |         self.on_shutdown() | ||||||
|  | 
 | ||||||
|  |     @staticmethod | ||||||
|  |     def on_shutdown(): | ||||||
|  |         """Method ran on worker shutdown.""" | ||||||
|  |         pass | ||||||
|  | @ -0,0 +1,279 @@ | ||||||
|  | # stdlib | ||||||
|  | import ddtrace | ||||||
|  | from json import loads | ||||||
|  | import socket | ||||||
|  | 
 | ||||||
|  | # project | ||||||
|  | from .encoding import get_encoder, JSONEncoder | ||||||
|  | from .compat import httplib, PYTHON_VERSION, PYTHON_INTERPRETER, get_connection_response | ||||||
|  | from .internal.logger import get_logger | ||||||
|  | from .internal.runtime import container | ||||||
|  | from .payload import Payload, PayloadFull | ||||||
|  | from .utils.deprecation import deprecated | ||||||
|  | from .utils import time | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _VERSIONS = {'v0.4': {'traces': '/v0.4/traces', | ||||||
|  |                       'services': '/v0.4/services', | ||||||
|  |                       'compatibility_mode': False, | ||||||
|  |                       'fallback': 'v0.3'}, | ||||||
|  |              'v0.3': {'traces': '/v0.3/traces', | ||||||
|  |                       'services': '/v0.3/services', | ||||||
|  |                       'compatibility_mode': False, | ||||||
|  |                       'fallback': 'v0.2'}, | ||||||
|  |              'v0.2': {'traces': '/v0.2/traces', | ||||||
|  |                       'services': '/v0.2/services', | ||||||
|  |                       'compatibility_mode': True, | ||||||
|  |                       'fallback': None}} | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Response(object): | ||||||
|  |     """ | ||||||
|  |     Custom API Response object to represent a response from calling the API. | ||||||
|  | 
 | ||||||
|  |     We do this to ensure we know expected properties will exist, and so we | ||||||
|  |     can call `resp.read()` and load the body once into an instance before we | ||||||
|  |     close the HTTPConnection used for the request. | ||||||
|  |     """ | ||||||
|  |     __slots__ = ['status', 'body', 'reason', 'msg'] | ||||||
|  | 
 | ||||||
|  |     def __init__(self, status=None, body=None, reason=None, msg=None): | ||||||
|  |         self.status = status | ||||||
|  |         self.body = body | ||||||
|  |         self.reason = reason | ||||||
|  |         self.msg = msg | ||||||
|  | 
 | ||||||
|  |     @classmethod | ||||||
|  |     def from_http_response(cls, resp): | ||||||
|  |         """ | ||||||
|  |         Build a ``Response`` from the provided ``HTTPResponse`` object. | ||||||
|  | 
 | ||||||
|  |         This function will call `.read()` to consume the body of the ``HTTPResponse`` object. | ||||||
|  | 
 | ||||||
|  |         :param resp: ``HTTPResponse`` object to build the ``Response`` from | ||||||
|  |         :type resp: ``HTTPResponse`` | ||||||
|  |         :rtype: ``Response`` | ||||||
|  |         :returns: A new ``Response`` | ||||||
|  |         """ | ||||||
|  |         return cls( | ||||||
|  |             status=resp.status, | ||||||
|  |             body=resp.read(), | ||||||
|  |             reason=getattr(resp, 'reason', None), | ||||||
|  |             msg=getattr(resp, 'msg', None), | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def get_json(self): | ||||||
|  |         """Helper to parse the body of this request as JSON""" | ||||||
|  |         try: | ||||||
|  |             body = self.body | ||||||
|  |             if not body: | ||||||
|  |                 log.debug('Empty reply from Datadog Agent, %r', self) | ||||||
|  |                 return | ||||||
|  | 
 | ||||||
|  |             if not isinstance(body, str) and hasattr(body, 'decode'): | ||||||
|  |                 body = body.decode('utf-8') | ||||||
|  | 
 | ||||||
|  |             if hasattr(body, 'startswith') and body.startswith('OK'): | ||||||
|  |                 # This typically happens when using a priority-sampling enabled | ||||||
|  |                 # library with an outdated agent. It still works, but priority sampling | ||||||
|  |                 # will probably send too many traces, so the next step is to upgrade agent. | ||||||
|  |                 log.debug('Cannot parse Datadog Agent response, please make sure your Datadog Agent is up to date') | ||||||
|  |                 return | ||||||
|  | 
 | ||||||
|  |             return loads(body) | ||||||
|  |         except (ValueError, TypeError): | ||||||
|  |             log.debug('Unable to parse Datadog Agent JSON response: %r', body, exc_info=True) | ||||||
|  | 
 | ||||||
|  |     def __repr__(self): | ||||||
|  |         return '{0}(status={1!r}, body={2!r}, reason={3!r}, msg={4!r})'.format( | ||||||
|  |             self.__class__.__name__, | ||||||
|  |             self.status, | ||||||
|  |             self.body, | ||||||
|  |             self.reason, | ||||||
|  |             self.msg, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class UDSHTTPConnection(httplib.HTTPConnection): | ||||||
|  |     """An HTTP connection established over a Unix Domain Socket.""" | ||||||
|  | 
 | ||||||
|  |     # It's "important" to keep the hostname and port arguments here; while there are not used by the connection | ||||||
|  |     # mechanism, they are actually used as HTTP headers such as `Host`. | ||||||
|  |     def __init__(self, path, https, *args, **kwargs): | ||||||
|  |         if https: | ||||||
|  |             httplib.HTTPSConnection.__init__(self, *args, **kwargs) | ||||||
|  |         else: | ||||||
|  |             httplib.HTTPConnection.__init__(self, *args, **kwargs) | ||||||
|  |         self.path = path | ||||||
|  | 
 | ||||||
|  |     def connect(self): | ||||||
|  |         sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | ||||||
|  |         sock.connect(self.path) | ||||||
|  |         self.sock = sock | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class API(object): | ||||||
|  |     """ | ||||||
|  |     Send data to the trace agent using the HTTP protocol and JSON format | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     TRACE_COUNT_HEADER = 'X-Datadog-Trace-Count' | ||||||
|  | 
 | ||||||
|  |     # Default timeout when establishing HTTP connection and sending/receiving from socket. | ||||||
|  |     # This ought to be enough as the agent is local | ||||||
|  |     TIMEOUT = 2 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, hostname, port, uds_path=None, https=False, headers=None, encoder=None, priority_sampling=False): | ||||||
|  |         """Create a new connection to the Tracer API. | ||||||
|  | 
 | ||||||
|  |         :param hostname: The hostname. | ||||||
|  |         :param port: The TCP port to use. | ||||||
|  |         :param uds_path: The path to use if the connection is to be established with a Unix Domain Socket. | ||||||
|  |         :param headers: The headers to pass along the request. | ||||||
|  |         :param encoder: The encoder to use to serialize data. | ||||||
|  |         :param priority_sampling: Whether to use priority sampling. | ||||||
|  |         """ | ||||||
|  |         self.hostname = hostname | ||||||
|  |         self.port = int(port) | ||||||
|  |         self.uds_path = uds_path | ||||||
|  |         self.https = https | ||||||
|  | 
 | ||||||
|  |         self._headers = headers or {} | ||||||
|  |         self._version = None | ||||||
|  | 
 | ||||||
|  |         if priority_sampling: | ||||||
|  |             self._set_version('v0.4', encoder=encoder) | ||||||
|  |         else: | ||||||
|  |             self._set_version('v0.3', encoder=encoder) | ||||||
|  | 
 | ||||||
|  |         self._headers.update({ | ||||||
|  |             'Datadog-Meta-Lang': 'python', | ||||||
|  |             'Datadog-Meta-Lang-Version': PYTHON_VERSION, | ||||||
|  |             'Datadog-Meta-Lang-Interpreter': PYTHON_INTERPRETER, | ||||||
|  |             'Datadog-Meta-Tracer-Version': ddtrace.__version__, | ||||||
|  |         }) | ||||||
|  | 
 | ||||||
|  |         # Add container information if we have it | ||||||
|  |         self._container_info = container.get_container_info() | ||||||
|  |         if self._container_info and self._container_info.container_id: | ||||||
|  |             self._headers.update({ | ||||||
|  |                 'Datadog-Container-Id': self._container_info.container_id, | ||||||
|  |             }) | ||||||
|  | 
 | ||||||
|  |     def __str__(self): | ||||||
|  |         if self.uds_path: | ||||||
|  |             return 'unix://' + self.uds_path | ||||||
|  |         if self.https: | ||||||
|  |             scheme = 'https://' | ||||||
|  |         else: | ||||||
|  |             scheme = 'http://' | ||||||
|  |         return '%s%s:%s' % (scheme, self.hostname, self.port) | ||||||
|  | 
 | ||||||
|  |     def _set_version(self, version, encoder=None): | ||||||
|  |         if version not in _VERSIONS: | ||||||
|  |             version = 'v0.2' | ||||||
|  |         if version == self._version: | ||||||
|  |             return | ||||||
|  |         self._version = version | ||||||
|  |         self._traces = _VERSIONS[version]['traces'] | ||||||
|  |         self._services = _VERSIONS[version]['services'] | ||||||
|  |         self._fallback = _VERSIONS[version]['fallback'] | ||||||
|  |         self._compatibility_mode = _VERSIONS[version]['compatibility_mode'] | ||||||
|  |         if self._compatibility_mode: | ||||||
|  |             self._encoder = JSONEncoder() | ||||||
|  |         else: | ||||||
|  |             self._encoder = encoder or get_encoder() | ||||||
|  |         # overwrite the Content-type with the one chosen in the Encoder | ||||||
|  |         self._headers.update({'Content-Type': self._encoder.content_type}) | ||||||
|  | 
 | ||||||
|  |     def _downgrade(self): | ||||||
|  |         """ | ||||||
|  |         Downgrades the used encoder and API level. This method must fallback to a safe | ||||||
|  |         encoder and API, so that it will success despite users' configurations. This action | ||||||
|  |         ensures that the compatibility mode is activated so that the downgrade will be | ||||||
|  |         executed only once. | ||||||
|  |         """ | ||||||
|  |         self._set_version(self._fallback) | ||||||
|  | 
 | ||||||
|  |     def send_traces(self, traces): | ||||||
|  |         """Send traces to the API. | ||||||
|  | 
 | ||||||
|  |         :param traces: A list of traces. | ||||||
|  |         :return: The list of API HTTP responses. | ||||||
|  |         """ | ||||||
|  |         if not traces: | ||||||
|  |             return [] | ||||||
|  | 
 | ||||||
|  |         with time.StopWatch() as sw: | ||||||
|  |             responses = [] | ||||||
|  |             payload = Payload(encoder=self._encoder) | ||||||
|  |             for trace in traces: | ||||||
|  |                 try: | ||||||
|  |                     payload.add_trace(trace) | ||||||
|  |                 except PayloadFull: | ||||||
|  |                     # Is payload full or is the trace too big? | ||||||
|  |                     # If payload is not empty, then using a new Payload might allow us to fit the trace. | ||||||
|  |                     # Let's flush the Payload and try to put the trace in a new empty Payload. | ||||||
|  |                     if not payload.empty: | ||||||
|  |                         responses.append(self._flush(payload)) | ||||||
|  |                         # Create a new payload | ||||||
|  |                         payload = Payload(encoder=self._encoder) | ||||||
|  |                         try: | ||||||
|  |                             # Add the trace that we were unable to add in that iteration | ||||||
|  |                             payload.add_trace(trace) | ||||||
|  |                         except PayloadFull: | ||||||
|  |                             # If the trace does not fit in a payload on its own, that's bad. Drop it. | ||||||
|  |                             log.warning('Trace %r is too big to fit in a payload, dropping it', trace) | ||||||
|  | 
 | ||||||
|  |             # Check that the Payload is not empty: | ||||||
|  |             # it could be empty if the last trace was too big to fit. | ||||||
|  |             if not payload.empty: | ||||||
|  |                 responses.append(self._flush(payload)) | ||||||
|  | 
 | ||||||
|  |         log.debug('reported %d traces in %.5fs', len(traces), sw.elapsed()) | ||||||
|  | 
 | ||||||
|  |         return responses | ||||||
|  | 
 | ||||||
|  |     def _flush(self, payload): | ||||||
|  |         try: | ||||||
|  |             response = self._put(self._traces, payload.get_payload(), payload.length) | ||||||
|  |         except (httplib.HTTPException, OSError, IOError) as e: | ||||||
|  |             return e | ||||||
|  | 
 | ||||||
|  |         # the API endpoint is not available so we should downgrade the connection and re-try the call | ||||||
|  |         if response.status in [404, 415] and self._fallback: | ||||||
|  |             log.debug("calling endpoint '%s' but received %s; downgrading API", self._traces, response.status) | ||||||
|  |             self._downgrade() | ||||||
|  |             return self._flush(payload) | ||||||
|  | 
 | ||||||
|  |         return response | ||||||
|  | 
 | ||||||
|  |     @deprecated(message='Sending services to the API is no longer necessary', version='1.0.0') | ||||||
|  |     def send_services(self, *args, **kwargs): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     def _put(self, endpoint, data, count): | ||||||
|  |         headers = self._headers.copy() | ||||||
|  |         headers[self.TRACE_COUNT_HEADER] = str(count) | ||||||
|  | 
 | ||||||
|  |         if self.uds_path is None: | ||||||
|  |             if self.https: | ||||||
|  |                 conn = httplib.HTTPSConnection(self.hostname, self.port, timeout=self.TIMEOUT) | ||||||
|  |             else: | ||||||
|  |                 conn = httplib.HTTPConnection(self.hostname, self.port, timeout=self.TIMEOUT) | ||||||
|  |         else: | ||||||
|  |             conn = UDSHTTPConnection(self.uds_path, self.https, self.hostname, self.port, timeout=self.TIMEOUT) | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             conn.request('PUT', endpoint, data, headers) | ||||||
|  | 
 | ||||||
|  |             # Parse the HTTPResponse into an API.Response | ||||||
|  |             # DEV: This will call `resp.read()` which must happen before the `conn.close()` below, | ||||||
|  |             #      if we call `.close()` then all future `.read()` calls will return `b''` | ||||||
|  |             resp = get_connection_response(conn) | ||||||
|  |             return Response.from_http_response(resp) | ||||||
|  |         finally: | ||||||
|  |             conn.close() | ||||||
|  | @ -0,0 +1,150 @@ | ||||||
|  | """ | ||||||
|  | Bootstrapping code that is run when using the `ddtrace-run` Python entrypoint | ||||||
|  | Add all monkey-patching that needs to run by default here | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | import imp | ||||||
|  | import sys | ||||||
|  | import logging | ||||||
|  | 
 | ||||||
|  | from ddtrace.utils.formats import asbool, get_env | ||||||
|  | from ddtrace.internal.logger import get_logger | ||||||
|  | from ddtrace import constants | ||||||
|  | 
 | ||||||
|  | logs_injection = asbool(get_env("logs", "injection")) | ||||||
|  | DD_LOG_FORMAT = "%(asctime)s %(levelname)s [%(name)s] [%(filename)s:%(lineno)d] {}- %(message)s".format( | ||||||
|  |     "[dd.trace_id=%(dd.trace_id)s dd.span_id=%(dd.span_id)s] " if logs_injection else "" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | if logs_injection: | ||||||
|  |     # immediately patch logging if trace id injected | ||||||
|  |     from ddtrace import patch | ||||||
|  | 
 | ||||||
|  |     patch(logging=True) | ||||||
|  | 
 | ||||||
|  | debug = os.environ.get("DATADOG_TRACE_DEBUG") | ||||||
|  | 
 | ||||||
|  | # Set here a default logging format for basicConfig | ||||||
|  | 
 | ||||||
|  | # DEV: Once basicConfig is called here, future calls to it cannot be used to | ||||||
|  | # change the formatter since it applies the formatter to the root handler only | ||||||
|  | # upon initializing it the first time. | ||||||
|  | # See https://github.com/python/cpython/blob/112e4afd582515fcdcc0cde5012a4866e5cfda12/Lib/logging/__init__.py#L1550 | ||||||
|  | if debug and debug.lower() == "true": | ||||||
|  |     logging.basicConfig(level=logging.DEBUG, format=DD_LOG_FORMAT) | ||||||
|  | else: | ||||||
|  |     logging.basicConfig(format=DD_LOG_FORMAT) | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | EXTRA_PATCHED_MODULES = { | ||||||
|  |     "bottle": True, | ||||||
|  |     "django": True, | ||||||
|  |     "falcon": True, | ||||||
|  |     "flask": True, | ||||||
|  |     "pylons": True, | ||||||
|  |     "pyramid": True, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def update_patched_modules(): | ||||||
|  |     modules_to_patch = os.environ.get("DATADOG_PATCH_MODULES") | ||||||
|  |     if not modules_to_patch: | ||||||
|  |         return | ||||||
|  |     for patch in modules_to_patch.split(","): | ||||||
|  |         if len(patch.split(":")) != 2: | ||||||
|  |             log.debug("skipping malformed patch instruction") | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         module, should_patch = patch.split(":") | ||||||
|  |         if should_patch.lower() not in ["true", "false"]: | ||||||
|  |             log.debug("skipping malformed patch instruction for %s", module) | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         EXTRA_PATCHED_MODULES.update({module: should_patch.lower() == "true"}) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def add_global_tags(tracer): | ||||||
|  |     tags = {} | ||||||
|  |     for tag in os.environ.get("DD_TRACE_GLOBAL_TAGS", "").split(","): | ||||||
|  |         tag_name, _, tag_value = tag.partition(":") | ||||||
|  |         if not tag_name or not tag_value: | ||||||
|  |             log.debug("skipping malformed tracer tag") | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         tags[tag_name] = tag_value | ||||||
|  |     tracer.set_tags(tags) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from ddtrace import tracer | ||||||
|  | 
 | ||||||
|  |     patch = True | ||||||
|  | 
 | ||||||
|  |     # Respect DATADOG_* environment variables in global tracer configuration | ||||||
|  |     # TODO: these variables are deprecated; use utils method and update our documentation | ||||||
|  |     # correct prefix should be DD_* | ||||||
|  |     enabled = os.environ.get("DATADOG_TRACE_ENABLED") | ||||||
|  |     hostname = os.environ.get("DD_AGENT_HOST", os.environ.get("DATADOG_TRACE_AGENT_HOSTNAME")) | ||||||
|  |     port = os.environ.get("DATADOG_TRACE_AGENT_PORT") | ||||||
|  |     priority_sampling = os.environ.get("DATADOG_PRIORITY_SAMPLING") | ||||||
|  | 
 | ||||||
|  |     opts = {} | ||||||
|  | 
 | ||||||
|  |     if enabled and enabled.lower() == "false": | ||||||
|  |         opts["enabled"] = False | ||||||
|  |         patch = False | ||||||
|  |     if hostname: | ||||||
|  |         opts["hostname"] = hostname | ||||||
|  |     if port: | ||||||
|  |         opts["port"] = int(port) | ||||||
|  |     if priority_sampling: | ||||||
|  |         opts["priority_sampling"] = asbool(priority_sampling) | ||||||
|  | 
 | ||||||
|  |     opts["collect_metrics"] = asbool(get_env("runtime_metrics", "enabled")) | ||||||
|  | 
 | ||||||
|  |     if opts: | ||||||
|  |         tracer.configure(**opts) | ||||||
|  | 
 | ||||||
|  |     if logs_injection: | ||||||
|  |         EXTRA_PATCHED_MODULES.update({"logging": True}) | ||||||
|  | 
 | ||||||
|  |     if patch: | ||||||
|  |         update_patched_modules() | ||||||
|  |         from ddtrace import patch_all | ||||||
|  | 
 | ||||||
|  |         patch_all(**EXTRA_PATCHED_MODULES) | ||||||
|  | 
 | ||||||
|  |     if "DATADOG_ENV" in os.environ: | ||||||
|  |         tracer.set_tags({constants.ENV_KEY: os.environ["DATADOG_ENV"]}) | ||||||
|  | 
 | ||||||
|  |     if "DD_TRACE_GLOBAL_TAGS" in os.environ: | ||||||
|  |         add_global_tags(tracer) | ||||||
|  | 
 | ||||||
|  |     # Ensure sitecustomize.py is properly called if available in application directories: | ||||||
|  |     # * exclude `bootstrap_dir` from the search | ||||||
|  |     # * find a user `sitecustomize.py` module | ||||||
|  |     # * import that module via `imp` | ||||||
|  |     bootstrap_dir = os.path.dirname(__file__) | ||||||
|  |     path = list(sys.path) | ||||||
|  | 
 | ||||||
|  |     if bootstrap_dir in path: | ||||||
|  |         path.remove(bootstrap_dir) | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         (f, path, description) = imp.find_module("sitecustomize", path) | ||||||
|  |     except ImportError: | ||||||
|  |         pass | ||||||
|  |     else: | ||||||
|  |         # `sitecustomize.py` found, load it | ||||||
|  |         log.debug("sitecustomize from user found in: %s", path) | ||||||
|  |         imp.load_module("sitecustomize", f, path, description) | ||||||
|  | 
 | ||||||
|  |     # Loading status used in tests to detect if the `sitecustomize` has been | ||||||
|  |     # properly loaded without exceptions. This must be the last action in the module | ||||||
|  |     # when the execution ends with a success. | ||||||
|  |     loaded = True | ||||||
|  | except Exception: | ||||||
|  |     loaded = False | ||||||
|  |     log.warning("error configuring Datadog tracing", exc_info=True) | ||||||
|  | @ -0,0 +1,82 @@ | ||||||
|  | #!/usr/bin/env python | ||||||
|  | from distutils import spawn | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import logging | ||||||
|  | 
 | ||||||
|  | debug = os.environ.get('DATADOG_TRACE_DEBUG') | ||||||
|  | if debug and debug.lower() == 'true': | ||||||
|  |     logging.basicConfig(level=logging.DEBUG) | ||||||
|  | 
 | ||||||
|  | # Do not use `ddtrace.internal.logger.get_logger` here | ||||||
|  | # DEV: It isn't really necessary to use `DDLogger` here so we want to | ||||||
|  | #        defer importing `ddtrace` until we actually need it. | ||||||
|  | #      As well, no actual rate limiting would apply here since we only | ||||||
|  | #        have a few logged lines | ||||||
|  | log = logging.getLogger(__name__) | ||||||
|  | 
 | ||||||
|  | USAGE = """ | ||||||
|  | Execute the given Python program after configuring it to emit Datadog traces. | ||||||
|  | Append command line arguments to your program as usual. | ||||||
|  | 
 | ||||||
|  | Usage: [ENV_VARS] ddtrace-run <my_program> | ||||||
|  | 
 | ||||||
|  | Available environment variables: | ||||||
|  | 
 | ||||||
|  |     DATADOG_ENV : override an application's environment (no default) | ||||||
|  |     DATADOG_TRACE_ENABLED=true|false : override the value of tracer.enabled (default: true) | ||||||
|  |     DATADOG_TRACE_DEBUG=true|false : enabled debug logging (default: false) | ||||||
|  |     DATADOG_PATCH_MODULES=module:patch,module:patch... e.g. boto:true,redis:false : override the modules patched for this execution of the program (default: none) | ||||||
|  |     DATADOG_TRACE_AGENT_HOSTNAME=localhost: override the address of the trace agent host that the default tracer will attempt to submit to  (default: localhost) | ||||||
|  |     DATADOG_TRACE_AGENT_PORT=8126: override the port that the default tracer will submit to (default: 8126) | ||||||
|  |     DATADOG_SERVICE_NAME : override the service name to be used for this program (no default) | ||||||
|  |                            This value is passed through when setting up middleware for web framework integrations. | ||||||
|  |                            (e.g. pylons, flask, django) | ||||||
|  |                            For tracing without a web integration, prefer setting the service name in code. | ||||||
|  |     DATADOG_PRIORITY_SAMPLING=true|false : (default: false): enables Priority Sampling. | ||||||
|  | """  # noqa: E501 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _ddtrace_root(): | ||||||
|  |     from ddtrace import __file__ | ||||||
|  |     return os.path.dirname(__file__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _add_bootstrap_to_pythonpath(bootstrap_dir): | ||||||
|  |     """ | ||||||
|  |     Add our bootstrap directory to the head of $PYTHONPATH to ensure | ||||||
|  |     it is loaded before program code | ||||||
|  |     """ | ||||||
|  |     python_path = os.environ.get('PYTHONPATH', '') | ||||||
|  | 
 | ||||||
|  |     if python_path: | ||||||
|  |         new_path = '%s%s%s' % (bootstrap_dir, os.path.pathsep, os.environ['PYTHONPATH']) | ||||||
|  |         os.environ['PYTHONPATH'] = new_path | ||||||
|  |     else: | ||||||
|  |         os.environ['PYTHONPATH'] = bootstrap_dir | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def main(): | ||||||
|  |     if len(sys.argv) < 2 or sys.argv[1] == '-h': | ||||||
|  |         print(USAGE) | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     log.debug('sys.argv: %s', sys.argv) | ||||||
|  | 
 | ||||||
|  |     root_dir = _ddtrace_root() | ||||||
|  |     log.debug('ddtrace root: %s', root_dir) | ||||||
|  | 
 | ||||||
|  |     bootstrap_dir = os.path.join(root_dir, 'bootstrap') | ||||||
|  |     log.debug('ddtrace bootstrap: %s', bootstrap_dir) | ||||||
|  | 
 | ||||||
|  |     _add_bootstrap_to_pythonpath(bootstrap_dir) | ||||||
|  |     log.debug('PYTHONPATH: %s', os.environ['PYTHONPATH']) | ||||||
|  |     log.debug('sys.path: %s', sys.path) | ||||||
|  | 
 | ||||||
|  |     executable = sys.argv[1] | ||||||
|  | 
 | ||||||
|  |     # Find the executable path | ||||||
|  |     executable = spawn.find_executable(executable) | ||||||
|  |     log.debug('program executable: %s', executable) | ||||||
|  | 
 | ||||||
|  |     os.execl(executable, executable, *sys.argv[2:]) | ||||||
|  | @ -0,0 +1,151 @@ | ||||||
|  | import platform | ||||||
|  | import re | ||||||
|  | import sys | ||||||
|  | import textwrap | ||||||
|  | 
 | ||||||
|  | from ddtrace.vendor import six | ||||||
|  | 
 | ||||||
|  | __all__ = [ | ||||||
|  |     'httplib', | ||||||
|  |     'iteritems', | ||||||
|  |     'PY2', | ||||||
|  |     'Queue', | ||||||
|  |     'stringify', | ||||||
|  |     'StringIO', | ||||||
|  |     'urlencode', | ||||||
|  |     'parse', | ||||||
|  |     'reraise', | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | PYTHON_VERSION_INFO = sys.version_info | ||||||
|  | PY2 = sys.version_info[0] == 2 | ||||||
|  | PY3 = sys.version_info[0] == 3 | ||||||
|  | 
 | ||||||
|  | # Infos about python passed to the trace agent through the header | ||||||
|  | PYTHON_VERSION = platform.python_version() | ||||||
|  | PYTHON_INTERPRETER = platform.python_implementation() | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     StringIO = six.moves.cStringIO | ||||||
|  | except ImportError: | ||||||
|  |     StringIO = six.StringIO | ||||||
|  | 
 | ||||||
|  | httplib = six.moves.http_client | ||||||
|  | urlencode = six.moves.urllib.parse.urlencode | ||||||
|  | parse = six.moves.urllib.parse | ||||||
|  | Queue = six.moves.queue.Queue | ||||||
|  | iteritems = six.iteritems | ||||||
|  | reraise = six.reraise | ||||||
|  | reload_module = six.moves.reload_module | ||||||
|  | 
 | ||||||
|  | stringify = six.text_type | ||||||
|  | string_type = six.string_types[0] | ||||||
|  | msgpack_type = six.binary_type | ||||||
|  | # DEV: `six` doesn't have `float` in `integer_types` | ||||||
|  | numeric_types = six.integer_types + (float, ) | ||||||
|  | 
 | ||||||
|  | # Pattern class generated by `re.compile` | ||||||
|  | if PYTHON_VERSION_INFO >= (3, 7): | ||||||
|  |     pattern_type = re.Pattern | ||||||
|  | else: | ||||||
|  |     pattern_type = re._pattern_type | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def is_integer(obj): | ||||||
|  |     """Helper to determine if the provided ``obj`` is an integer type or not""" | ||||||
|  |     # DEV: We have to make sure it is an integer and not a boolean | ||||||
|  |     # >>> type(True) | ||||||
|  |     # <class 'bool'> | ||||||
|  |     # >>> isinstance(True, int) | ||||||
|  |     # True | ||||||
|  |     return isinstance(obj, six.integer_types) and not isinstance(obj, bool) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from time import time_ns | ||||||
|  | except ImportError: | ||||||
|  |     from time import time as _time | ||||||
|  | 
 | ||||||
|  |     def time_ns(): | ||||||
|  |         return int(_time() * 10e5) * 1000 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if PYTHON_VERSION_INFO[0:2] >= (3, 4): | ||||||
|  |     from asyncio import iscoroutinefunction | ||||||
|  | 
 | ||||||
|  |     # Execute from a string to get around syntax errors from `yield from` | ||||||
|  |     # DEV: The idea to do this was stolen from `six` | ||||||
|  |     #   https://github.com/benjaminp/six/blob/15e31431af97e5e64b80af0a3f598d382bcdd49a/six.py#L719-L737 | ||||||
|  |     six.exec_(textwrap.dedent(""" | ||||||
|  |     import functools | ||||||
|  |     import asyncio | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     def make_async_decorator(tracer, coro, *params, **kw_params): | ||||||
|  |         \"\"\" | ||||||
|  |         Decorator factory that creates an asynchronous wrapper that yields | ||||||
|  |         a coroutine result. This factory is required to handle Python 2 | ||||||
|  |         compatibilities. | ||||||
|  | 
 | ||||||
|  |         :param object tracer: the tracer instance that is used | ||||||
|  |         :param function f: the coroutine that must be executed | ||||||
|  |         :param tuple params: arguments given to the Tracer.trace() | ||||||
|  |         :param dict kw_params: keyword arguments given to the Tracer.trace() | ||||||
|  |         \"\"\" | ||||||
|  |         @functools.wraps(coro) | ||||||
|  |         @asyncio.coroutine | ||||||
|  |         def func_wrapper(*args, **kwargs): | ||||||
|  |             with tracer.trace(*params, **kw_params): | ||||||
|  |                 result = yield from coro(*args, **kwargs)  # noqa: E999 | ||||||
|  |                 return result | ||||||
|  | 
 | ||||||
|  |         return func_wrapper | ||||||
|  |     """)) | ||||||
|  | 
 | ||||||
|  | else: | ||||||
|  |     # asyncio is missing so we can't have coroutines; these | ||||||
|  |     # functions are used only to ensure code executions in case | ||||||
|  |     # of an unexpected behavior | ||||||
|  |     def iscoroutinefunction(fn): | ||||||
|  |         return False | ||||||
|  | 
 | ||||||
|  |     def make_async_decorator(tracer, fn, *params, **kw_params): | ||||||
|  |         return fn | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # DEV: There is `six.u()` which does something similar, but doesn't have the guard around `hasattr(s, 'decode')` | ||||||
|  | def to_unicode(s): | ||||||
|  |     """ Return a unicode string for the given bytes or string instance. """ | ||||||
|  |     # No reason to decode if we already have the unicode compatible object we expect | ||||||
|  |     # DEV: `six.text_type` will be a `str` for python 3 and `unicode` for python 2 | ||||||
|  |     # DEV: Double decoding a `unicode` can cause a `UnicodeEncodeError` | ||||||
|  |     #   e.g. `'\xc3\xbf'.decode('utf-8').decode('utf-8')` | ||||||
|  |     if isinstance(s, six.text_type): | ||||||
|  |         return s | ||||||
|  | 
 | ||||||
|  |     # If the object has a `decode` method, then decode into `utf-8` | ||||||
|  |     #   e.g. Python 2 `str`, Python 2/3 `bytearray`, etc | ||||||
|  |     if hasattr(s, 'decode'): | ||||||
|  |         return s.decode('utf-8') | ||||||
|  | 
 | ||||||
|  |     # Always try to coerce the object into the `six.text_type` object we expect | ||||||
|  |     #   e.g. `to_unicode(1)`, `to_unicode(dict(key='value'))` | ||||||
|  |     return six.text_type(s) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_connection_response(conn): | ||||||
|  |     """Returns the response for a connection. | ||||||
|  | 
 | ||||||
|  |     If using Python 2 enable buffering. | ||||||
|  | 
 | ||||||
|  |     Python 2 does not enable buffering by default resulting in many recv | ||||||
|  |     syscalls. | ||||||
|  | 
 | ||||||
|  |     See: | ||||||
|  |     https://bugs.python.org/issue4879 | ||||||
|  |     https://github.com/python/cpython/commit/3c43fcba8b67ea0cec4a443c755ce5f25990a6cf | ||||||
|  |     """ | ||||||
|  |     if PY2: | ||||||
|  |         return conn.getresponse(buffering=True) | ||||||
|  |     else: | ||||||
|  |         return conn.getresponse() | ||||||
|  | @ -0,0 +1,15 @@ | ||||||
|  | FILTERS_KEY = 'FILTERS' | ||||||
|  | SAMPLE_RATE_METRIC_KEY = '_sample_rate' | ||||||
|  | SAMPLING_PRIORITY_KEY = '_sampling_priority_v1' | ||||||
|  | ANALYTICS_SAMPLE_RATE_KEY = '_dd1.sr.eausr' | ||||||
|  | SAMPLING_AGENT_DECISION = '_dd.agent_psr' | ||||||
|  | SAMPLING_RULE_DECISION = '_dd.rule_psr' | ||||||
|  | SAMPLING_LIMIT_DECISION = '_dd.limit_psr' | ||||||
|  | ORIGIN_KEY = '_dd.origin' | ||||||
|  | HOSTNAME_KEY = '_dd.hostname' | ||||||
|  | ENV_KEY = 'env' | ||||||
|  | 
 | ||||||
|  | NUMERIC_TAGS = (ANALYTICS_SAMPLE_RATE_KEY, ) | ||||||
|  | 
 | ||||||
|  | MANUAL_DROP_KEY = 'manual.drop' | ||||||
|  | MANUAL_KEEP_KEY = 'manual.keep' | ||||||
|  | @ -0,0 +1,216 @@ | ||||||
|  | import logging | ||||||
|  | import threading | ||||||
|  | 
 | ||||||
|  | from .constants import HOSTNAME_KEY, SAMPLING_PRIORITY_KEY, ORIGIN_KEY | ||||||
|  | from .internal.logger import get_logger | ||||||
|  | from .internal import hostname | ||||||
|  | from .settings import config | ||||||
|  | from .utils.formats import asbool, get_env | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class Context(object): | ||||||
|  |     """ | ||||||
|  |     Context is used to keep track of a hierarchy of spans for the current | ||||||
|  |     execution flow. During each logical execution, the same ``Context`` is | ||||||
|  |     used to represent a single logical trace, even if the trace is built | ||||||
|  |     asynchronously. | ||||||
|  | 
 | ||||||
|  |     A single code execution may use multiple ``Context`` if part of the execution | ||||||
|  |     must not be related to the current tracing. As example, a delayed job may | ||||||
|  |     compose a standalone trace instead of being related to the same trace that | ||||||
|  |     generates the job itself. On the other hand, if it's part of the same | ||||||
|  |     ``Context``, it will be related to the original trace. | ||||||
|  | 
 | ||||||
|  |     This data structure is thread-safe. | ||||||
|  |     """ | ||||||
|  |     _partial_flush_enabled = asbool(get_env('tracer', 'partial_flush_enabled', 'false')) | ||||||
|  |     _partial_flush_min_spans = int(get_env('tracer', 'partial_flush_min_spans', 500)) | ||||||
|  | 
 | ||||||
|  |     def __init__(self, trace_id=None, span_id=None, sampling_priority=None, _dd_origin=None): | ||||||
|  |         """ | ||||||
|  |         Initialize a new thread-safe ``Context``. | ||||||
|  | 
 | ||||||
|  |         :param int trace_id: trace_id of parent span | ||||||
|  |         :param int span_id: span_id of parent span | ||||||
|  |         """ | ||||||
|  |         self._trace = [] | ||||||
|  |         self._finished_spans = 0 | ||||||
|  |         self._current_span = None | ||||||
|  |         self._lock = threading.Lock() | ||||||
|  | 
 | ||||||
|  |         self._parent_trace_id = trace_id | ||||||
|  |         self._parent_span_id = span_id | ||||||
|  |         self._sampling_priority = sampling_priority | ||||||
|  |         self._dd_origin = _dd_origin | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def trace_id(self): | ||||||
|  |         """Return current context trace_id.""" | ||||||
|  |         with self._lock: | ||||||
|  |             return self._parent_trace_id | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def span_id(self): | ||||||
|  |         """Return current context span_id.""" | ||||||
|  |         with self._lock: | ||||||
|  |             return self._parent_span_id | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def sampling_priority(self): | ||||||
|  |         """Return current context sampling priority.""" | ||||||
|  |         with self._lock: | ||||||
|  |             return self._sampling_priority | ||||||
|  | 
 | ||||||
|  |     @sampling_priority.setter | ||||||
|  |     def sampling_priority(self, value): | ||||||
|  |         """Set sampling priority.""" | ||||||
|  |         with self._lock: | ||||||
|  |             self._sampling_priority = value | ||||||
|  | 
 | ||||||
|  |     def clone(self): | ||||||
|  |         """ | ||||||
|  |         Partially clones the current context. | ||||||
|  |         It copies everything EXCEPT the registered and finished spans. | ||||||
|  |         """ | ||||||
|  |         with self._lock: | ||||||
|  |             new_ctx = Context( | ||||||
|  |                 trace_id=self._parent_trace_id, | ||||||
|  |                 span_id=self._parent_span_id, | ||||||
|  |                 sampling_priority=self._sampling_priority, | ||||||
|  |             ) | ||||||
|  |             new_ctx._current_span = self._current_span | ||||||
|  |             return new_ctx | ||||||
|  | 
 | ||||||
|  |     def get_current_root_span(self): | ||||||
|  |         """ | ||||||
|  |         Return the root span of the context or None if it does not exist. | ||||||
|  |         """ | ||||||
|  |         return self._trace[0] if len(self._trace) > 0 else None | ||||||
|  | 
 | ||||||
|  |     def get_current_span(self): | ||||||
|  |         """ | ||||||
|  |         Return the last active span that corresponds to the last inserted | ||||||
|  |         item in the trace list. This cannot be considered as the current active | ||||||
|  |         span in asynchronous environments, because some spans can be closed | ||||||
|  |         earlier while child spans still need to finish their traced execution. | ||||||
|  |         """ | ||||||
|  |         with self._lock: | ||||||
|  |             return self._current_span | ||||||
|  | 
 | ||||||
|  |     def _set_current_span(self, span): | ||||||
|  |         """ | ||||||
|  |         Set current span internally. | ||||||
|  | 
 | ||||||
|  |         Non-safe if not used with a lock. For internal Context usage only. | ||||||
|  |         """ | ||||||
|  |         self._current_span = span | ||||||
|  |         if span: | ||||||
|  |             self._parent_trace_id = span.trace_id | ||||||
|  |             self._parent_span_id = span.span_id | ||||||
|  |         else: | ||||||
|  |             self._parent_span_id = None | ||||||
|  | 
 | ||||||
|  |     def add_span(self, span): | ||||||
|  |         """ | ||||||
|  |         Add a span to the context trace list, keeping it as the last active span. | ||||||
|  |         """ | ||||||
|  |         with self._lock: | ||||||
|  |             self._set_current_span(span) | ||||||
|  | 
 | ||||||
|  |             self._trace.append(span) | ||||||
|  |             span._context = self | ||||||
|  | 
 | ||||||
|  |     def close_span(self, span): | ||||||
|  |         """ | ||||||
|  |         Mark a span as a finished, increasing the internal counter to prevent | ||||||
|  |         cycles inside _trace list. | ||||||
|  |         """ | ||||||
|  |         with self._lock: | ||||||
|  |             self._finished_spans += 1 | ||||||
|  |             self._set_current_span(span._parent) | ||||||
|  | 
 | ||||||
|  |             # notify if the trace is not closed properly; this check is executed only | ||||||
|  |             # if the debug logging is enabled and when the root span is closed | ||||||
|  |             # for an unfinished trace. This logging is meant to be used for debugging | ||||||
|  |             # reasons, and it doesn't mean that the trace is wrongly generated. | ||||||
|  |             # In asynchronous environments, it's legit to close the root span before | ||||||
|  |             # some children. On the other hand, asynchronous web frameworks still expect | ||||||
|  |             # to close the root span after all the children. | ||||||
|  |             if span.tracer and span.tracer.log.isEnabledFor(logging.DEBUG) and span._parent is None: | ||||||
|  |                 unfinished_spans = [x for x in self._trace if not x.finished] | ||||||
|  |                 if unfinished_spans: | ||||||
|  |                     log.debug('Root span "%s" closed, but the trace has %d unfinished spans:', | ||||||
|  |                               span.name, len(unfinished_spans)) | ||||||
|  |                     for wrong_span in unfinished_spans: | ||||||
|  |                         log.debug('\n%s', wrong_span.pprint()) | ||||||
|  | 
 | ||||||
|  |     def _is_sampled(self): | ||||||
|  |         return any(span.sampled for span in self._trace) | ||||||
|  | 
 | ||||||
|  |     def get(self): | ||||||
|  |         """ | ||||||
|  |         Returns a tuple containing the trace list generated in the current context and | ||||||
|  |         if the context is sampled or not. It returns (None, None) if the ``Context`` is | ||||||
|  |         not finished. If a trace is returned, the ``Context`` will be reset so that it | ||||||
|  |         can be re-used immediately. | ||||||
|  | 
 | ||||||
|  |         This operation is thread-safe. | ||||||
|  |         """ | ||||||
|  |         with self._lock: | ||||||
|  |             # All spans are finished? | ||||||
|  |             if self._finished_spans == len(self._trace): | ||||||
|  |                 # get the trace | ||||||
|  |                 trace = self._trace | ||||||
|  |                 sampled = self._is_sampled() | ||||||
|  |                 sampling_priority = self._sampling_priority | ||||||
|  |                 # attach the sampling priority to the context root span | ||||||
|  |                 if sampled and sampling_priority is not None and trace: | ||||||
|  |                     trace[0].set_metric(SAMPLING_PRIORITY_KEY, sampling_priority) | ||||||
|  |                 origin = self._dd_origin | ||||||
|  |                 # attach the origin to the root span tag | ||||||
|  |                 if sampled and origin is not None and trace: | ||||||
|  |                     trace[0].set_tag(ORIGIN_KEY, origin) | ||||||
|  | 
 | ||||||
|  |                 # Set hostname tag if they requested it | ||||||
|  |                 if config.report_hostname: | ||||||
|  |                     # DEV: `get_hostname()` value is cached | ||||||
|  |                     trace[0].set_tag(HOSTNAME_KEY, hostname.get_hostname()) | ||||||
|  | 
 | ||||||
|  |                 # clean the current state | ||||||
|  |                 self._trace = [] | ||||||
|  |                 self._finished_spans = 0 | ||||||
|  |                 self._parent_trace_id = None | ||||||
|  |                 self._parent_span_id = None | ||||||
|  |                 self._sampling_priority = None | ||||||
|  |                 return trace, sampled | ||||||
|  | 
 | ||||||
|  |             elif self._partial_flush_enabled: | ||||||
|  |                 finished_spans = [t for t in self._trace if t.finished] | ||||||
|  |                 if len(finished_spans) >= self._partial_flush_min_spans: | ||||||
|  |                     # partial flush when enabled and we have more than the minimal required spans | ||||||
|  |                     trace = self._trace | ||||||
|  |                     sampled = self._is_sampled() | ||||||
|  |                     sampling_priority = self._sampling_priority | ||||||
|  |                     # attach the sampling priority to the context root span | ||||||
|  |                     if sampled and sampling_priority is not None and trace: | ||||||
|  |                         trace[0].set_metric(SAMPLING_PRIORITY_KEY, sampling_priority) | ||||||
|  |                     origin = self._dd_origin | ||||||
|  |                     # attach the origin to the root span tag | ||||||
|  |                     if sampled and origin is not None and trace: | ||||||
|  |                         trace[0].set_tag(ORIGIN_KEY, origin) | ||||||
|  | 
 | ||||||
|  |                     # Set hostname tag if they requested it | ||||||
|  |                     if config.report_hostname: | ||||||
|  |                         # DEV: `get_hostname()` value is cached | ||||||
|  |                         trace[0].set_tag(HOSTNAME_KEY, hostname.get_hostname()) | ||||||
|  | 
 | ||||||
|  |                     self._finished_spans = 0 | ||||||
|  | 
 | ||||||
|  |                     # Any open spans will remain as `self._trace` | ||||||
|  |                     # Any finished spans will get returned to be flushed | ||||||
|  |                     self._trace = [t for t in self._trace if not t.finished] | ||||||
|  | 
 | ||||||
|  |                     return finished_spans, sampled | ||||||
|  |             return None, None | ||||||
|  | @ -0,0 +1 @@ | ||||||
|  | from ..utils.importlib import func_name, module_name, require_modules  # noqa | ||||||
|  | @ -0,0 +1,30 @@ | ||||||
|  | """ | ||||||
|  | The aiobotocore integration will trace all AWS calls made with the ``aiobotocore`` | ||||||
|  | library. This integration isn't enabled when applying the default patching. | ||||||
|  | To enable it, you must run ``patch_all(aiobotocore=True)`` | ||||||
|  | 
 | ||||||
|  | :: | ||||||
|  | 
 | ||||||
|  |     import aiobotocore.session | ||||||
|  |     from ddtrace import patch | ||||||
|  | 
 | ||||||
|  |     # If not patched yet, you can patch botocore specifically | ||||||
|  |     patch(aiobotocore=True) | ||||||
|  | 
 | ||||||
|  |     # This will report spans with the default instrumentation | ||||||
|  |     aiobotocore.session.get_session() | ||||||
|  |     lambda_client = session.create_client('lambda', region_name='us-east-1') | ||||||
|  | 
 | ||||||
|  |     # This query generates a trace | ||||||
|  |     lambda_client.list_functions() | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['aiobotocore.client'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch | ||||||
|  | 
 | ||||||
|  |         __all__ = ['patch'] | ||||||
|  | @ -0,0 +1,129 @@ | ||||||
|  | import asyncio | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | from ddtrace import config | ||||||
|  | import aiobotocore.client | ||||||
|  | 
 | ||||||
|  | from aiobotocore.endpoint import ClientResponseContentProxy | ||||||
|  | 
 | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...ext import SpanTypes, http, aws | ||||||
|  | from ...compat import PYTHON_VERSION_INFO | ||||||
|  | from ...utils.formats import deep_getattr | ||||||
|  | from ...utils.wrappers import unwrap | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ARGS_NAME = ('action', 'params', 'path', 'verb') | ||||||
|  | TRACED_ARGS = ['params', 'path', 'verb'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     if getattr(aiobotocore.client, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(aiobotocore.client, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     wrapt.wrap_function_wrapper('aiobotocore.client', 'AioBaseClient._make_api_call', _wrapped_api_call) | ||||||
|  |     Pin(service='aws', app='aws').onto(aiobotocore.client.AioBaseClient) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     if getattr(aiobotocore.client, '_datadog_patch', False): | ||||||
|  |         setattr(aiobotocore.client, '_datadog_patch', False) | ||||||
|  |         unwrap(aiobotocore.client.AioBaseClient, '_make_api_call') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class WrappedClientResponseContentProxy(wrapt.ObjectProxy): | ||||||
|  |     def __init__(self, body, pin, parent_span): | ||||||
|  |         super(WrappedClientResponseContentProxy, self).__init__(body) | ||||||
|  |         self._self_pin = pin | ||||||
|  |         self._self_parent_span = parent_span | ||||||
|  | 
 | ||||||
|  |     @asyncio.coroutine | ||||||
|  |     def read(self, *args, **kwargs): | ||||||
|  |         # async read that must be child of the parent span operation | ||||||
|  |         operation_name = '{}.read'.format(self._self_parent_span.name) | ||||||
|  | 
 | ||||||
|  |         with self._self_pin.tracer.start_span(operation_name, child_of=self._self_parent_span) as span: | ||||||
|  |             # inherit parent attributes | ||||||
|  |             span.resource = self._self_parent_span.resource | ||||||
|  |             span.span_type = self._self_parent_span.span_type | ||||||
|  |             span.meta = dict(self._self_parent_span.meta) | ||||||
|  |             span.metrics = dict(self._self_parent_span.metrics) | ||||||
|  | 
 | ||||||
|  |             result = yield from self.__wrapped__.read(*args, **kwargs) | ||||||
|  |             span.set_tag('Length', len(result)) | ||||||
|  | 
 | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     # wrapt doesn't proxy `async with` context managers | ||||||
|  |     if PYTHON_VERSION_INFO >= (3, 5, 0): | ||||||
|  |         @asyncio.coroutine | ||||||
|  |         def __aenter__(self): | ||||||
|  |             # call the wrapped method but return the object proxy | ||||||
|  |             yield from self.__wrapped__.__aenter__() | ||||||
|  |             return self | ||||||
|  | 
 | ||||||
|  |         @asyncio.coroutine | ||||||
|  |         def __aexit__(self, *args, **kwargs): | ||||||
|  |             response = yield from self.__wrapped__.__aexit__(*args, **kwargs) | ||||||
|  |             return response | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @asyncio.coroutine | ||||||
|  | def _wrapped_api_call(original_func, instance, args, kwargs): | ||||||
|  |     pin = Pin.get_from(instance) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         result = yield from original_func(*args, **kwargs) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     endpoint_name = deep_getattr(instance, '_endpoint._endpoint_prefix') | ||||||
|  | 
 | ||||||
|  |     with pin.tracer.trace('{}.command'.format(endpoint_name), | ||||||
|  |                           service='{}.{}'.format(pin.service, endpoint_name), | ||||||
|  |                           span_type=SpanTypes.HTTP) as span: | ||||||
|  | 
 | ||||||
|  |         if len(args) > 0: | ||||||
|  |             operation = args[0] | ||||||
|  |             span.resource = '{}.{}'.format(endpoint_name, operation.lower()) | ||||||
|  |         else: | ||||||
|  |             operation = None | ||||||
|  |             span.resource = endpoint_name | ||||||
|  | 
 | ||||||
|  |         aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS) | ||||||
|  | 
 | ||||||
|  |         region_name = deep_getattr(instance, 'meta.region_name') | ||||||
|  | 
 | ||||||
|  |         meta = { | ||||||
|  |             'aws.agent': 'aiobotocore', | ||||||
|  |             'aws.operation': operation, | ||||||
|  |             'aws.region': region_name, | ||||||
|  |         } | ||||||
|  |         span.set_tags(meta) | ||||||
|  | 
 | ||||||
|  |         result = yield from original_func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         body = result.get('Body') | ||||||
|  |         if isinstance(body, ClientResponseContentProxy): | ||||||
|  |             result['Body'] = WrappedClientResponseContentProxy(body, pin, span) | ||||||
|  | 
 | ||||||
|  |         response_meta = result['ResponseMetadata'] | ||||||
|  |         response_headers = response_meta['HTTPHeaders'] | ||||||
|  | 
 | ||||||
|  |         span.set_tag(http.STATUS_CODE, response_meta['HTTPStatusCode']) | ||||||
|  |         span.set_tag('retry_attempts', response_meta['RetryAttempts']) | ||||||
|  | 
 | ||||||
|  |         request_id = response_meta.get('RequestId') | ||||||
|  |         if request_id: | ||||||
|  |             span.set_tag('aws.requestid', request_id) | ||||||
|  | 
 | ||||||
|  |         request_id2 = response_headers.get('x-amz-id-2') | ||||||
|  |         if request_id2: | ||||||
|  |             span.set_tag('aws.requestid2', request_id2) | ||||||
|  | 
 | ||||||
|  |         # set analytics sample rate | ||||||
|  |         span.set_tag( | ||||||
|  |             ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |             config.aiobotocore.get_analytics_sample_rate() | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         return result | ||||||
|  | @ -0,0 +1,61 @@ | ||||||
|  | """ | ||||||
|  | The ``aiohttp`` integration traces all requests defined in the application handlers. | ||||||
|  | Auto instrumentation is available using the ``trace_app`` function:: | ||||||
|  | 
 | ||||||
|  |     from aiohttp import web | ||||||
|  |     from ddtrace import tracer, patch | ||||||
|  |     from ddtrace.contrib.aiohttp import trace_app | ||||||
|  | 
 | ||||||
|  |     # patch third-party modules like aiohttp_jinja2 | ||||||
|  |     patch(aiohttp=True) | ||||||
|  | 
 | ||||||
|  |     # create your application | ||||||
|  |     app = web.Application() | ||||||
|  |     app.router.add_get('/', home_handler) | ||||||
|  | 
 | ||||||
|  |     # trace your application handlers | ||||||
|  |     trace_app(app, tracer, service='async-api') | ||||||
|  |     web.run_app(app, port=8000) | ||||||
|  | 
 | ||||||
|  | Integration settings are attached to your application under the ``datadog_trace`` | ||||||
|  | namespace. You can read or update them as follows:: | ||||||
|  | 
 | ||||||
|  |     # disables distributed tracing for all received requests | ||||||
|  |     app['datadog_trace']['distributed_tracing_enabled'] = False | ||||||
|  | 
 | ||||||
|  | Available settings are: | ||||||
|  | 
 | ||||||
|  | * ``tracer`` (default: ``ddtrace.tracer``): set the default tracer instance that is used to | ||||||
|  |   trace `aiohttp` internals. By default the `ddtrace` tracer is used. | ||||||
|  | * ``service`` (default: ``aiohttp-web``): set the service name used by the tracer. Usually | ||||||
|  |   this configuration must be updated with a meaningful name. | ||||||
|  | * ``distributed_tracing_enabled`` (default: ``True``): enable distributed tracing during | ||||||
|  |   the middleware execution, so that a new span is created with the given ``trace_id`` and | ||||||
|  |   ``parent_id`` injected via request headers. | ||||||
|  | * ``analytics_enabled`` (default: ``None``): enables APM events in Trace Search & Analytics. | ||||||
|  | 
 | ||||||
|  | Third-party modules that are currently supported by the ``patch()`` method are: | ||||||
|  | 
 | ||||||
|  | * ``aiohttp_jinja2`` | ||||||
|  | 
 | ||||||
|  | When a request span is created, a new ``Context`` for this logical execution is attached | ||||||
|  | to the ``request`` object, so that it can be used in the application code:: | ||||||
|  | 
 | ||||||
|  |     async def home_handler(request): | ||||||
|  |         ctx = request['datadog_context'] | ||||||
|  |         # do something with the tracing Context | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | required_modules = ['aiohttp'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch, unpatch | ||||||
|  |         from .middlewares import trace_app | ||||||
|  | 
 | ||||||
|  |         __all__ = [ | ||||||
|  |             'patch', | ||||||
|  |             'unpatch', | ||||||
|  |             'trace_app', | ||||||
|  |         ] | ||||||
|  | @ -0,0 +1,146 @@ | ||||||
|  | import asyncio | ||||||
|  | 
 | ||||||
|  | from ..asyncio import context_provider | ||||||
|  | from ...compat import stringify | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import SpanTypes, http | ||||||
|  | from ...propagation.http import HTTPPropagator | ||||||
|  | from ...settings import config | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | CONFIG_KEY = 'datadog_trace' | ||||||
|  | REQUEST_CONTEXT_KEY = 'datadog_context' | ||||||
|  | REQUEST_CONFIG_KEY = '__datadog_trace_config' | ||||||
|  | REQUEST_SPAN_KEY = '__datadog_request_span' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @asyncio.coroutine | ||||||
|  | def trace_middleware(app, handler): | ||||||
|  |     """ | ||||||
|  |     ``aiohttp`` middleware that traces the handler execution. | ||||||
|  |     Because handlers are run in different tasks for each request, we attach the Context | ||||||
|  |     instance both to the Task and to the Request objects. In this way: | ||||||
|  | 
 | ||||||
|  |     * the Task is used by the internal automatic instrumentation | ||||||
|  |     * the ``Context`` attached to the request can be freely used in the application code | ||||||
|  |     """ | ||||||
|  |     @asyncio.coroutine | ||||||
|  |     def attach_context(request): | ||||||
|  |         # application configs | ||||||
|  |         tracer = app[CONFIG_KEY]['tracer'] | ||||||
|  |         service = app[CONFIG_KEY]['service'] | ||||||
|  |         distributed_tracing = app[CONFIG_KEY]['distributed_tracing_enabled'] | ||||||
|  | 
 | ||||||
|  |         # Create a new context based on the propagated information. | ||||||
|  |         if distributed_tracing: | ||||||
|  |             propagator = HTTPPropagator() | ||||||
|  |             context = propagator.extract(request.headers) | ||||||
|  |             # Only need to active the new context if something was propagated | ||||||
|  |             if context.trace_id: | ||||||
|  |                 tracer.context_provider.activate(context) | ||||||
|  | 
 | ||||||
|  |         # trace the handler | ||||||
|  |         request_span = tracer.trace( | ||||||
|  |             'aiohttp.request', | ||||||
|  |             service=service, | ||||||
|  |             span_type=SpanTypes.WEB, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # Configure trace search sample rate | ||||||
|  |         # DEV: aiohttp is special case maintains separate configuration from config api | ||||||
|  |         analytics_enabled = app[CONFIG_KEY]['analytics_enabled'] | ||||||
|  |         if (config.analytics_enabled and analytics_enabled is not False) or analytics_enabled is True: | ||||||
|  |             request_span.set_tag( | ||||||
|  |                 ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |                 app[CONFIG_KEY].get('analytics_sample_rate', True) | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |         # attach the context and the root span to the request; the Context | ||||||
|  |         # may be freely used by the application code | ||||||
|  |         request[REQUEST_CONTEXT_KEY] = request_span.context | ||||||
|  |         request[REQUEST_SPAN_KEY] = request_span | ||||||
|  |         request[REQUEST_CONFIG_KEY] = app[CONFIG_KEY] | ||||||
|  |         try: | ||||||
|  |             response = yield from handler(request) | ||||||
|  |             return response | ||||||
|  |         except Exception: | ||||||
|  |             request_span.set_traceback() | ||||||
|  |             raise | ||||||
|  |     return attach_context | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @asyncio.coroutine | ||||||
|  | def on_prepare(request, response): | ||||||
|  |     """ | ||||||
|  |     The on_prepare signal is used to close the request span that is created during | ||||||
|  |     the trace middleware execution. | ||||||
|  |     """ | ||||||
|  |     # safe-guard: discard if we don't have a request span | ||||||
|  |     request_span = request.get(REQUEST_SPAN_KEY, None) | ||||||
|  |     if not request_span: | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # default resource name | ||||||
|  |     resource = stringify(response.status) | ||||||
|  | 
 | ||||||
|  |     if request.match_info.route.resource: | ||||||
|  |         # collect the resource name based on http resource type | ||||||
|  |         res_info = request.match_info.route.resource.get_info() | ||||||
|  | 
 | ||||||
|  |         if res_info.get('path'): | ||||||
|  |             resource = res_info.get('path') | ||||||
|  |         elif res_info.get('formatter'): | ||||||
|  |             resource = res_info.get('formatter') | ||||||
|  |         elif res_info.get('prefix'): | ||||||
|  |             resource = res_info.get('prefix') | ||||||
|  | 
 | ||||||
|  |         # prefix the resource name by the http method | ||||||
|  |         resource = '{} {}'.format(request.method, resource) | ||||||
|  | 
 | ||||||
|  |     if 500 <= response.status < 600: | ||||||
|  |         request_span.error = 1 | ||||||
|  | 
 | ||||||
|  |     request_span.resource = resource | ||||||
|  |     request_span.set_tag('http.method', request.method) | ||||||
|  |     request_span.set_tag('http.status_code', response.status) | ||||||
|  |     request_span.set_tag(http.URL, request.url.with_query(None)) | ||||||
|  |     # DEV: aiohttp is special case maintains separate configuration from config api | ||||||
|  |     trace_query_string = request[REQUEST_CONFIG_KEY].get('trace_query_string') | ||||||
|  |     if trace_query_string is None: | ||||||
|  |         trace_query_string = config._http.trace_query_string | ||||||
|  |     if trace_query_string: | ||||||
|  |         request_span.set_tag(http.QUERY_STRING, request.query_string) | ||||||
|  |     request_span.finish() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def trace_app(app, tracer, service='aiohttp-web'): | ||||||
|  |     """ | ||||||
|  |     Tracing function that patches the ``aiohttp`` application so that it will be | ||||||
|  |     traced using the given ``tracer``. | ||||||
|  | 
 | ||||||
|  |     :param app: aiohttp application to trace | ||||||
|  |     :param tracer: tracer instance to use | ||||||
|  |     :param service: service name of tracer | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     # safe-guard: don't trace an application twice | ||||||
|  |     if getattr(app, '__datadog_trace', False): | ||||||
|  |         return | ||||||
|  |     setattr(app, '__datadog_trace', True) | ||||||
|  | 
 | ||||||
|  |     # configure datadog settings | ||||||
|  |     app[CONFIG_KEY] = { | ||||||
|  |         'tracer': tracer, | ||||||
|  |         'service': service, | ||||||
|  |         'distributed_tracing_enabled': True, | ||||||
|  |         'analytics_enabled': None, | ||||||
|  |         'analytics_sample_rate': 1.0, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     # the tracer must work with asynchronous Context propagation | ||||||
|  |     tracer.configure(context_provider=context_provider) | ||||||
|  | 
 | ||||||
|  |     # add the async tracer middleware as a first middleware | ||||||
|  |     # and be sure that the on_prepare signal is the last one | ||||||
|  |     app.middlewares.insert(0, trace_middleware) | ||||||
|  |     app.on_response_prepare.append(on_prepare) | ||||||
|  | @ -0,0 +1,39 @@ | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | 
 | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...utils.wrappers import unwrap | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     # instrument external packages only if they're available | ||||||
|  |     import aiohttp_jinja2 | ||||||
|  |     from .template import _trace_render_template | ||||||
|  | 
 | ||||||
|  |     template_module = True | ||||||
|  | except ImportError: | ||||||
|  |     template_module = False | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """ | ||||||
|  |     Patch aiohttp third party modules: | ||||||
|  |         * aiohttp_jinja2 | ||||||
|  |     """ | ||||||
|  |     if template_module: | ||||||
|  |         if getattr(aiohttp_jinja2, '__datadog_patch', False): | ||||||
|  |             return | ||||||
|  |         setattr(aiohttp_jinja2, '__datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |         _w = wrapt.wrap_function_wrapper | ||||||
|  |         _w('aiohttp_jinja2', 'render_template', _trace_render_template) | ||||||
|  |         Pin(app='aiohttp', service=None).onto(aiohttp_jinja2) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     """ | ||||||
|  |     Remove tracing from patched modules. | ||||||
|  |     """ | ||||||
|  |     if template_module: | ||||||
|  |         if getattr(aiohttp_jinja2, '__datadog_patch', False): | ||||||
|  |             setattr(aiohttp_jinja2, '__datadog_patch', False) | ||||||
|  |             unwrap(aiohttp_jinja2, 'render_template') | ||||||
|  | @ -0,0 +1,29 @@ | ||||||
|  | import aiohttp_jinja2 | ||||||
|  | 
 | ||||||
|  | from ddtrace import Pin | ||||||
|  | 
 | ||||||
|  | from ...ext import SpanTypes | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _trace_render_template(func, module, args, kwargs): | ||||||
|  |     """ | ||||||
|  |     Trace the template rendering | ||||||
|  |     """ | ||||||
|  |     # get the module pin | ||||||
|  |     pin = Pin.get_from(aiohttp_jinja2) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     # original signature: | ||||||
|  |     # render_template(template_name, request, context, *, app_key=APP_KEY, encoding='utf-8') | ||||||
|  |     template_name = args[0] | ||||||
|  |     request = args[1] | ||||||
|  |     env = aiohttp_jinja2.get_env(request.app) | ||||||
|  | 
 | ||||||
|  |     # the prefix is available only on PackageLoader | ||||||
|  |     template_prefix = getattr(env.loader, 'package_path', '') | ||||||
|  |     template_meta = '{}/{}'.format(template_prefix, template_name) | ||||||
|  | 
 | ||||||
|  |     with pin.tracer.trace('aiohttp.template', span_type=SpanTypes.TEMPLATE) as span: | ||||||
|  |         span.set_meta('aiohttp.template', template_meta) | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | """ | ||||||
|  | Instrument aiopg to report a span for each executed Postgres queries:: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import Pin, patch | ||||||
|  |     import aiopg | ||||||
|  | 
 | ||||||
|  |     # If not patched yet, you can patch aiopg specifically | ||||||
|  |     patch(aiopg=True) | ||||||
|  | 
 | ||||||
|  |     # This will report a span with the default settings | ||||||
|  |     async with aiopg.connect(DSN) as db: | ||||||
|  |         with (await db.cursor()) as cursor: | ||||||
|  |             await cursor.execute("SELECT * FROM users WHERE id = 1") | ||||||
|  | 
 | ||||||
|  |     # Use a pin to specify metadata related to this connection | ||||||
|  |     Pin.override(db, service='postgres-users') | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['aiopg'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch | ||||||
|  | 
 | ||||||
|  |         __all__ = ['patch'] | ||||||
|  | @ -0,0 +1,97 @@ | ||||||
|  | import asyncio | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | 
 | ||||||
|  | from aiopg.utils import _ContextManager | ||||||
|  | 
 | ||||||
|  | from .. import dbapi | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import SpanTypes, sql | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...settings import config | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AIOTracedCursor(wrapt.ObjectProxy): | ||||||
|  |     """ TracedCursor wraps a psql cursor and traces its queries. """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, cursor, pin): | ||||||
|  |         super(AIOTracedCursor, self).__init__(cursor) | ||||||
|  |         pin.onto(self) | ||||||
|  |         name = pin.app or 'sql' | ||||||
|  |         self._datadog_name = '%s.query' % name | ||||||
|  | 
 | ||||||
|  |     @asyncio.coroutine | ||||||
|  |     def _trace_method(self, method, resource, extra_tags, *args, **kwargs): | ||||||
|  |         pin = Pin.get_from(self) | ||||||
|  |         if not pin or not pin.enabled(): | ||||||
|  |             result = yield from method(*args, **kwargs) | ||||||
|  |             return result | ||||||
|  |         service = pin.service | ||||||
|  | 
 | ||||||
|  |         with pin.tracer.trace(self._datadog_name, service=service, | ||||||
|  |                               resource=resource, span_type=SpanTypes.SQL) as s: | ||||||
|  |             s.set_tag(sql.QUERY, resource) | ||||||
|  |             s.set_tags(pin.tags) | ||||||
|  |             s.set_tags(extra_tags) | ||||||
|  | 
 | ||||||
|  |             # set analytics sample rate | ||||||
|  |             s.set_tag( | ||||||
|  |                 ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |                 config.aiopg.get_analytics_sample_rate() | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             try: | ||||||
|  |                 result = yield from method(*args, **kwargs) | ||||||
|  |                 return result | ||||||
|  |             finally: | ||||||
|  |                 s.set_metric('db.rowcount', self.rowcount) | ||||||
|  | 
 | ||||||
|  |     @asyncio.coroutine | ||||||
|  |     def executemany(self, query, *args, **kwargs): | ||||||
|  |         # FIXME[matt] properly handle kwargs here. arg names can be different | ||||||
|  |         # with different libs. | ||||||
|  |         result = yield from self._trace_method( | ||||||
|  |             self.__wrapped__.executemany, query, {'sql.executemany': 'true'}, | ||||||
|  |             query, *args, **kwargs) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     @asyncio.coroutine | ||||||
|  |     def execute(self, query, *args, **kwargs): | ||||||
|  |         result = yield from self._trace_method( | ||||||
|  |             self.__wrapped__.execute, query, {}, query, *args, **kwargs) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     @asyncio.coroutine | ||||||
|  |     def callproc(self, proc, args): | ||||||
|  |         result = yield from self._trace_method( | ||||||
|  |             self.__wrapped__.callproc, proc, {}, proc, args) | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  |     def __aiter__(self): | ||||||
|  |         return self.__wrapped__.__aiter__() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AIOTracedConnection(wrapt.ObjectProxy): | ||||||
|  |     """ TracedConnection wraps a Connection with tracing code. """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, conn, pin=None, cursor_cls=AIOTracedCursor): | ||||||
|  |         super(AIOTracedConnection, self).__init__(conn) | ||||||
|  |         name = dbapi._get_vendor(conn) | ||||||
|  |         db_pin = pin or Pin(service=name, app=name) | ||||||
|  |         db_pin.onto(self) | ||||||
|  |         # wrapt requires prefix of `_self` for attributes that are only in the | ||||||
|  |         # proxy (since some of our source objects will use `__slots__`) | ||||||
|  |         self._self_cursor_cls = cursor_cls | ||||||
|  | 
 | ||||||
|  |     def cursor(self, *args, **kwargs): | ||||||
|  |         # unfortunately we also need to patch this method as otherwise "self" | ||||||
|  |         # ends up being the aiopg connection object | ||||||
|  |         coro = self._cursor(*args, **kwargs) | ||||||
|  |         return _ContextManager(coro) | ||||||
|  | 
 | ||||||
|  |     @asyncio.coroutine | ||||||
|  |     def _cursor(self, *args, **kwargs): | ||||||
|  |         cursor = yield from self.__wrapped__._cursor(*args, **kwargs) | ||||||
|  |         pin = Pin.get_from(self) | ||||||
|  |         if not pin: | ||||||
|  |             return cursor | ||||||
|  |         return self._self_cursor_cls(cursor, pin) | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | # 3p | ||||||
|  | import asyncio | ||||||
|  | 
 | ||||||
|  | import aiopg.connection | ||||||
|  | import psycopg2.extensions | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | 
 | ||||||
|  | from .connection import AIOTracedConnection | ||||||
|  | from ..psycopg.patch import _patch_extensions, \ | ||||||
|  |     _unpatch_extensions, patch_conn as psycopg_patch_conn | ||||||
|  | from ...utils.wrappers import unwrap as _u | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """ Patch monkey patches psycopg's connection function | ||||||
|  |         so that the connection's functions are traced. | ||||||
|  |     """ | ||||||
|  |     if getattr(aiopg, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(aiopg, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     wrapt.wrap_function_wrapper(aiopg.connection, '_connect', patched_connect) | ||||||
|  |     _patch_extensions(_aiopg_extensions)  # do this early just in case | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     if getattr(aiopg, '_datadog_patch', False): | ||||||
|  |         setattr(aiopg, '_datadog_patch', False) | ||||||
|  |         _u(aiopg.connection, '_connect') | ||||||
|  |         _unpatch_extensions(_aiopg_extensions) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @asyncio.coroutine | ||||||
|  | def patched_connect(connect_func, _, args, kwargs): | ||||||
|  |     conn = yield from connect_func(*args, **kwargs) | ||||||
|  |     return psycopg_patch_conn(conn, traced_conn_cls=AIOTracedConnection) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _extensions_register_type(func, _, args, kwargs): | ||||||
|  |     def _unroll_args(obj, scope=None): | ||||||
|  |         return obj, scope | ||||||
|  |     obj, scope = _unroll_args(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     # register_type performs a c-level check of the object | ||||||
|  |     # type so we must be sure to pass in the actual db connection | ||||||
|  |     if scope and isinstance(scope, wrapt.ObjectProxy): | ||||||
|  |         scope = scope.__wrapped__._conn | ||||||
|  | 
 | ||||||
|  |     return func(obj, scope) if scope else func(obj) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # extension hooks | ||||||
|  | _aiopg_extensions = [ | ||||||
|  |     (psycopg2.extensions.register_type, | ||||||
|  |      psycopg2.extensions, 'register_type', | ||||||
|  |      _extensions_register_type), | ||||||
|  | ] | ||||||
|  | @ -0,0 +1,32 @@ | ||||||
|  | """ | ||||||
|  | The Algoliasearch__ integration will add tracing to your Algolia searches. | ||||||
|  | 
 | ||||||
|  | :: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import patch_all | ||||||
|  |     patch_all() | ||||||
|  | 
 | ||||||
|  |     from algoliasearch import algoliasearch | ||||||
|  |     client = alogliasearch.Client(<ID>, <API_KEY>) | ||||||
|  |     index = client.init_index(<INDEX_NAME>) | ||||||
|  |     index.search("your query", args={"attributesToRetrieve": "attribute1,attribute1"}) | ||||||
|  | 
 | ||||||
|  | Configuration | ||||||
|  | ~~~~~~~~~~~~~ | ||||||
|  | 
 | ||||||
|  | .. py:data:: ddtrace.config.algoliasearch['collect_query_text'] | ||||||
|  | 
 | ||||||
|  |    Whether to pass the text of your query onto Datadog. Since this may contain sensitive data it's off by default | ||||||
|  | 
 | ||||||
|  |    Default: ``False`` | ||||||
|  | 
 | ||||||
|  | .. __: https://www.algolia.com | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | with require_modules(['algoliasearch', 'algoliasearch.version']) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch, unpatch | ||||||
|  | 
 | ||||||
|  |         __all__ = ['patch', 'unpatch'] | ||||||
|  | @ -0,0 +1,143 @@ | ||||||
|  | from ddtrace.pin import Pin | ||||||
|  | from ddtrace.settings import config | ||||||
|  | from ddtrace.utils.wrappers import unwrap as _u | ||||||
|  | from ddtrace.vendor.wrapt import wrap_function_wrapper as _w | ||||||
|  | 
 | ||||||
|  | DD_PATCH_ATTR = '_datadog_patch' | ||||||
|  | 
 | ||||||
|  | SERVICE_NAME = 'algoliasearch' | ||||||
|  | APP_NAME = 'algoliasearch' | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     import algoliasearch | ||||||
|  |     from algoliasearch.version import VERSION | ||||||
|  |     algoliasearch_version = tuple([int(i) for i in VERSION.split('.')]) | ||||||
|  | 
 | ||||||
|  |     # Default configuration | ||||||
|  |     config._add('algoliasearch', dict( | ||||||
|  |         service_name=SERVICE_NAME, | ||||||
|  |         collect_query_text=False | ||||||
|  |     )) | ||||||
|  | except ImportError: | ||||||
|  |     algoliasearch_version = (0, 0) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     if algoliasearch_version == (0, 0): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     if getattr(algoliasearch, DD_PATCH_ATTR, False): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     setattr(algoliasearch, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     pin = Pin( | ||||||
|  |         service=config.algoliasearch.service_name, app=APP_NAME | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     if algoliasearch_version < (2, 0) and algoliasearch_version >= (1, 0): | ||||||
|  |         _w(algoliasearch.index, 'Index.search', _patched_search) | ||||||
|  |         pin.onto(algoliasearch.index.Index) | ||||||
|  |     elif algoliasearch_version >= (2, 0) and algoliasearch_version < (3, 0): | ||||||
|  |         from algoliasearch import search_index | ||||||
|  |         _w(algoliasearch, 'search_index.SearchIndex.search', _patched_search) | ||||||
|  |         pin.onto(search_index.SearchIndex) | ||||||
|  |     else: | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     if algoliasearch_version == (0, 0): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     if getattr(algoliasearch, DD_PATCH_ATTR, False): | ||||||
|  |         setattr(algoliasearch, DD_PATCH_ATTR, False) | ||||||
|  | 
 | ||||||
|  |     if algoliasearch_version < (2, 0) and algoliasearch_version >= (1, 0): | ||||||
|  |         _u(algoliasearch.index.Index, 'search') | ||||||
|  |     elif algoliasearch_version >= (2, 0) and algoliasearch_version < (3, 0): | ||||||
|  |         from algoliasearch import search_index | ||||||
|  |         _u(search_index.SearchIndex, 'search') | ||||||
|  |     else: | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # DEV: this maps serves the dual purpose of enumerating the algoliasearch.search() query_args that | ||||||
|  | # will be sent along as tags, as well as converting arguments names into tag names compliant with | ||||||
|  | # tag naming recommendations set out here: https://docs.datadoghq.com/tagging/ | ||||||
|  | QUERY_ARGS_DD_TAG_MAP = { | ||||||
|  |     'page': 'page', | ||||||
|  |     'hitsPerPage': 'hits_per_page', | ||||||
|  |     'attributesToRetrieve': 'attributes_to_retrieve', | ||||||
|  |     'attributesToHighlight': 'attributes_to_highlight', | ||||||
|  |     'attributesToSnippet': 'attributes_to_snippet', | ||||||
|  |     'minWordSizefor1Typo': 'min_word_size_for_1_typo', | ||||||
|  |     'minWordSizefor2Typos': 'min_word_size_for_2_typos', | ||||||
|  |     'getRankingInfo': 'get_ranking_info', | ||||||
|  |     'aroundLatLng': 'around_lat_lng', | ||||||
|  |     'numericFilters': 'numeric_filters', | ||||||
|  |     'tagFilters': 'tag_filters', | ||||||
|  |     'queryType': 'query_type', | ||||||
|  |     'optionalWords': 'optional_words', | ||||||
|  |     'distinct': 'distinct' | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _patched_search(func, instance, wrapt_args, wrapt_kwargs): | ||||||
|  |     """ | ||||||
|  |         wrapt_args is called the way it is to distinguish it from the 'args' | ||||||
|  |         argument to the algoliasearch.index.Index.search() method. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     if algoliasearch_version < (2, 0) and algoliasearch_version >= (1, 0): | ||||||
|  |         function_query_arg_name = 'args' | ||||||
|  |     elif algoliasearch_version >= (2, 0) and algoliasearch_version < (3, 0): | ||||||
|  |         function_query_arg_name = 'request_options' | ||||||
|  |     else: | ||||||
|  |         return func(*wrapt_args, **wrapt_kwargs) | ||||||
|  | 
 | ||||||
|  |     pin = Pin.get_from(instance) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return func(*wrapt_args, **wrapt_kwargs) | ||||||
|  | 
 | ||||||
|  |     with pin.tracer.trace('algoliasearch.search', service=pin.service) as span: | ||||||
|  |         if not span.sampled: | ||||||
|  |             return func(*wrapt_args, **wrapt_kwargs) | ||||||
|  | 
 | ||||||
|  |         if config.algoliasearch.collect_query_text: | ||||||
|  |             span.set_tag('query.text', wrapt_kwargs.get('query', wrapt_args[0])) | ||||||
|  | 
 | ||||||
|  |         query_args = wrapt_kwargs.get(function_query_arg_name, wrapt_args[1] if len(wrapt_args) > 1 else None) | ||||||
|  | 
 | ||||||
|  |         if query_args and isinstance(query_args, dict): | ||||||
|  |             for query_arg, tag_name in QUERY_ARGS_DD_TAG_MAP.items(): | ||||||
|  |                 value = query_args.get(query_arg) | ||||||
|  |                 if value is not None: | ||||||
|  |                     span.set_tag('query.args.{}'.format(tag_name), value) | ||||||
|  | 
 | ||||||
|  |         # Result would look like this | ||||||
|  |         # { | ||||||
|  |         #   'hits': [ | ||||||
|  |         #     { | ||||||
|  |         #       .... your search results ... | ||||||
|  |         #     } | ||||||
|  |         #   ], | ||||||
|  |         #   'processingTimeMS': 1, | ||||||
|  |         #   'nbHits': 1, | ||||||
|  |         #   'hitsPerPage': 20, | ||||||
|  |         #   'exhaustiveNbHits': true, | ||||||
|  |         #   'params': 'query=xxx', | ||||||
|  |         #   'nbPages': 1, | ||||||
|  |         #   'query': 'xxx', | ||||||
|  |         #   'page': 0 | ||||||
|  |         # } | ||||||
|  |         result = func(*wrapt_args, **wrapt_kwargs) | ||||||
|  | 
 | ||||||
|  |         if isinstance(result, dict): | ||||||
|  |             if result.get('processingTimeMS', None) is not None: | ||||||
|  |                 span.set_metric('processing_time_ms', int(result['processingTimeMS'])) | ||||||
|  | 
 | ||||||
|  |             if result.get('nbHits', None) is not None: | ||||||
|  |                 span.set_metric('number_of_hits', int(result['nbHits'])) | ||||||
|  | 
 | ||||||
|  |         return result | ||||||
|  | @ -0,0 +1,72 @@ | ||||||
|  | """ | ||||||
|  | This integration provides the ``AsyncioContextProvider`` that follows the execution | ||||||
|  | flow of a ``Task``, making possible to trace asynchronous code built on top | ||||||
|  | of ``asyncio``. To trace asynchronous execution, you must:: | ||||||
|  | 
 | ||||||
|  |     import asyncio | ||||||
|  |     from ddtrace import tracer | ||||||
|  |     from ddtrace.contrib.asyncio import context_provider | ||||||
|  | 
 | ||||||
|  |     # enable asyncio support | ||||||
|  |     tracer.configure(context_provider=context_provider) | ||||||
|  | 
 | ||||||
|  |     async def some_work(): | ||||||
|  |         with tracer.trace('asyncio.some_work'): | ||||||
|  |             # do something | ||||||
|  | 
 | ||||||
|  |     # launch your coroutines as usual | ||||||
|  |     loop = asyncio.get_event_loop() | ||||||
|  |     loop.run_until_complete(some_work()) | ||||||
|  |     loop.close() | ||||||
|  | 
 | ||||||
|  | If ``contextvars`` is available, we use the | ||||||
|  | :class:`ddtrace.provider.DefaultContextProvider`, otherwise we use the legacy | ||||||
|  | :class:`ddtrace.contrib.asyncio.provider.AsyncioContextProvider`. | ||||||
|  | 
 | ||||||
|  | In addition, helpers are provided to simplify how the tracing ``Context`` is | ||||||
|  | handled between scheduled coroutines and ``Future`` invoked in separated | ||||||
|  | threads: | ||||||
|  | 
 | ||||||
|  |     * ``set_call_context(task, ctx)``: attach the context to the given ``Task`` | ||||||
|  |       so that it will be available from the ``tracer.get_call_context()`` | ||||||
|  |     * ``ensure_future(coro_or_future, *, loop=None)``: wrapper for the | ||||||
|  |       ``asyncio.ensure_future`` that attaches the current context to a new | ||||||
|  |       ``Task`` instance | ||||||
|  |     * ``run_in_executor(loop, executor, func, *args)``: wrapper for the | ||||||
|  |       ``loop.run_in_executor`` that attaches the current context to the | ||||||
|  |       new thread so that the trace can be resumed regardless when | ||||||
|  |       it's executed | ||||||
|  |     * ``create_task(coro)``: creates a new asyncio ``Task`` that inherits | ||||||
|  |       the current active ``Context`` so that generated traces in the new task | ||||||
|  |       are attached to the main trace | ||||||
|  | 
 | ||||||
|  | A ``patch(asyncio=True)`` is available if you want to automatically use above | ||||||
|  | wrappers without changing your code. In that case, the patch method **must be | ||||||
|  | called before** importing stdlib functions. | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['asyncio'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .provider import AsyncioContextProvider | ||||||
|  |         from ...internal.context_manager import CONTEXTVARS_IS_AVAILABLE | ||||||
|  |         from ...provider import DefaultContextProvider | ||||||
|  | 
 | ||||||
|  |         if CONTEXTVARS_IS_AVAILABLE: | ||||||
|  |             context_provider = DefaultContextProvider() | ||||||
|  |         else: | ||||||
|  |             context_provider = AsyncioContextProvider() | ||||||
|  | 
 | ||||||
|  |         from .helpers import set_call_context, ensure_future, run_in_executor | ||||||
|  |         from .patch import patch | ||||||
|  | 
 | ||||||
|  |         __all__ = [ | ||||||
|  |             'context_provider', | ||||||
|  |             'set_call_context', | ||||||
|  |             'ensure_future', | ||||||
|  |             'run_in_executor', | ||||||
|  |             'patch' | ||||||
|  |         ] | ||||||
|  | @ -0,0 +1,9 @@ | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | # asyncio.Task.current_task method is deprecated and will be removed in Python | ||||||
|  | # 3.9. Instead use asyncio.current_task | ||||||
|  | if sys.version_info >= (3, 7, 0): | ||||||
|  |     from asyncio import current_task as asyncio_current_task | ||||||
|  | else: | ||||||
|  |     import asyncio | ||||||
|  |     asyncio_current_task = asyncio.Task.current_task | ||||||
|  | @ -0,0 +1,83 @@ | ||||||
|  | """ | ||||||
|  | This module includes a list of convenience methods that | ||||||
|  | can be used to simplify some operations while handling | ||||||
|  | Context and Spans in instrumented ``asyncio`` code. | ||||||
|  | """ | ||||||
|  | import asyncio | ||||||
|  | import ddtrace | ||||||
|  | 
 | ||||||
|  | from .provider import CONTEXT_ATTR | ||||||
|  | from .wrappers import wrapped_create_task | ||||||
|  | from ...context import Context | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def set_call_context(task, ctx): | ||||||
|  |     """ | ||||||
|  |     Updates the ``Context`` for the given Task. Useful when you need to | ||||||
|  |     pass the context among different tasks. | ||||||
|  | 
 | ||||||
|  |     This method is available for backward-compatibility. Use the | ||||||
|  |     ``AsyncioContextProvider`` API to set the current active ``Context``. | ||||||
|  |     """ | ||||||
|  |     setattr(task, CONTEXT_ATTR, ctx) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def ensure_future(coro_or_future, *, loop=None, tracer=None): | ||||||
|  |     """Wrapper that sets a context to the newly created Task. | ||||||
|  | 
 | ||||||
|  |     If the current task already has a Context, it will be attached to the new Task so the Trace list will be preserved. | ||||||
|  |     """ | ||||||
|  |     tracer = tracer or ddtrace.tracer | ||||||
|  |     current_ctx = tracer.get_call_context() | ||||||
|  |     task = asyncio.ensure_future(coro_or_future, loop=loop) | ||||||
|  |     set_call_context(task, current_ctx) | ||||||
|  |     return task | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def run_in_executor(loop, executor, func, *args, tracer=None): | ||||||
|  |     """Wrapper function that sets a context to the newly created Thread. | ||||||
|  | 
 | ||||||
|  |     If the current task has a Context, it will be attached as an empty Context with the current_span activated to | ||||||
|  |     inherit the ``trace_id`` and the ``parent_id``. | ||||||
|  | 
 | ||||||
|  |     Because the Executor can run the Thread immediately or after the | ||||||
|  |     coroutine is executed, we may have two different scenarios: | ||||||
|  |     * the Context is copied in the new Thread and the trace is sent twice | ||||||
|  |     * the coroutine flushes the Context and when the Thread copies the | ||||||
|  |     Context it is already empty (so it will be a root Span) | ||||||
|  | 
 | ||||||
|  |     To support both situations, we create a new Context that knows only what was | ||||||
|  |     the latest active Span when the new thread was created. In this new thread, | ||||||
|  |     we fallback to the thread-local ``Context`` storage. | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  |     tracer = tracer or ddtrace.tracer | ||||||
|  |     ctx = Context() | ||||||
|  |     current_ctx = tracer.get_call_context() | ||||||
|  |     ctx._current_span = current_ctx._current_span | ||||||
|  | 
 | ||||||
|  |     # prepare the future using an executor wrapper | ||||||
|  |     future = loop.run_in_executor(executor, _wrap_executor, func, args, tracer, ctx) | ||||||
|  |     return future | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _wrap_executor(fn, args, tracer, ctx): | ||||||
|  |     """ | ||||||
|  |     This function is executed in the newly created Thread so the right | ||||||
|  |     ``Context`` can be set in the thread-local storage. This operation | ||||||
|  |     is safe because the ``Context`` class is thread-safe and can be | ||||||
|  |     updated concurrently. | ||||||
|  |     """ | ||||||
|  |     # the AsyncioContextProvider knows that this is a new thread | ||||||
|  |     # so it is legit to pass the Context in the thread-local storage; | ||||||
|  |     # fn() will be executed outside the asyncio loop as a synchronous code | ||||||
|  |     tracer.context_provider.activate(ctx) | ||||||
|  |     return fn(*args) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def create_task(*args, **kwargs): | ||||||
|  |     """This function spawns a task with a Context that inherits the | ||||||
|  |     `trace_id` and the `parent_id` from the current active one if available. | ||||||
|  |     """ | ||||||
|  |     loop = asyncio.get_event_loop() | ||||||
|  |     return wrapped_create_task(loop.create_task, None, args, kwargs) | ||||||
|  | @ -0,0 +1,32 @@ | ||||||
|  | import asyncio | ||||||
|  | 
 | ||||||
|  | from ddtrace.vendor.wrapt import wrap_function_wrapper as _w | ||||||
|  | 
 | ||||||
|  | from ...internal.context_manager import CONTEXTVARS_IS_AVAILABLE | ||||||
|  | from .wrappers import wrapped_create_task, wrapped_create_task_contextvars | ||||||
|  | from ...utils.wrappers import unwrap as _u | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """Patches current loop `create_task()` method to enable spawned tasks to | ||||||
|  |     parent to the base task context. | ||||||
|  |     """ | ||||||
|  |     if getattr(asyncio, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(asyncio, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     loop = asyncio.get_event_loop() | ||||||
|  |     if CONTEXTVARS_IS_AVAILABLE: | ||||||
|  |         _w(loop, 'create_task', wrapped_create_task_contextvars) | ||||||
|  |     else: | ||||||
|  |         _w(loop, 'create_task', wrapped_create_task) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     """Remove tracing from patched modules.""" | ||||||
|  | 
 | ||||||
|  |     if getattr(asyncio, '_datadog_patch', False): | ||||||
|  |         setattr(asyncio, '_datadog_patch', False) | ||||||
|  | 
 | ||||||
|  |     loop = asyncio.get_event_loop() | ||||||
|  |     _u(loop, 'create_task') | ||||||
|  | @ -0,0 +1,86 @@ | ||||||
|  | import asyncio | ||||||
|  | 
 | ||||||
|  | from ...context import Context | ||||||
|  | from ...provider import DefaultContextProvider | ||||||
|  | 
 | ||||||
|  | # Task attribute used to set/get the Context instance | ||||||
|  | CONTEXT_ATTR = '__datadog_context' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class AsyncioContextProvider(DefaultContextProvider): | ||||||
|  |     """ | ||||||
|  |     Context provider that retrieves all contexts for the current asyncio | ||||||
|  |     execution. It must be used in asynchronous programming that relies | ||||||
|  |     in the built-in ``asyncio`` library. Framework instrumentation that | ||||||
|  |     is built on top of the ``asyncio`` library, can use this provider. | ||||||
|  | 
 | ||||||
|  |     This Context Provider inherits from ``DefaultContextProvider`` because | ||||||
|  |     it uses a thread-local storage when the ``Context`` is propagated to | ||||||
|  |     a different thread, than the one that is running the async loop. | ||||||
|  |     """ | ||||||
|  |     def activate(self, context, loop=None): | ||||||
|  |         """Sets the scoped ``Context`` for the current running ``Task``. | ||||||
|  |         """ | ||||||
|  |         loop = self._get_loop(loop) | ||||||
|  |         if not loop: | ||||||
|  |             self._local.set(context) | ||||||
|  |             return context | ||||||
|  | 
 | ||||||
|  |         # the current unit of work (if tasks are used) | ||||||
|  |         task = asyncio.Task.current_task(loop=loop) | ||||||
|  |         setattr(task, CONTEXT_ATTR, context) | ||||||
|  |         return context | ||||||
|  | 
 | ||||||
|  |     def _get_loop(self, loop=None): | ||||||
|  |         """Helper to try and resolve the current loop""" | ||||||
|  |         try: | ||||||
|  |             return loop or asyncio.get_event_loop() | ||||||
|  |         except RuntimeError: | ||||||
|  |             # Detects if a loop is available in the current thread; | ||||||
|  |             # DEV: This happens when a new thread is created from the out that is running the async loop | ||||||
|  |             # DEV: It's possible that a different Executor is handling a different Thread that | ||||||
|  |             #      works with blocking code. In that case, we fallback to a thread-local Context. | ||||||
|  |             pass | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     def _has_active_context(self, loop=None): | ||||||
|  |         """Helper to determine if we have a currently active context""" | ||||||
|  |         loop = self._get_loop(loop=loop) | ||||||
|  |         if loop is None: | ||||||
|  |             return self._local._has_active_context() | ||||||
|  | 
 | ||||||
|  |         # the current unit of work (if tasks are used) | ||||||
|  |         task = asyncio.Task.current_task(loop=loop) | ||||||
|  |         if task is None: | ||||||
|  |             return False | ||||||
|  | 
 | ||||||
|  |         ctx = getattr(task, CONTEXT_ATTR, None) | ||||||
|  |         return ctx is not None | ||||||
|  | 
 | ||||||
|  |     def active(self, loop=None): | ||||||
|  |         """ | ||||||
|  |         Returns the scoped Context for this execution flow. The ``Context`` uses | ||||||
|  |         the current task as a carrier so if a single task is used for the entire application, | ||||||
|  |         the context must be handled separately. | ||||||
|  |         """ | ||||||
|  |         loop = self._get_loop(loop=loop) | ||||||
|  |         if not loop: | ||||||
|  |             return self._local.get() | ||||||
|  | 
 | ||||||
|  |         # the current unit of work (if tasks are used) | ||||||
|  |         task = asyncio.Task.current_task(loop=loop) | ||||||
|  |         if task is None: | ||||||
|  |             # providing a detached Context from the current Task, may lead to | ||||||
|  |             # wrong traces. This defensive behavior grants that a trace can | ||||||
|  |             # still be built without raising exceptions | ||||||
|  |             return Context() | ||||||
|  | 
 | ||||||
|  |         ctx = getattr(task, CONTEXT_ATTR, None) | ||||||
|  |         if ctx is not None: | ||||||
|  |             # return the active Context for this task (if any) | ||||||
|  |             return ctx | ||||||
|  | 
 | ||||||
|  |         # create a new Context using the Task as a Context carrier | ||||||
|  |         ctx = Context() | ||||||
|  |         setattr(task, CONTEXT_ATTR, ctx) | ||||||
|  |         return ctx | ||||||
|  | @ -0,0 +1,58 @@ | ||||||
|  | import ddtrace | ||||||
|  | 
 | ||||||
|  | from .compat import asyncio_current_task | ||||||
|  | from .provider import CONTEXT_ATTR | ||||||
|  | from ...context import Context | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def wrapped_create_task(wrapped, instance, args, kwargs): | ||||||
|  |     """Wrapper for ``create_task(coro)`` that propagates the current active | ||||||
|  |     ``Context`` to the new ``Task``. This function is useful to connect traces | ||||||
|  |     of detached executions. | ||||||
|  | 
 | ||||||
|  |     Note: we can't just link the task contexts due to the following scenario: | ||||||
|  |         * begin task A | ||||||
|  |         * task A starts task B1..B10 | ||||||
|  |         * finish task B1-B9 (B10 still on trace stack) | ||||||
|  |         * task A starts task C | ||||||
|  |         * now task C gets parented to task B10 since it's still on the stack, | ||||||
|  |           however was not actually triggered by B10 | ||||||
|  |     """ | ||||||
|  |     new_task = wrapped(*args, **kwargs) | ||||||
|  |     current_task = asyncio_current_task() | ||||||
|  | 
 | ||||||
|  |     ctx = getattr(current_task, CONTEXT_ATTR, None) | ||||||
|  |     if ctx: | ||||||
|  |         # current task has a context, so parent a new context to the base context | ||||||
|  |         new_ctx = Context( | ||||||
|  |             trace_id=ctx.trace_id, | ||||||
|  |             span_id=ctx.span_id, | ||||||
|  |             sampling_priority=ctx.sampling_priority, | ||||||
|  |         ) | ||||||
|  |         setattr(new_task, CONTEXT_ATTR, new_ctx) | ||||||
|  | 
 | ||||||
|  |     return new_task | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def wrapped_create_task_contextvars(wrapped, instance, args, kwargs): | ||||||
|  |     """Wrapper for ``create_task(coro)`` that propagates the current active | ||||||
|  |     ``Context`` to the new ``Task``. This function is useful to connect traces | ||||||
|  |     of detached executions. Uses contextvars for task-local storage. | ||||||
|  |     """ | ||||||
|  |     current_task_ctx = ddtrace.tracer.get_call_context() | ||||||
|  | 
 | ||||||
|  |     if not current_task_ctx: | ||||||
|  |         # no current context exists so nothing special to be done in handling | ||||||
|  |         # context for new task | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     # clone and activate current task's context for new task to support | ||||||
|  |     # detached executions | ||||||
|  |     new_task_ctx = current_task_ctx.clone() | ||||||
|  |     ddtrace.tracer.context_provider.activate(new_task_ctx) | ||||||
|  |     try: | ||||||
|  |         # activated context will now be copied to new task | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  |     finally: | ||||||
|  |         # reactivate current task context | ||||||
|  |         ddtrace.tracer.context_provider.activate(current_task_ctx) | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | """ | ||||||
|  | Boto integration will trace all AWS calls made via boto2. | ||||||
|  | This integration is automatically patched when using ``patch_all()``:: | ||||||
|  | 
 | ||||||
|  |     import boto.ec2 | ||||||
|  |     from ddtrace import patch | ||||||
|  | 
 | ||||||
|  |     # If not patched yet, you can patch boto specifically | ||||||
|  |     patch(boto=True) | ||||||
|  | 
 | ||||||
|  |     # This will report spans with the default instrumentation | ||||||
|  |     ec2 = boto.ec2.connect_to_region("us-west-2") | ||||||
|  |     # Example of instrumented query | ||||||
|  |     ec2.get_all_instances() | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | required_modules = ['boto.connection'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch | ||||||
|  |         __all__ = ['patch'] | ||||||
|  | @ -0,0 +1,183 @@ | ||||||
|  | import boto.connection | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | import inspect | ||||||
|  | 
 | ||||||
|  | from ddtrace import config | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...ext import SpanTypes, http, aws | ||||||
|  | from ...utils.wrappers import unwrap | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Original boto client class | ||||||
|  | _Boto_client = boto.connection.AWSQueryConnection | ||||||
|  | 
 | ||||||
|  | AWS_QUERY_ARGS_NAME = ('operation_name', 'params', 'path', 'verb') | ||||||
|  | AWS_AUTH_ARGS_NAME = ( | ||||||
|  |     'method', | ||||||
|  |     'path', | ||||||
|  |     'headers', | ||||||
|  |     'data', | ||||||
|  |     'host', | ||||||
|  |     'auth_path', | ||||||
|  |     'sender', | ||||||
|  | ) | ||||||
|  | AWS_QUERY_TRACED_ARGS = ['operation_name', 'params', 'path'] | ||||||
|  | AWS_AUTH_TRACED_ARGS = ['path', 'data', 'host'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     if getattr(boto.connection, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(boto.connection, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     # AWSQueryConnection and AWSAuthConnection are two different classes called by | ||||||
|  |     # different services for connection. | ||||||
|  |     # For exemple EC2 uses AWSQueryConnection and S3 uses AWSAuthConnection | ||||||
|  |     wrapt.wrap_function_wrapper( | ||||||
|  |         'boto.connection', 'AWSQueryConnection.make_request', patched_query_request | ||||||
|  |     ) | ||||||
|  |     wrapt.wrap_function_wrapper( | ||||||
|  |         'boto.connection', 'AWSAuthConnection.make_request', patched_auth_request | ||||||
|  |     ) | ||||||
|  |     Pin(service='aws', app='aws').onto( | ||||||
|  |         boto.connection.AWSQueryConnection | ||||||
|  |     ) | ||||||
|  |     Pin(service='aws', app='aws').onto( | ||||||
|  |         boto.connection.AWSAuthConnection | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     if getattr(boto.connection, '_datadog_patch', False): | ||||||
|  |         setattr(boto.connection, '_datadog_patch', False) | ||||||
|  |         unwrap(boto.connection.AWSQueryConnection, 'make_request') | ||||||
|  |         unwrap(boto.connection.AWSAuthConnection, 'make_request') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # ec2, sqs, kinesis | ||||||
|  | def patched_query_request(original_func, instance, args, kwargs): | ||||||
|  | 
 | ||||||
|  |     pin = Pin.get_from(instance) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return original_func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     endpoint_name = getattr(instance, 'host').split('.')[0] | ||||||
|  | 
 | ||||||
|  |     with pin.tracer.trace( | ||||||
|  |         '{}.command'.format(endpoint_name), | ||||||
|  |         service='{}.{}'.format(pin.service, endpoint_name), | ||||||
|  |         span_type=SpanTypes.HTTP, | ||||||
|  |     ) as span: | ||||||
|  | 
 | ||||||
|  |         operation_name = None | ||||||
|  |         if args: | ||||||
|  |             operation_name = args[0] | ||||||
|  |             span.resource = '%s.%s' % (endpoint_name, operation_name.lower()) | ||||||
|  |         else: | ||||||
|  |             span.resource = endpoint_name | ||||||
|  | 
 | ||||||
|  |         aws.add_span_arg_tags(span, endpoint_name, args, AWS_QUERY_ARGS_NAME, AWS_QUERY_TRACED_ARGS) | ||||||
|  | 
 | ||||||
|  |         # Obtaining region name | ||||||
|  |         region_name = _get_instance_region_name(instance) | ||||||
|  | 
 | ||||||
|  |         meta = { | ||||||
|  |             aws.AGENT: 'boto', | ||||||
|  |             aws.OPERATION: operation_name, | ||||||
|  |         } | ||||||
|  |         if region_name: | ||||||
|  |             meta[aws.REGION] = region_name | ||||||
|  | 
 | ||||||
|  |         span.set_tags(meta) | ||||||
|  | 
 | ||||||
|  |         # Original func returns a boto.connection.HTTPResponse object | ||||||
|  |         result = original_func(*args, **kwargs) | ||||||
|  |         span.set_tag(http.STATUS_CODE, getattr(result, 'status')) | ||||||
|  |         span.set_tag(http.METHOD, getattr(result, '_method')) | ||||||
|  | 
 | ||||||
|  |         # set analytics sample rate | ||||||
|  |         span.set_tag( | ||||||
|  |             ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |             config.boto.get_analytics_sample_rate() | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # s3, lambda | ||||||
|  | def patched_auth_request(original_func, instance, args, kwargs): | ||||||
|  | 
 | ||||||
|  |     # Catching the name of the operation that called make_request() | ||||||
|  |     operation_name = None | ||||||
|  | 
 | ||||||
|  |     # Go up the stack until we get the first non-ddtrace module | ||||||
|  |     # DEV: For `lambda.list_functions()` this should be: | ||||||
|  |     #        - ddtrace.contrib.boto.patch | ||||||
|  |     #        - ddtrace.vendor.wrapt.wrappers | ||||||
|  |     #        - boto.awslambda.layer1 (make_request) | ||||||
|  |     #        - boto.awslambda.layer1 (list_functions) | ||||||
|  |     # But can vary depending on Python versions; that's why we use an heuristic | ||||||
|  |     frame = inspect.currentframe().f_back | ||||||
|  |     operation_name = None | ||||||
|  |     while frame: | ||||||
|  |         if frame.f_code.co_name == 'make_request': | ||||||
|  |             operation_name = frame.f_back.f_code.co_name | ||||||
|  |             break | ||||||
|  |         frame = frame.f_back | ||||||
|  | 
 | ||||||
|  |     pin = Pin.get_from(instance) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return original_func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     endpoint_name = getattr(instance, 'host').split('.')[0] | ||||||
|  | 
 | ||||||
|  |     with pin.tracer.trace( | ||||||
|  |         '{}.command'.format(endpoint_name), | ||||||
|  |         service='{}.{}'.format(pin.service, endpoint_name), | ||||||
|  |         span_type=SpanTypes.HTTP, | ||||||
|  |     ) as span: | ||||||
|  | 
 | ||||||
|  |         if args: | ||||||
|  |             http_method = args[0] | ||||||
|  |             span.resource = '%s.%s' % (endpoint_name, http_method.lower()) | ||||||
|  |         else: | ||||||
|  |             span.resource = endpoint_name | ||||||
|  | 
 | ||||||
|  |         aws.add_span_arg_tags(span, endpoint_name, args, AWS_AUTH_ARGS_NAME, AWS_AUTH_TRACED_ARGS) | ||||||
|  | 
 | ||||||
|  |         # Obtaining region name | ||||||
|  |         region_name = _get_instance_region_name(instance) | ||||||
|  | 
 | ||||||
|  |         meta = { | ||||||
|  |             aws.AGENT: 'boto', | ||||||
|  |             aws.OPERATION: operation_name, | ||||||
|  |         } | ||||||
|  |         if region_name: | ||||||
|  |             meta[aws.REGION] = region_name | ||||||
|  | 
 | ||||||
|  |         span.set_tags(meta) | ||||||
|  | 
 | ||||||
|  |         # Original func returns a boto.connection.HTTPResponse object | ||||||
|  |         result = original_func(*args, **kwargs) | ||||||
|  |         span.set_tag(http.STATUS_CODE, getattr(result, 'status')) | ||||||
|  |         span.set_tag(http.METHOD, getattr(result, '_method')) | ||||||
|  | 
 | ||||||
|  |         # set analytics sample rate | ||||||
|  |         span.set_tag( | ||||||
|  |             ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |             config.boto.get_analytics_sample_rate() | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         return result | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _get_instance_region_name(instance): | ||||||
|  |     region = getattr(instance, 'region', None) | ||||||
|  | 
 | ||||||
|  |     if not region: | ||||||
|  |         return None | ||||||
|  |     if isinstance(region, str): | ||||||
|  |         return region.split(':')[1] | ||||||
|  |     else: | ||||||
|  |         return region.name | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | """ | ||||||
|  | The Botocore integration will trace all AWS calls made with the botocore | ||||||
|  | library. Libraries like Boto3 that use Botocore will also be patched. | ||||||
|  | 
 | ||||||
|  | This integration is automatically patched when using ``patch_all()``:: | ||||||
|  | 
 | ||||||
|  |     import botocore.session | ||||||
|  |     from ddtrace import patch | ||||||
|  | 
 | ||||||
|  |     # If not patched yet, you can patch botocore specifically | ||||||
|  |     patch(botocore=True) | ||||||
|  | 
 | ||||||
|  |     # This will report spans with the default instrumentation | ||||||
|  |     botocore.session.get_session() | ||||||
|  |     lambda_client = session.create_client('lambda', region_name='us-east-1') | ||||||
|  |     # Example of instrumented query | ||||||
|  |     lambda_client.list_functions() | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | required_modules = ['botocore.client'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch | ||||||
|  |         __all__ = ['patch'] | ||||||
|  | @ -0,0 +1,81 @@ | ||||||
|  | """ | ||||||
|  | Trace queries to aws api done via botocore client | ||||||
|  | """ | ||||||
|  | # 3p | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | from ddtrace import config | ||||||
|  | import botocore.client | ||||||
|  | 
 | ||||||
|  | # project | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...ext import SpanTypes, http, aws | ||||||
|  | from ...utils.formats import deep_getattr | ||||||
|  | from ...utils.wrappers import unwrap | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Original botocore client class | ||||||
|  | _Botocore_client = botocore.client.BaseClient | ||||||
|  | 
 | ||||||
|  | ARGS_NAME = ('action', 'params', 'path', 'verb') | ||||||
|  | TRACED_ARGS = ['params', 'path', 'verb'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     if getattr(botocore.client, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(botocore.client, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     wrapt.wrap_function_wrapper('botocore.client', 'BaseClient._make_api_call', patched_api_call) | ||||||
|  |     Pin(service='aws', app='aws').onto(botocore.client.BaseClient) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     if getattr(botocore.client, '_datadog_patch', False): | ||||||
|  |         setattr(botocore.client, '_datadog_patch', False) | ||||||
|  |         unwrap(botocore.client.BaseClient, '_make_api_call') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patched_api_call(original_func, instance, args, kwargs): | ||||||
|  | 
 | ||||||
|  |     pin = Pin.get_from(instance) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return original_func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     endpoint_name = deep_getattr(instance, '_endpoint._endpoint_prefix') | ||||||
|  | 
 | ||||||
|  |     with pin.tracer.trace('{}.command'.format(endpoint_name), | ||||||
|  |                           service='{}.{}'.format(pin.service, endpoint_name), | ||||||
|  |                           span_type=SpanTypes.HTTP) as span: | ||||||
|  | 
 | ||||||
|  |         operation = None | ||||||
|  |         if args: | ||||||
|  |             operation = args[0] | ||||||
|  |             span.resource = '%s.%s' % (endpoint_name, operation.lower()) | ||||||
|  | 
 | ||||||
|  |         else: | ||||||
|  |             span.resource = endpoint_name | ||||||
|  | 
 | ||||||
|  |         aws.add_span_arg_tags(span, endpoint_name, args, ARGS_NAME, TRACED_ARGS) | ||||||
|  | 
 | ||||||
|  |         region_name = deep_getattr(instance, 'meta.region_name') | ||||||
|  | 
 | ||||||
|  |         meta = { | ||||||
|  |             'aws.agent': 'botocore', | ||||||
|  |             'aws.operation': operation, | ||||||
|  |             'aws.region': region_name, | ||||||
|  |         } | ||||||
|  |         span.set_tags(meta) | ||||||
|  | 
 | ||||||
|  |         result = original_func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         span.set_tag(http.STATUS_CODE, result['ResponseMetadata']['HTTPStatusCode']) | ||||||
|  |         span.set_tag('retry_attempts', result['ResponseMetadata']['RetryAttempts']) | ||||||
|  | 
 | ||||||
|  |         # set analytics sample rate | ||||||
|  |         span.set_tag( | ||||||
|  |             ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |             config.botocore.get_analytics_sample_rate() | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         return result | ||||||
|  | @ -0,0 +1,23 @@ | ||||||
|  | """ | ||||||
|  | The bottle integration traces the Bottle web framework. Add the following | ||||||
|  | plugin to your app:: | ||||||
|  | 
 | ||||||
|  |     import bottle | ||||||
|  |     from ddtrace import tracer | ||||||
|  |     from ddtrace.contrib.bottle import TracePlugin | ||||||
|  | 
 | ||||||
|  |     app = bottle.Bottle() | ||||||
|  |     plugin = TracePlugin(service="my-web-app") | ||||||
|  |     app.install(plugin) | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | required_modules = ['bottle'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .trace import TracePlugin | ||||||
|  |         from .patch import patch | ||||||
|  | 
 | ||||||
|  |         __all__ = ['TracePlugin', 'patch'] | ||||||
|  | @ -0,0 +1,26 @@ | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | from .trace import TracePlugin | ||||||
|  | 
 | ||||||
|  | import bottle | ||||||
|  | 
 | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """Patch the bottle.Bottle class | ||||||
|  |     """ | ||||||
|  |     if getattr(bottle, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     setattr(bottle, '_datadog_patch', True) | ||||||
|  |     wrapt.wrap_function_wrapper('bottle', 'Bottle.__init__', traced_init) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_init(wrapped, instance, args, kwargs): | ||||||
|  |     wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     service = os.environ.get('DATADOG_SERVICE_NAME') or 'bottle' | ||||||
|  | 
 | ||||||
|  |     plugin = TracePlugin(service=service) | ||||||
|  |     instance.install(plugin) | ||||||
|  | @ -0,0 +1,83 @@ | ||||||
|  | # 3p | ||||||
|  | from bottle import response, request, HTTPError, HTTPResponse | ||||||
|  | 
 | ||||||
|  | # stdlib | ||||||
|  | import ddtrace | ||||||
|  | 
 | ||||||
|  | # project | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import SpanTypes, http | ||||||
|  | from ...propagation.http import HTTPPropagator | ||||||
|  | from ...settings import config | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TracePlugin(object): | ||||||
|  |     name = 'trace' | ||||||
|  |     api = 2 | ||||||
|  | 
 | ||||||
|  |     def __init__(self, service='bottle', tracer=None, distributed_tracing=True): | ||||||
|  |         self.service = service | ||||||
|  |         self.tracer = tracer or ddtrace.tracer | ||||||
|  |         self.distributed_tracing = distributed_tracing | ||||||
|  | 
 | ||||||
|  |     def apply(self, callback, route): | ||||||
|  | 
 | ||||||
|  |         def wrapped(*args, **kwargs): | ||||||
|  |             if not self.tracer or not self.tracer.enabled: | ||||||
|  |                 return callback(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |             resource = '{} {}'.format(request.method, route.rule) | ||||||
|  | 
 | ||||||
|  |             # Propagate headers such as x-datadog-trace-id. | ||||||
|  |             if self.distributed_tracing: | ||||||
|  |                 propagator = HTTPPropagator() | ||||||
|  |                 context = propagator.extract(request.headers) | ||||||
|  |                 if context.trace_id: | ||||||
|  |                     self.tracer.context_provider.activate(context) | ||||||
|  | 
 | ||||||
|  |             with self.tracer.trace( | ||||||
|  |                 'bottle.request', service=self.service, resource=resource, span_type=SpanTypes.WEB | ||||||
|  |             ) as s: | ||||||
|  |                 # set analytics sample rate with global config enabled | ||||||
|  |                 s.set_tag( | ||||||
|  |                     ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |                     config.bottle.get_analytics_sample_rate(use_global_config=True) | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |                 code = None | ||||||
|  |                 result = None | ||||||
|  |                 try: | ||||||
|  |                     result = callback(*args, **kwargs) | ||||||
|  |                     return result | ||||||
|  |                 except (HTTPError, HTTPResponse) as e: | ||||||
|  |                     # you can interrupt flows using abort(status_code, 'message')... | ||||||
|  |                     # we need to respect the defined status_code. | ||||||
|  |                     # we also need to handle when response is raised as is the | ||||||
|  |                     # case with a 4xx status | ||||||
|  |                     code = e.status_code | ||||||
|  |                     raise | ||||||
|  |                 except Exception: | ||||||
|  |                     # bottle doesn't always translate unhandled exceptions, so | ||||||
|  |                     # we mark it here. | ||||||
|  |                     code = 500 | ||||||
|  |                     raise | ||||||
|  |                 finally: | ||||||
|  |                     if isinstance(result, HTTPResponse): | ||||||
|  |                         response_code = result.status_code | ||||||
|  |                     elif code: | ||||||
|  |                         response_code = code | ||||||
|  |                     else: | ||||||
|  |                         # bottle local response has not yet been updated so this | ||||||
|  |                         # will be default | ||||||
|  |                         response_code = response.status_code | ||||||
|  | 
 | ||||||
|  |                     if 500 <= response_code < 600: | ||||||
|  |                         s.error = 1 | ||||||
|  | 
 | ||||||
|  |                     s.set_tag(http.STATUS_CODE, response_code) | ||||||
|  |                     s.set_tag(http.URL, request.urlparts._replace(query='').geturl()) | ||||||
|  |                     s.set_tag(http.METHOD, request.method) | ||||||
|  |                     if config.bottle.trace_query_string: | ||||||
|  |                         s.set_tag(http.QUERY_STRING, request.query_string) | ||||||
|  | 
 | ||||||
|  |         return wrapped | ||||||
|  | @ -0,0 +1,35 @@ | ||||||
|  | """Instrument Cassandra to report Cassandra queries. | ||||||
|  | 
 | ||||||
|  | ``patch_all`` will automatically patch your Cluster instance to make it work. | ||||||
|  | :: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import Pin, patch | ||||||
|  |     from cassandra.cluster import Cluster | ||||||
|  | 
 | ||||||
|  |     # If not patched yet, you can patch cassandra specifically | ||||||
|  |     patch(cassandra=True) | ||||||
|  | 
 | ||||||
|  |     # This will report spans with the default instrumentation | ||||||
|  |     cluster = Cluster(contact_points=["127.0.0.1"], port=9042) | ||||||
|  |     session = cluster.connect("my_keyspace") | ||||||
|  |     # Example of instrumented query | ||||||
|  |     session.execute("select id from my_table limit 10;") | ||||||
|  | 
 | ||||||
|  |     # Use a pin to specify metadata related to this cluster | ||||||
|  |     cluster = Cluster(contact_points=['10.1.1.3', '10.1.1.4', '10.1.1.5'], port=9042) | ||||||
|  |     Pin.override(cluster, service='cassandra-backend') | ||||||
|  |     session = cluster.connect("my_keyspace") | ||||||
|  |     session.execute("select id from my_table limit 10;") | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['cassandra.cluster'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .session import get_traced_cassandra, patch | ||||||
|  |         __all__ = [ | ||||||
|  |             'get_traced_cassandra', | ||||||
|  |             'patch', | ||||||
|  |         ] | ||||||
|  | @ -0,0 +1,3 @@ | ||||||
|  | from .session import patch, unpatch | ||||||
|  | 
 | ||||||
|  | __all__ = ['patch', 'unpatch'] | ||||||
|  | @ -0,0 +1,297 @@ | ||||||
|  | """ | ||||||
|  | Trace queries along a session to a cassandra cluster | ||||||
|  | """ | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | # 3p | ||||||
|  | import cassandra.cluster | ||||||
|  | 
 | ||||||
|  | # project | ||||||
|  | from ...compat import stringify | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import SpanTypes, net, cassandra as cassx, errors | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...settings import config | ||||||
|  | from ...utils.deprecation import deprecated | ||||||
|  | from ...utils.formats import deep_getattr | ||||||
|  | from ...vendor import wrapt | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | RESOURCE_MAX_LENGTH = 5000 | ||||||
|  | SERVICE = 'cassandra' | ||||||
|  | CURRENT_SPAN = '_ddtrace_current_span' | ||||||
|  | PAGE_NUMBER = '_ddtrace_page_number' | ||||||
|  | 
 | ||||||
|  | # Original connect connect function | ||||||
|  | _connect = cassandra.cluster.Cluster.connect | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """ patch will add tracing to the cassandra library. """ | ||||||
|  |     setattr(cassandra.cluster.Cluster, 'connect', | ||||||
|  |             wrapt.FunctionWrapper(_connect, traced_connect)) | ||||||
|  |     Pin(service=SERVICE, app=SERVICE).onto(cassandra.cluster.Cluster) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     cassandra.cluster.Cluster.connect = _connect | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_connect(func, instance, args, kwargs): | ||||||
|  |     session = func(*args, **kwargs) | ||||||
|  |     if not isinstance(session.execute, wrapt.FunctionWrapper): | ||||||
|  |         # FIXME[matt] this should probably be private. | ||||||
|  |         setattr(session, 'execute_async', wrapt.FunctionWrapper(session.execute_async, traced_execute_async)) | ||||||
|  |     return session | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _close_span_on_success(result, future): | ||||||
|  |     span = getattr(future, CURRENT_SPAN, None) | ||||||
|  |     if not span: | ||||||
|  |         log.debug('traced_set_final_result was not able to get the current span from the ResponseFuture') | ||||||
|  |         return | ||||||
|  |     try: | ||||||
|  |         span.set_tags(_extract_result_metas(cassandra.cluster.ResultSet(future, result))) | ||||||
|  |     except Exception: | ||||||
|  |         log.debug('an exception occured while setting tags', exc_info=True) | ||||||
|  |     finally: | ||||||
|  |         span.finish() | ||||||
|  |         delattr(future, CURRENT_SPAN) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_set_final_result(func, instance, args, kwargs): | ||||||
|  |     result = args[0] | ||||||
|  |     _close_span_on_success(result, instance) | ||||||
|  |     return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _close_span_on_error(exc, future): | ||||||
|  |     span = getattr(future, CURRENT_SPAN, None) | ||||||
|  |     if not span: | ||||||
|  |         log.debug('traced_set_final_exception was not able to get the current span from the ResponseFuture') | ||||||
|  |         return | ||||||
|  |     try: | ||||||
|  |         # handling the exception manually because we | ||||||
|  |         # don't have an ongoing exception here | ||||||
|  |         span.error = 1 | ||||||
|  |         span.set_tag(errors.ERROR_MSG, exc.args[0]) | ||||||
|  |         span.set_tag(errors.ERROR_TYPE, exc.__class__.__name__) | ||||||
|  |     except Exception: | ||||||
|  |         log.debug('traced_set_final_exception was not able to set the error, failed with error', exc_info=True) | ||||||
|  |     finally: | ||||||
|  |         span.finish() | ||||||
|  |         delattr(future, CURRENT_SPAN) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_set_final_exception(func, instance, args, kwargs): | ||||||
|  |     exc = args[0] | ||||||
|  |     _close_span_on_error(exc, instance) | ||||||
|  |     return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_start_fetching_next_page(func, instance, args, kwargs): | ||||||
|  |     has_more_pages = getattr(instance, 'has_more_pages', True) | ||||||
|  |     if not has_more_pages: | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  |     session = getattr(instance, 'session', None) | ||||||
|  |     cluster = getattr(session, 'cluster', None) | ||||||
|  |     pin = Pin.get_from(cluster) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     # In case the current span is not finished we make sure to finish it | ||||||
|  |     old_span = getattr(instance, CURRENT_SPAN, None) | ||||||
|  |     if old_span: | ||||||
|  |         log.debug('previous span was not finished before fetching next page') | ||||||
|  |         old_span.finish() | ||||||
|  | 
 | ||||||
|  |     query = getattr(instance, 'query', None) | ||||||
|  | 
 | ||||||
|  |     span = _start_span_and_set_tags(pin, query, session, cluster) | ||||||
|  | 
 | ||||||
|  |     page_number = getattr(instance, PAGE_NUMBER, 1) + 1 | ||||||
|  |     setattr(instance, PAGE_NUMBER, page_number) | ||||||
|  |     setattr(instance, CURRENT_SPAN, span) | ||||||
|  |     try: | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  |     except Exception: | ||||||
|  |         with span: | ||||||
|  |             span.set_exc_info(*sys.exc_info()) | ||||||
|  |         raise | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_execute_async(func, instance, args, kwargs): | ||||||
|  |     cluster = getattr(instance, 'cluster', None) | ||||||
|  |     pin = Pin.get_from(cluster) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     query = kwargs.get('query') or args[0] | ||||||
|  | 
 | ||||||
|  |     span = _start_span_and_set_tags(pin, query, instance, cluster) | ||||||
|  | 
 | ||||||
|  |     try: | ||||||
|  |         result = func(*args, **kwargs) | ||||||
|  |         setattr(result, CURRENT_SPAN, span) | ||||||
|  |         setattr(result, PAGE_NUMBER, 1) | ||||||
|  |         setattr( | ||||||
|  |             result, | ||||||
|  |             '_set_final_result', | ||||||
|  |             wrapt.FunctionWrapper( | ||||||
|  |                 result._set_final_result, | ||||||
|  |                 traced_set_final_result | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         setattr( | ||||||
|  |             result, | ||||||
|  |             '_set_final_exception', | ||||||
|  |             wrapt.FunctionWrapper( | ||||||
|  |                 result._set_final_exception, | ||||||
|  |                 traced_set_final_exception | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         setattr( | ||||||
|  |             result, | ||||||
|  |             'start_fetching_next_page', | ||||||
|  |             wrapt.FunctionWrapper( | ||||||
|  |                 result.start_fetching_next_page, | ||||||
|  |                 traced_start_fetching_next_page | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |         # Since we cannot be sure that the previous methods were overwritten | ||||||
|  |         # before the call ended, we add callbacks that will be run | ||||||
|  |         # synchronously if the call already returned and we remove them right | ||||||
|  |         # after. | ||||||
|  |         result.add_callbacks( | ||||||
|  |             _close_span_on_success, | ||||||
|  |             _close_span_on_error, | ||||||
|  |             callback_args=(result,), | ||||||
|  |             errback_args=(result,) | ||||||
|  |         ) | ||||||
|  |         result.clear_callbacks() | ||||||
|  |         return result | ||||||
|  |     except Exception: | ||||||
|  |         with span: | ||||||
|  |             span.set_exc_info(*sys.exc_info()) | ||||||
|  |         raise | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _start_span_and_set_tags(pin, query, session, cluster): | ||||||
|  |     service = pin.service | ||||||
|  |     tracer = pin.tracer | ||||||
|  |     span = tracer.trace('cassandra.query', service=service, span_type=SpanTypes.CASSANDRA) | ||||||
|  |     _sanitize_query(span, query) | ||||||
|  |     span.set_tags(_extract_session_metas(session))     # FIXME[matt] do once? | ||||||
|  |     span.set_tags(_extract_cluster_metas(cluster)) | ||||||
|  |     # set analytics sample rate if enabled | ||||||
|  |     span.set_tag( | ||||||
|  |         ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |         config.cassandra.get_analytics_sample_rate() | ||||||
|  |     ) | ||||||
|  |     return span | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _extract_session_metas(session): | ||||||
|  |     metas = {} | ||||||
|  | 
 | ||||||
|  |     if getattr(session, 'keyspace', None): | ||||||
|  |         # FIXME the keyspace can be overridden explicitly in the query itself | ||||||
|  |         # e.g. 'select * from trace.hash_to_resource' | ||||||
|  |         metas[cassx.KEYSPACE] = session.keyspace.lower() | ||||||
|  | 
 | ||||||
|  |     return metas | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _extract_cluster_metas(cluster): | ||||||
|  |     metas = {} | ||||||
|  |     if deep_getattr(cluster, 'metadata.cluster_name'): | ||||||
|  |         metas[cassx.CLUSTER] = cluster.metadata.cluster_name | ||||||
|  |     if getattr(cluster, 'port', None): | ||||||
|  |         metas[net.TARGET_PORT] = cluster.port | ||||||
|  | 
 | ||||||
|  |     return metas | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _extract_result_metas(result): | ||||||
|  |     metas = {} | ||||||
|  |     if result is None: | ||||||
|  |         return metas | ||||||
|  | 
 | ||||||
|  |     future = getattr(result, 'response_future', None) | ||||||
|  | 
 | ||||||
|  |     if future: | ||||||
|  |         # get the host | ||||||
|  |         host = getattr(future, 'coordinator_host', None) | ||||||
|  |         if host: | ||||||
|  |             metas[net.TARGET_HOST] = host | ||||||
|  |         elif hasattr(future, '_current_host'): | ||||||
|  |             address = deep_getattr(future, '_current_host.address') | ||||||
|  |             if address: | ||||||
|  |                 metas[net.TARGET_HOST] = address | ||||||
|  | 
 | ||||||
|  |         query = getattr(future, 'query', None) | ||||||
|  |         if getattr(query, 'consistency_level', None): | ||||||
|  |             metas[cassx.CONSISTENCY_LEVEL] = query.consistency_level | ||||||
|  |         if getattr(query, 'keyspace', None): | ||||||
|  |             metas[cassx.KEYSPACE] = query.keyspace.lower() | ||||||
|  | 
 | ||||||
|  |         page_number = getattr(future, PAGE_NUMBER, 1) | ||||||
|  |         has_more_pages = getattr(future, 'has_more_pages') | ||||||
|  |         is_paginated = has_more_pages or page_number > 1 | ||||||
|  |         metas[cassx.PAGINATED] = is_paginated | ||||||
|  |         if is_paginated: | ||||||
|  |             metas[cassx.PAGE_NUMBER] = page_number | ||||||
|  | 
 | ||||||
|  |     if hasattr(result, 'current_rows'): | ||||||
|  |         result_rows = result.current_rows or [] | ||||||
|  |         metas[cassx.ROW_COUNT] = len(result_rows) | ||||||
|  | 
 | ||||||
|  |     return metas | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _sanitize_query(span, query): | ||||||
|  |     # TODO (aaditya): fix this hacky type check. we need it to avoid circular imports | ||||||
|  |     t = type(query).__name__ | ||||||
|  | 
 | ||||||
|  |     resource = None | ||||||
|  |     if t in ('SimpleStatement', 'PreparedStatement'): | ||||||
|  |         # reset query if a string is available | ||||||
|  |         resource = getattr(query, 'query_string', query) | ||||||
|  |     elif t == 'BatchStatement': | ||||||
|  |         resource = 'BatchStatement' | ||||||
|  |         # Each element in `_statements_and_parameters` is: | ||||||
|  |         #   (is_prepared, statement, parameters) | ||||||
|  |         #  ref:https://github.com/datastax/python-driver/blob/13d6d72be74f40fcef5ec0f2b3e98538b3b87459/cassandra/query.py#L844 | ||||||
|  |         # | ||||||
|  |         # For prepared statements, the `statement` value is just the query_id | ||||||
|  |         #   which is not a statement and when trying to join with other strings | ||||||
|  |         #   raises an error in python3 around joining bytes to unicode, so this | ||||||
|  |         #   just filters out prepared statements from this tag value | ||||||
|  |         q = '; '.join(q[1] for q in query._statements_and_parameters[:2] if not q[0]) | ||||||
|  |         span.set_tag('cassandra.query', q) | ||||||
|  |         span.set_metric('cassandra.batch_size', len(query._statements_and_parameters)) | ||||||
|  |     elif t == 'BoundStatement': | ||||||
|  |         ps = getattr(query, 'prepared_statement', None) | ||||||
|  |         if ps: | ||||||
|  |             resource = getattr(ps, 'query_string', None) | ||||||
|  |     elif t == 'str': | ||||||
|  |         resource = query | ||||||
|  |     else: | ||||||
|  |         resource = 'unknown-query-type'  # FIXME[matt] what else do to here? | ||||||
|  | 
 | ||||||
|  |     span.resource = stringify(resource)[:RESOURCE_MAX_LENGTH] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # | ||||||
|  | # DEPRECATED | ||||||
|  | # | ||||||
|  | 
 | ||||||
|  | @deprecated(message='Use patching instead (see the docs).', version='1.0.0') | ||||||
|  | def get_traced_cassandra(*args, **kwargs): | ||||||
|  |     return _get_traced_cluster(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _get_traced_cluster(*args, **kwargs): | ||||||
|  |     return cassandra.cluster.Cluster | ||||||
|  | @ -0,0 +1,54 @@ | ||||||
|  | """ | ||||||
|  | The Celery integration will trace all tasks that are executed in the | ||||||
|  | background. Functions and class based tasks are traced only if the Celery API | ||||||
|  | is used, so calling the function directly or via the ``run()`` method will not | ||||||
|  | generate traces. However, calling ``apply()``, ``apply_async()`` and ``delay()`` | ||||||
|  | will produce tracing data. To trace your Celery application, call the patch method:: | ||||||
|  | 
 | ||||||
|  |     import celery | ||||||
|  |     from ddtrace import patch | ||||||
|  | 
 | ||||||
|  |     patch(celery=True) | ||||||
|  |     app = celery.Celery() | ||||||
|  | 
 | ||||||
|  |     @app.task | ||||||
|  |     def my_task(): | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  |     class MyTask(app.Task): | ||||||
|  |         def run(self): | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | To change Celery service name, you can use the ``Config`` API as follows:: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import config | ||||||
|  | 
 | ||||||
|  |     # change service names for producers and workers | ||||||
|  |     config.celery['producer_service_name'] = 'task-queue' | ||||||
|  |     config.celery['worker_service_name'] = 'worker-notify' | ||||||
|  | 
 | ||||||
|  | By default, reported service names are: | ||||||
|  |     * ``celery-producer`` when tasks are enqueued for processing | ||||||
|  |     * ``celery-worker`` when tasks are processed by a Celery process | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['celery'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .app import patch_app, unpatch_app | ||||||
|  |         from .patch import patch, unpatch | ||||||
|  |         from .task import patch_task, unpatch_task | ||||||
|  | 
 | ||||||
|  |         __all__ = [ | ||||||
|  |             'patch', | ||||||
|  |             'patch_app', | ||||||
|  |             'patch_task', | ||||||
|  |             'unpatch', | ||||||
|  |             'unpatch_app', | ||||||
|  |             'unpatch_task', | ||||||
|  |         ] | ||||||
|  | @ -0,0 +1,60 @@ | ||||||
|  | from celery import signals | ||||||
|  | 
 | ||||||
|  | from ddtrace import Pin, config | ||||||
|  | from ddtrace.pin import _DD_PIN_NAME | ||||||
|  | 
 | ||||||
|  | from .constants import APP | ||||||
|  | from .signals import ( | ||||||
|  |     trace_prerun, | ||||||
|  |     trace_postrun, | ||||||
|  |     trace_before_publish, | ||||||
|  |     trace_after_publish, | ||||||
|  |     trace_failure, | ||||||
|  |     trace_retry, | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch_app(app, pin=None): | ||||||
|  |     """Attach the Pin class to the application and connect | ||||||
|  |     our handlers to Celery signals. | ||||||
|  |     """ | ||||||
|  |     if getattr(app, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(app, '__datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     # attach the PIN object | ||||||
|  |     pin = pin or Pin( | ||||||
|  |         service=config.celery['worker_service_name'], | ||||||
|  |         app=APP, | ||||||
|  |         _config=config.celery, | ||||||
|  |     ) | ||||||
|  |     pin.onto(app) | ||||||
|  |     # connect to the Signal framework | ||||||
|  | 
 | ||||||
|  |     signals.task_prerun.connect(trace_prerun, weak=False) | ||||||
|  |     signals.task_postrun.connect(trace_postrun, weak=False) | ||||||
|  |     signals.before_task_publish.connect(trace_before_publish, weak=False) | ||||||
|  |     signals.after_task_publish.connect(trace_after_publish, weak=False) | ||||||
|  |     signals.task_failure.connect(trace_failure, weak=False) | ||||||
|  |     signals.task_retry.connect(trace_retry, weak=False) | ||||||
|  |     return app | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch_app(app): | ||||||
|  |     """Remove the Pin instance from the application and disconnect | ||||||
|  |     our handlers from Celery signal framework. | ||||||
|  |     """ | ||||||
|  |     if not getattr(app, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(app, '__datadog_patch', False) | ||||||
|  | 
 | ||||||
|  |     pin = Pin.get_from(app) | ||||||
|  |     if pin is not None: | ||||||
|  |         delattr(app, _DD_PIN_NAME) | ||||||
|  | 
 | ||||||
|  |     signals.task_prerun.disconnect(trace_prerun) | ||||||
|  |     signals.task_postrun.disconnect(trace_postrun) | ||||||
|  |     signals.before_task_publish.disconnect(trace_before_publish) | ||||||
|  |     signals.after_task_publish.disconnect(trace_after_publish) | ||||||
|  |     signals.task_failure.disconnect(trace_failure) | ||||||
|  |     signals.task_retry.disconnect(trace_retry) | ||||||
|  | @ -0,0 +1,22 @@ | ||||||
|  | from os import getenv | ||||||
|  | 
 | ||||||
|  | # Celery Context key | ||||||
|  | CTX_KEY = '__dd_task_span' | ||||||
|  | 
 | ||||||
|  | # Span names | ||||||
|  | PRODUCER_ROOT_SPAN = 'celery.apply' | ||||||
|  | WORKER_ROOT_SPAN = 'celery.run' | ||||||
|  | 
 | ||||||
|  | # Task operations | ||||||
|  | TASK_TAG_KEY = 'celery.action' | ||||||
|  | TASK_APPLY = 'apply' | ||||||
|  | TASK_APPLY_ASYNC = 'apply_async' | ||||||
|  | TASK_RUN = 'run' | ||||||
|  | TASK_RETRY_REASON_KEY = 'celery.retry.reason' | ||||||
|  | 
 | ||||||
|  | # Service info | ||||||
|  | APP = 'celery' | ||||||
|  | # `getenv()` call must be kept for backward compatibility; we may remove it | ||||||
|  | # later when we do a full migration to the `Config` class | ||||||
|  | PRODUCER_SERVICE = getenv('DATADOG_SERVICE_NAME') or 'celery-producer' | ||||||
|  | WORKER_SERVICE = getenv('DATADOG_SERVICE_NAME') or 'celery-worker' | ||||||
|  | @ -0,0 +1,28 @@ | ||||||
|  | import celery | ||||||
|  | 
 | ||||||
|  | from ddtrace import config | ||||||
|  | 
 | ||||||
|  | from .app import patch_app, unpatch_app | ||||||
|  | from .constants import PRODUCER_SERVICE, WORKER_SERVICE | ||||||
|  | from ...utils.formats import get_env | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Celery default settings | ||||||
|  | config._add('celery', { | ||||||
|  |     'producer_service_name': get_env('celery', 'producer_service_name', PRODUCER_SERVICE), | ||||||
|  |     'worker_service_name': get_env('celery', 'worker_service_name', WORKER_SERVICE), | ||||||
|  | }) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """Instrument Celery base application and the `TaskRegistry` so | ||||||
|  |     that any new registered task is automatically instrumented. In the | ||||||
|  |     case of Django-Celery integration, also the `@shared_task` decorator | ||||||
|  |     must be instrumented because Django doesn't use the Celery registry. | ||||||
|  |     """ | ||||||
|  |     patch_app(celery.Celery) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     """Disconnect all signals and remove Tracing capabilities""" | ||||||
|  |     unpatch_app(celery.Celery) | ||||||
|  | @ -0,0 +1,154 @@ | ||||||
|  | from ddtrace import Pin, config | ||||||
|  | 
 | ||||||
|  | from celery import registry | ||||||
|  | 
 | ||||||
|  | from ...ext import SpanTypes | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | from . import constants as c | ||||||
|  | from .utils import tags_from_context, retrieve_task_id, attach_span, detach_span, retrieve_span | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def trace_prerun(*args, **kwargs): | ||||||
|  |     # safe-guard to avoid crashes in case the signals API | ||||||
|  |     # changes in Celery | ||||||
|  |     task = kwargs.get('sender') | ||||||
|  |     task_id = kwargs.get('task_id') | ||||||
|  |     log.debug('prerun signal start task_id=%s', task_id) | ||||||
|  |     if task is None or task_id is None: | ||||||
|  |         log.debug('unable to extract the Task and the task_id. This version of Celery may not be supported.') | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # retrieve the task Pin or fallback to the global one | ||||||
|  |     pin = Pin.get_from(task) or Pin.get_from(task.app) | ||||||
|  |     if pin is None: | ||||||
|  |         log.debug('no pin found on task or task.app task_id=%s', task_id) | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # propagate the `Span` in the current task Context | ||||||
|  |     service = config.celery['worker_service_name'] | ||||||
|  |     span = pin.tracer.trace(c.WORKER_ROOT_SPAN, service=service, resource=task.name, span_type=SpanTypes.WORKER) | ||||||
|  |     attach_span(task, task_id, span) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def trace_postrun(*args, **kwargs): | ||||||
|  |     # safe-guard to avoid crashes in case the signals API | ||||||
|  |     # changes in Celery | ||||||
|  |     task = kwargs.get('sender') | ||||||
|  |     task_id = kwargs.get('task_id') | ||||||
|  |     log.debug('postrun signal task_id=%s', task_id) | ||||||
|  |     if task is None or task_id is None: | ||||||
|  |         log.debug('unable to extract the Task and the task_id. This version of Celery may not be supported.') | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # retrieve and finish the Span | ||||||
|  |     span = retrieve_span(task, task_id) | ||||||
|  |     if span is None: | ||||||
|  |         log.warning('no existing span found for task_id=%s', task_id) | ||||||
|  |         return | ||||||
|  |     else: | ||||||
|  |         # request context tags | ||||||
|  |         span.set_tag(c.TASK_TAG_KEY, c.TASK_RUN) | ||||||
|  |         span.set_tags(tags_from_context(kwargs)) | ||||||
|  |         span.set_tags(tags_from_context(task.request)) | ||||||
|  |         span.finish() | ||||||
|  |         detach_span(task, task_id) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def trace_before_publish(*args, **kwargs): | ||||||
|  |     # `before_task_publish` signal doesn't propagate the task instance so | ||||||
|  |     # we need to retrieve it from the Celery Registry to access the `Pin`. The | ||||||
|  |     # `Task` instance **does not** include any information about the current | ||||||
|  |     # execution, so it **must not** be used to retrieve `request` data. | ||||||
|  |     task_name = kwargs.get('sender') | ||||||
|  |     task = registry.tasks.get(task_name) | ||||||
|  |     task_id = retrieve_task_id(kwargs) | ||||||
|  |     # safe-guard to avoid crashes in case the signals API | ||||||
|  |     # changes in Celery | ||||||
|  |     if task is None or task_id is None: | ||||||
|  |         log.debug('unable to extract the Task and the task_id. This version of Celery may not be supported.') | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # propagate the `Span` in the current task Context | ||||||
|  |     pin = Pin.get_from(task) or Pin.get_from(task.app) | ||||||
|  |     if pin is None: | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # apply some tags here because most of the data is not available | ||||||
|  |     # in the task_after_publish signal | ||||||
|  |     service = config.celery['producer_service_name'] | ||||||
|  |     span = pin.tracer.trace(c.PRODUCER_ROOT_SPAN, service=service, resource=task_name) | ||||||
|  |     span.set_tag(c.TASK_TAG_KEY, c.TASK_APPLY_ASYNC) | ||||||
|  |     span.set_tag('celery.id', task_id) | ||||||
|  |     span.set_tags(tags_from_context(kwargs)) | ||||||
|  |     # Note: adding tags from `traceback` or `state` calls will make an | ||||||
|  |     # API call to the backend for the properties so we should rely | ||||||
|  |     # only on the given `Context` | ||||||
|  |     attach_span(task, task_id, span, is_publish=True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def trace_after_publish(*args, **kwargs): | ||||||
|  |     task_name = kwargs.get('sender') | ||||||
|  |     task = registry.tasks.get(task_name) | ||||||
|  |     task_id = retrieve_task_id(kwargs) | ||||||
|  |     # safe-guard to avoid crashes in case the signals API | ||||||
|  |     # changes in Celery | ||||||
|  |     if task is None or task_id is None: | ||||||
|  |         log.debug('unable to extract the Task and the task_id. This version of Celery may not be supported.') | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # retrieve and finish the Span | ||||||
|  |     span = retrieve_span(task, task_id, is_publish=True) | ||||||
|  |     if span is None: | ||||||
|  |         return | ||||||
|  |     else: | ||||||
|  |         span.finish() | ||||||
|  |         detach_span(task, task_id, is_publish=True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def trace_failure(*args, **kwargs): | ||||||
|  |     # safe-guard to avoid crashes in case the signals API | ||||||
|  |     # changes in Celery | ||||||
|  |     task = kwargs.get('sender') | ||||||
|  |     task_id = kwargs.get('task_id') | ||||||
|  |     if task is None or task_id is None: | ||||||
|  |         log.debug('unable to extract the Task and the task_id. This version of Celery may not be supported.') | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # retrieve and finish the Span | ||||||
|  |     span = retrieve_span(task, task_id) | ||||||
|  |     if span is None: | ||||||
|  |         return | ||||||
|  |     else: | ||||||
|  |         # add Exception tags; post signals are still called | ||||||
|  |         # so we don't need to attach other tags here | ||||||
|  |         ex = kwargs.get('einfo') | ||||||
|  |         if ex is None: | ||||||
|  |             return | ||||||
|  |         if hasattr(task, 'throws') and isinstance(ex.exception, task.throws): | ||||||
|  |             return | ||||||
|  |         span.set_exc_info(ex.type, ex.exception, ex.tb) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def trace_retry(*args, **kwargs): | ||||||
|  |     # safe-guard to avoid crashes in case the signals API | ||||||
|  |     # changes in Celery | ||||||
|  |     task = kwargs.get('sender') | ||||||
|  |     context = kwargs.get('request') | ||||||
|  |     if task is None or context is None: | ||||||
|  |         log.debug('unable to extract the Task or the Context. This version of Celery may not be supported.') | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     reason = kwargs.get('reason') | ||||||
|  |     if not reason: | ||||||
|  |         log.debug('unable to extract the retry reason. This version of Celery may not be supported.') | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     span = retrieve_span(task, context.id) | ||||||
|  |     if span is None: | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # Add retry reason metadata to span | ||||||
|  |     # DEV: Use `str(reason)` instead of `reason.message` in case we get something that isn't an `Exception` | ||||||
|  |     span.set_tag(c.TASK_RETRY_REASON_KEY, str(reason)) | ||||||
|  | @ -0,0 +1,32 @@ | ||||||
|  | from .app import patch_app | ||||||
|  | 
 | ||||||
|  | from ...utils.deprecation import deprecation | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch_task(task, pin=None): | ||||||
|  |     """Deprecated API. The new API uses signals that can be activated via | ||||||
|  |     patch(celery=True) or through `ddtrace-run` script. Using this API | ||||||
|  |     enables instrumentation on all tasks. | ||||||
|  |     """ | ||||||
|  |     deprecation( | ||||||
|  |         name='ddtrace.contrib.celery.patch_task', | ||||||
|  |         message='Use `patch(celery=True)` or `ddtrace-run` script instead', | ||||||
|  |         version='1.0.0', | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     # Enable instrumentation everywhere | ||||||
|  |     patch_app(task.app) | ||||||
|  |     return task | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch_task(task): | ||||||
|  |     """Deprecated API. The new API uses signals that can be deactivated | ||||||
|  |     via unpatch() API. This API is now a no-op implementation so it doesn't | ||||||
|  |     affect instrumented tasks. | ||||||
|  |     """ | ||||||
|  |     deprecation( | ||||||
|  |         name='ddtrace.contrib.celery.patch_task', | ||||||
|  |         message='Use `unpatch()` instead', | ||||||
|  |         version='1.0.0', | ||||||
|  |     ) | ||||||
|  |     return task | ||||||
|  | @ -0,0 +1,106 @@ | ||||||
|  | from weakref import WeakValueDictionary | ||||||
|  | 
 | ||||||
|  | from .constants import CTX_KEY | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def tags_from_context(context): | ||||||
|  |     """Helper to extract meta values from a Celery Context""" | ||||||
|  |     tag_keys = ( | ||||||
|  |         'compression', 'correlation_id', 'countdown', 'delivery_info', 'eta', | ||||||
|  |         'exchange', 'expires', 'hostname', 'id', 'priority', 'queue', 'reply_to', | ||||||
|  |         'retries', 'routing_key', 'serializer', 'timelimit', 'origin', 'state', | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     tags = {} | ||||||
|  |     for key in tag_keys: | ||||||
|  |         value = context.get(key) | ||||||
|  | 
 | ||||||
|  |         # Skip this key if it is not set | ||||||
|  |         if value is None or value == '': | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         # Skip `timelimit` if it is not set (it's default/unset value is a | ||||||
|  |         # tuple or a list of `None` values | ||||||
|  |         if key == 'timelimit' and value in [(None, None), [None, None]]: | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         # Skip `retries` if it's value is `0` | ||||||
|  |         if key == 'retries' and value == 0: | ||||||
|  |             continue | ||||||
|  | 
 | ||||||
|  |         # Celery 4.0 uses `origin` instead of `hostname`; this change preserves | ||||||
|  |         # the same name for the tag despite Celery version | ||||||
|  |         if key == 'origin': | ||||||
|  |             key = 'hostname' | ||||||
|  | 
 | ||||||
|  |         # prefix the tag as 'celery' | ||||||
|  |         tag_name = 'celery.{}'.format(key) | ||||||
|  |         tags[tag_name] = value | ||||||
|  |     return tags | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def attach_span(task, task_id, span, is_publish=False): | ||||||
|  |     """Helper to propagate a `Span` for the given `Task` instance. This | ||||||
|  |     function uses a `WeakValueDictionary` that stores a Datadog Span using | ||||||
|  |     the `(task_id, is_publish)` as a key. This is useful when information must be | ||||||
|  |     propagated from one Celery signal to another. | ||||||
|  | 
 | ||||||
|  |     DEV: We use (task_id, is_publish) for the key to ensure that publishing a | ||||||
|  |          task from within another task does not cause any conflicts. | ||||||
|  | 
 | ||||||
|  |          This mostly happens when either a task fails and a retry policy is in place, | ||||||
|  |          or when a task is manually retries (e.g. `task.retry()`), we end up trying | ||||||
|  |          to publish a task with the same id as the task currently running. | ||||||
|  | 
 | ||||||
|  |          Previously publishing the new task would overwrite the existing `celery.run` span | ||||||
|  |          in the `weak_dict` causing that span to be forgotten and never finished. | ||||||
|  | 
 | ||||||
|  |          NOTE: We cannot test for this well yet, because we do not run a celery worker, | ||||||
|  |          and cannot run `task.apply_async()` | ||||||
|  |     """ | ||||||
|  |     weak_dict = getattr(task, CTX_KEY, None) | ||||||
|  |     if weak_dict is None: | ||||||
|  |         weak_dict = WeakValueDictionary() | ||||||
|  |         setattr(task, CTX_KEY, weak_dict) | ||||||
|  | 
 | ||||||
|  |     weak_dict[(task_id, is_publish)] = span | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def detach_span(task, task_id, is_publish=False): | ||||||
|  |     """Helper to remove a `Span` in a Celery task when it's propagated. | ||||||
|  |     This function handles tasks where the `Span` is not attached. | ||||||
|  |     """ | ||||||
|  |     weak_dict = getattr(task, CTX_KEY, None) | ||||||
|  |     if weak_dict is None: | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     # DEV: See note in `attach_span` for key info | ||||||
|  |     weak_dict.pop((task_id, is_publish), None) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def retrieve_span(task, task_id, is_publish=False): | ||||||
|  |     """Helper to retrieve an active `Span` stored in a `Task` | ||||||
|  |     instance | ||||||
|  |     """ | ||||||
|  |     weak_dict = getattr(task, CTX_KEY, None) | ||||||
|  |     if weak_dict is None: | ||||||
|  |         return | ||||||
|  |     else: | ||||||
|  |         # DEV: See note in `attach_span` for key info | ||||||
|  |         return weak_dict.get((task_id, is_publish)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def retrieve_task_id(context): | ||||||
|  |     """Helper to retrieve the `Task` identifier from the message `body`. | ||||||
|  |     This helper supports Protocol Version 1 and 2. The Protocol is well | ||||||
|  |     detailed in the official documentation: | ||||||
|  |     http://docs.celeryproject.org/en/latest/internals/protocol.html | ||||||
|  |     """ | ||||||
|  |     headers = context.get('headers') | ||||||
|  |     body = context.get('body') | ||||||
|  |     if headers: | ||||||
|  |         # Protocol Version 2 (default from Celery 4.0) | ||||||
|  |         return headers.get('id') | ||||||
|  |     else: | ||||||
|  |         # Protocol Version 1 | ||||||
|  |         return body.get('id') | ||||||
|  | @ -0,0 +1,29 @@ | ||||||
|  | """Instrument Consul to trace KV queries. | ||||||
|  | 
 | ||||||
|  | Only supports tracing for the syncronous client. | ||||||
|  | 
 | ||||||
|  | ``patch_all`` will automatically patch your Consul client to make it work. | ||||||
|  | :: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import Pin, patch | ||||||
|  |     import consul | ||||||
|  | 
 | ||||||
|  |     # If not patched yet, you can patch consul specifically | ||||||
|  |     patch(consul=True) | ||||||
|  | 
 | ||||||
|  |     # This will report a span with the default settings | ||||||
|  |     client = consul.Consul(host="127.0.0.1", port=8500) | ||||||
|  |     client.get("my-key") | ||||||
|  | 
 | ||||||
|  |     # Use a pin to specify metadata related to this client | ||||||
|  |     Pin.override(client, service='consul-kv') | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | required_modules = ['consul'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch, unpatch | ||||||
|  |         __all__ = ['patch', 'unpatch'] | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | import consul | ||||||
|  | 
 | ||||||
|  | from ddtrace.vendor.wrapt import wrap_function_wrapper as _w | ||||||
|  | 
 | ||||||
|  | from ddtrace import config | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import consul as consulx | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...utils.wrappers import unwrap as _u | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _KV_FUNCS = ['put', 'get', 'delete'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     if getattr(consul, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(consul, '__datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     pin = Pin(service=consulx.SERVICE, app=consulx.APP) | ||||||
|  |     pin.onto(consul.Consul.KV) | ||||||
|  | 
 | ||||||
|  |     for f_name in _KV_FUNCS: | ||||||
|  |         _w('consul', 'Consul.KV.%s' % f_name, wrap_function(f_name)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     if not getattr(consul, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(consul, '__datadog_patch', False) | ||||||
|  | 
 | ||||||
|  |     for f_name in _KV_FUNCS: | ||||||
|  |         _u(consul.Consul.KV, f_name) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def wrap_function(name): | ||||||
|  |     def trace_func(wrapped, instance, args, kwargs): | ||||||
|  |         pin = Pin.get_from(instance) | ||||||
|  |         if not pin or not pin.enabled(): | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         # Only patch the syncronous implementation | ||||||
|  |         if not isinstance(instance.agent.http, consul.std.HTTPClient): | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         path = kwargs.get('key') or args[0] | ||||||
|  |         resource = name.upper() | ||||||
|  | 
 | ||||||
|  |         with pin.tracer.trace(consulx.CMD, service=pin.service, resource=resource) as span: | ||||||
|  |             rate = config.consul.get_analytics_sample_rate() | ||||||
|  |             if rate is not None: | ||||||
|  |                 span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, rate) | ||||||
|  |             span.set_tag(consulx.KEY, path) | ||||||
|  |             span.set_tag(consulx.CMD, resource) | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     return trace_func | ||||||
|  | @ -0,0 +1,204 @@ | ||||||
|  | """ | ||||||
|  | Generic dbapi tracing code. | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import SpanTypes, sql | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...settings import config | ||||||
|  | from ...utils.formats import asbool, get_env | ||||||
|  | from ...vendor import wrapt | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | config._add('dbapi2', dict( | ||||||
|  |     trace_fetch_methods=asbool(get_env('dbapi2', 'trace_fetch_methods', 'false')), | ||||||
|  | )) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TracedCursor(wrapt.ObjectProxy): | ||||||
|  |     """ TracedCursor wraps a psql cursor and traces it's queries. """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, cursor, pin): | ||||||
|  |         super(TracedCursor, self).__init__(cursor) | ||||||
|  |         pin.onto(self) | ||||||
|  |         name = pin.app or 'sql' | ||||||
|  |         self._self_datadog_name = '{}.query'.format(name) | ||||||
|  |         self._self_last_execute_operation = None | ||||||
|  | 
 | ||||||
|  |     def _trace_method(self, method, name, resource, extra_tags, *args, **kwargs): | ||||||
|  |         """ | ||||||
|  |         Internal function to trace the call to the underlying cursor method | ||||||
|  |         :param method: The callable to be wrapped | ||||||
|  |         :param name: The name of the resulting span. | ||||||
|  |         :param resource: The sql query. Sql queries are obfuscated on the agent side. | ||||||
|  |         :param extra_tags: A dict of tags to store into the span's meta | ||||||
|  |         :param args: The args that will be passed as positional args to the wrapped method | ||||||
|  |         :param kwargs: The args that will be passed as kwargs to the wrapped method | ||||||
|  |         :return: The result of the wrapped method invocation | ||||||
|  |         """ | ||||||
|  |         pin = Pin.get_from(self) | ||||||
|  |         if not pin or not pin.enabled(): | ||||||
|  |             return method(*args, **kwargs) | ||||||
|  |         service = pin.service | ||||||
|  |         with pin.tracer.trace(name, service=service, resource=resource, span_type=SpanTypes.SQL) as s: | ||||||
|  |             # No reason to tag the query since it is set as the resource by the agent. See: | ||||||
|  |             # https://github.com/DataDog/datadog-trace-agent/blob/bda1ebbf170dd8c5879be993bdd4dbae70d10fda/obfuscate/sql.go#L232 | ||||||
|  |             s.set_tags(pin.tags) | ||||||
|  |             s.set_tags(extra_tags) | ||||||
|  | 
 | ||||||
|  |             # set analytics sample rate if enabled but only for non-FetchTracedCursor | ||||||
|  |             if not isinstance(self, FetchTracedCursor): | ||||||
|  |                 s.set_tag( | ||||||
|  |                     ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |                     config.dbapi2.get_analytics_sample_rate() | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             try: | ||||||
|  |                 return method(*args, **kwargs) | ||||||
|  |             finally: | ||||||
|  |                 row_count = self.__wrapped__.rowcount | ||||||
|  |                 s.set_metric('db.rowcount', row_count) | ||||||
|  |                 # Necessary for django integration backward compatibility. Django integration used to provide its own | ||||||
|  |                 # implementation of the TracedCursor, which used to store the row count into a tag instead of | ||||||
|  |                 # as a metric. Such custom implementation has been replaced by this generic dbapi implementation and | ||||||
|  |                 # this tag has been added since. | ||||||
|  |                 if row_count and row_count >= 0: | ||||||
|  |                     s.set_tag(sql.ROWS, row_count) | ||||||
|  | 
 | ||||||
|  |     def executemany(self, query, *args, **kwargs): | ||||||
|  |         """ Wraps the cursor.executemany method""" | ||||||
|  |         self._self_last_execute_operation = query | ||||||
|  |         # Always return the result as-is | ||||||
|  |         # DEV: Some libraries return `None`, others `int`, and others the cursor objects | ||||||
|  |         #      These differences should be overriden at the integration specific layer (e.g. in `sqlite3/patch.py`) | ||||||
|  |         # FIXME[matt] properly handle kwargs here. arg names can be different | ||||||
|  |         # with different libs. | ||||||
|  |         return self._trace_method( | ||||||
|  |             self.__wrapped__.executemany, self._self_datadog_name, query, {'sql.executemany': 'true'}, | ||||||
|  |             query, *args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def execute(self, query, *args, **kwargs): | ||||||
|  |         """ Wraps the cursor.execute method""" | ||||||
|  |         self._self_last_execute_operation = query | ||||||
|  | 
 | ||||||
|  |         # Always return the result as-is | ||||||
|  |         # DEV: Some libraries return `None`, others `int`, and others the cursor objects | ||||||
|  |         #      These differences should be overriden at the integration specific layer (e.g. in `sqlite3/patch.py`) | ||||||
|  |         return self._trace_method(self.__wrapped__.execute, self._self_datadog_name, query, {}, query, *args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def callproc(self, proc, args): | ||||||
|  |         """ Wraps the cursor.callproc method""" | ||||||
|  |         self._self_last_execute_operation = proc | ||||||
|  |         return self._trace_method(self.__wrapped__.callproc, self._self_datadog_name, proc, {}, proc, args) | ||||||
|  | 
 | ||||||
|  |     def __enter__(self): | ||||||
|  |         # previous versions of the dbapi didn't support context managers. let's | ||||||
|  |         # reference the func that would be called to ensure that errors | ||||||
|  |         # messages will be the same. | ||||||
|  |         self.__wrapped__.__enter__ | ||||||
|  | 
 | ||||||
|  |         # and finally, yield the traced cursor. | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class FetchTracedCursor(TracedCursor): | ||||||
|  |     """ | ||||||
|  |     Sub-class of :class:`TracedCursor` that also instruments `fetchone`, `fetchall`, and `fetchmany` methods. | ||||||
|  | 
 | ||||||
|  |     We do not trace these functions by default since they can get very noisy (e.g. `fetchone` with 100k rows). | ||||||
|  |     """ | ||||||
|  |     def fetchone(self, *args, **kwargs): | ||||||
|  |         """ Wraps the cursor.fetchone method""" | ||||||
|  |         span_name = '{}.{}'.format(self._self_datadog_name, 'fetchone') | ||||||
|  |         return self._trace_method(self.__wrapped__.fetchone, span_name, self._self_last_execute_operation, {}, | ||||||
|  |                                   *args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def fetchall(self, *args, **kwargs): | ||||||
|  |         """ Wraps the cursor.fetchall method""" | ||||||
|  |         span_name = '{}.{}'.format(self._self_datadog_name, 'fetchall') | ||||||
|  |         return self._trace_method(self.__wrapped__.fetchall, span_name, self._self_last_execute_operation, {}, | ||||||
|  |                                   *args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def fetchmany(self, *args, **kwargs): | ||||||
|  |         """ Wraps the cursor.fetchmany method""" | ||||||
|  |         span_name = '{}.{}'.format(self._self_datadog_name, 'fetchmany') | ||||||
|  |         # We want to trace the information about how many rows were requested. Note that this number may be larger | ||||||
|  |         # the number of rows actually returned if less then requested are available from the query. | ||||||
|  |         size_tag_key = 'db.fetch.size' | ||||||
|  |         if 'size' in kwargs: | ||||||
|  |             extra_tags = {size_tag_key: kwargs.get('size')} | ||||||
|  |         elif len(args) == 1 and isinstance(args[0], int): | ||||||
|  |             extra_tags = {size_tag_key: args[0]} | ||||||
|  |         else: | ||||||
|  |             default_array_size = getattr(self.__wrapped__, 'arraysize', None) | ||||||
|  |             extra_tags = {size_tag_key: default_array_size} if default_array_size else {} | ||||||
|  | 
 | ||||||
|  |         return self._trace_method(self.__wrapped__.fetchmany, span_name, self._self_last_execute_operation, extra_tags, | ||||||
|  |                                   *args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TracedConnection(wrapt.ObjectProxy): | ||||||
|  |     """ TracedConnection wraps a Connection with tracing code. """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, conn, pin=None, cursor_cls=None): | ||||||
|  |         # Set default cursor class if one was not provided | ||||||
|  |         if not cursor_cls: | ||||||
|  |             # Do not trace `fetch*` methods by default | ||||||
|  |             cursor_cls = TracedCursor | ||||||
|  |             if config.dbapi2.trace_fetch_methods: | ||||||
|  |                 cursor_cls = FetchTracedCursor | ||||||
|  | 
 | ||||||
|  |         super(TracedConnection, self).__init__(conn) | ||||||
|  |         name = _get_vendor(conn) | ||||||
|  |         self._self_datadog_name = '{}.connection'.format(name) | ||||||
|  |         db_pin = pin or Pin(service=name, app=name) | ||||||
|  |         db_pin.onto(self) | ||||||
|  |         # wrapt requires prefix of `_self` for attributes that are only in the | ||||||
|  |         # proxy (since some of our source objects will use `__slots__`) | ||||||
|  |         self._self_cursor_cls = cursor_cls | ||||||
|  | 
 | ||||||
|  |     def _trace_method(self, method, name, extra_tags, *args, **kwargs): | ||||||
|  |         pin = Pin.get_from(self) | ||||||
|  |         if not pin or not pin.enabled(): | ||||||
|  |             return method(*args, **kwargs) | ||||||
|  |         service = pin.service | ||||||
|  | 
 | ||||||
|  |         with pin.tracer.trace(name, service=service) as s: | ||||||
|  |             s.set_tags(pin.tags) | ||||||
|  |             s.set_tags(extra_tags) | ||||||
|  | 
 | ||||||
|  |             return method(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def cursor(self, *args, **kwargs): | ||||||
|  |         cursor = self.__wrapped__.cursor(*args, **kwargs) | ||||||
|  |         pin = Pin.get_from(self) | ||||||
|  |         if not pin: | ||||||
|  |             return cursor | ||||||
|  |         return self._self_cursor_cls(cursor, pin) | ||||||
|  | 
 | ||||||
|  |     def commit(self, *args, **kwargs): | ||||||
|  |         span_name = '{}.{}'.format(self._self_datadog_name, 'commit') | ||||||
|  |         return self._trace_method(self.__wrapped__.commit, span_name, {}, *args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def rollback(self, *args, **kwargs): | ||||||
|  |         span_name = '{}.{}'.format(self._self_datadog_name, 'rollback') | ||||||
|  |         return self._trace_method(self.__wrapped__.rollback, span_name, {}, *args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _get_vendor(conn): | ||||||
|  |     """ Return the vendor (e.g postgres, mysql) of the given | ||||||
|  |         database. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         name = _get_module_name(conn) | ||||||
|  |     except Exception: | ||||||
|  |         log.debug('couldnt parse module name', exc_info=True) | ||||||
|  |         name = 'sql' | ||||||
|  |     return sql.normalize_vendor(name) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _get_module_name(conn): | ||||||
|  |     return conn.__class__.__module__.split('.')[0] | ||||||
|  | @ -0,0 +1,101 @@ | ||||||
|  | """ | ||||||
|  | The Django integration will trace users requests, template renderers, database and cache | ||||||
|  | calls. | ||||||
|  | 
 | ||||||
|  | **Note:** by default the tracer is **disabled** (will not send spans) when | ||||||
|  | the Django setting ``DEBUG`` is ``True``. This can be overridden by explicitly enabling | ||||||
|  | the tracer with ``DATADOG_TRACE['ENABLED'] = True``, as described below. | ||||||
|  | 
 | ||||||
|  | To enable the Django integration, add the application to your installed | ||||||
|  | apps, as follows:: | ||||||
|  | 
 | ||||||
|  |     INSTALLED_APPS = [ | ||||||
|  |         # your Django apps... | ||||||
|  | 
 | ||||||
|  |         # the order is not important | ||||||
|  |         'ddtrace.contrib.django', | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  | The configuration for this integration is namespaced under the ``DATADOG_TRACE`` | ||||||
|  | Django setting. For example, your ``settings.py`` may contain:: | ||||||
|  | 
 | ||||||
|  |     DATADOG_TRACE = { | ||||||
|  |         'DEFAULT_SERVICE': 'my-django-app', | ||||||
|  |         'TAGS': {'env': 'production'}, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | If you need to access to Datadog settings, you can:: | ||||||
|  | 
 | ||||||
|  |     from ddtrace.contrib.django.conf import settings | ||||||
|  | 
 | ||||||
|  |     tracer = settings.TRACER | ||||||
|  |     tracer.trace("something") | ||||||
|  |     # your code ... | ||||||
|  | 
 | ||||||
|  | To have Django capture the tracer logs, ensure the ``LOGGING`` variable in | ||||||
|  | ``settings.py`` looks similar to:: | ||||||
|  | 
 | ||||||
|  |     LOGGING = { | ||||||
|  |         'loggers': { | ||||||
|  |             'ddtrace': { | ||||||
|  |                 'handlers': ['console'], | ||||||
|  |                 'level': 'WARNING', | ||||||
|  |             }, | ||||||
|  |         }, | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | The available settings are: | ||||||
|  | 
 | ||||||
|  | * ``DEFAULT_SERVICE`` (default: ``'django'``): set the service name used by the | ||||||
|  |   tracer. Usually this configuration must be updated with a meaningful name. | ||||||
|  | * ``DEFAULT_DATABASE_PREFIX`` (default: ``''``): set a prefix value to database services, | ||||||
|  |   so that your service is listed such as `prefix-defaultdb`. | ||||||
|  | * ``DEFAULT_CACHE_SERVICE`` (default: ``''``): set the django cache service name used | ||||||
|  |   by the tracer. Change this name if you want to see django cache spans as a cache application. | ||||||
|  | * ``TAGS`` (default: ``{}``): set global tags that should be applied to all | ||||||
|  |   spans. | ||||||
|  | * ``TRACER`` (default: ``ddtrace.tracer``): set the default tracer | ||||||
|  |   instance that is used to trace Django internals. By default the ``ddtrace`` | ||||||
|  |   tracer is used. | ||||||
|  | * ``ENABLED`` (default: ``not django_settings.DEBUG``): defines if the tracer is | ||||||
|  |   enabled or not. If set to false, the code is still instrumented but no spans | ||||||
|  |   are sent to the trace agent. This setting cannot be changed at runtime | ||||||
|  |   and a restart is required. By default the tracer is disabled when in ``DEBUG`` | ||||||
|  |   mode, enabled otherwise. | ||||||
|  | * ``DISTRIBUTED_TRACING`` (default: ``True``): defines if the tracer should | ||||||
|  |   use incoming X-DATADOG-* HTTP headers to extend a trace created remotely. It is | ||||||
|  |   required for distributed tracing if this application is called remotely from another | ||||||
|  |   instrumented application. | ||||||
|  |   We suggest to enable it only for internal services where headers are under your control. | ||||||
|  | * ``ANALYTICS_ENABLED`` (default: ``None``): enables APM events in Trace Search & Analytics. | ||||||
|  | * ``AGENT_HOSTNAME`` (default: ``localhost``): define the hostname of the trace agent. | ||||||
|  | * ``AGENT_PORT`` (default: ``8126``): define the port of the trace agent. | ||||||
|  | * ``AUTO_INSTRUMENT`` (default: ``True``): if set to false the code will not be | ||||||
|  |   instrumented (even if ``INSTRUMENT_DATABASE``, ``INSTRUMENT_CACHE`` or | ||||||
|  |   ``INSTRUMENT_TEMPLATE`` are set to ``True``), while the tracer may be active | ||||||
|  |   for your internal usage. This could be useful if you want to use the Django | ||||||
|  |   integration, but you want to trace only particular functions or views. If set | ||||||
|  |   to False, the request middleware will be disabled even if present. | ||||||
|  | * ``INSTRUMENT_DATABASE`` (default: ``True``): if set to ``False`` database will not | ||||||
|  |   be instrumented. Only configurable when ``AUTO_INSTRUMENT`` is set to ``True``. | ||||||
|  | * ``INSTRUMENT_CACHE`` (default: ``True``): if set to ``False`` cache will not | ||||||
|  |   be instrumented. Only configurable when ``AUTO_INSTRUMENT`` is set to ``True``. | ||||||
|  | * ``INSTRUMENT_TEMPLATE`` (default: ``True``): if set to ``False`` template | ||||||
|  |   rendering will not be instrumented. Only configurable when ``AUTO_INSTRUMENT`` | ||||||
|  |   is set to ``True``. | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['django'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .middleware import TraceMiddleware, TraceExceptionMiddleware | ||||||
|  |         from .patch import patch | ||||||
|  |         __all__ = ['TraceMiddleware', 'TraceExceptionMiddleware', 'patch'] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # define the Django app configuration | ||||||
|  | default_app_config = 'ddtrace.contrib.django.apps.TracerConfig' | ||||||
|  | @ -0,0 +1,19 @@ | ||||||
|  | # 3rd party | ||||||
|  | from django.apps import AppConfig, apps | ||||||
|  | 
 | ||||||
|  | # project | ||||||
|  | from .patch import apply_django_patches | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TracerConfig(AppConfig): | ||||||
|  |     name = 'ddtrace.contrib.django' | ||||||
|  |     label = 'datadog_django' | ||||||
|  | 
 | ||||||
|  |     def ready(self): | ||||||
|  |         """ | ||||||
|  |         Ready is called as soon as the registry is fully populated. | ||||||
|  |         Tracing capabilities must be enabled in this function so that | ||||||
|  |         all Django internals are properly configured. | ||||||
|  |         """ | ||||||
|  |         rest_framework_is_installed = apps.is_installed('rest_framework') | ||||||
|  |         apply_django_patches(patch_rest_framework=rest_framework_is_installed) | ||||||
|  | @ -0,0 +1,111 @@ | ||||||
|  | from functools import wraps | ||||||
|  | 
 | ||||||
|  | from django.conf import settings as django_settings | ||||||
|  | 
 | ||||||
|  | from ...ext import SpanTypes | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | from .conf import settings, import_from_string | ||||||
|  | from .utils import quantize_key_values, _resource_from_cache_prefix | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | # code instrumentation | ||||||
|  | DATADOG_NAMESPACE = '__datadog_original_{method}' | ||||||
|  | TRACED_METHODS = [ | ||||||
|  |     'get', | ||||||
|  |     'set', | ||||||
|  |     'add', | ||||||
|  |     'delete', | ||||||
|  |     'incr', | ||||||
|  |     'decr', | ||||||
|  |     'get_many', | ||||||
|  |     'set_many', | ||||||
|  |     'delete_many', | ||||||
|  | ] | ||||||
|  | 
 | ||||||
|  | # standard tags | ||||||
|  | CACHE_BACKEND = 'django.cache.backend' | ||||||
|  | CACHE_COMMAND_KEY = 'django.cache.key' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch_cache(tracer): | ||||||
|  |     """ | ||||||
|  |     Function that patches the inner cache system. Because the cache backend | ||||||
|  |     can have different implementations and connectors, this function must | ||||||
|  |     handle all possible interactions with the Django cache. What follows | ||||||
|  |     is currently traced: | ||||||
|  | 
 | ||||||
|  |     * in-memory cache | ||||||
|  |     * the cache client wrapper that could use any of the common | ||||||
|  |       Django supported cache servers (Redis, Memcached, Database, Custom) | ||||||
|  |     """ | ||||||
|  |     # discover used cache backends | ||||||
|  |     cache_backends = set([cache['BACKEND'] for cache in django_settings.CACHES.values()]) | ||||||
|  | 
 | ||||||
|  |     def _trace_operation(fn, method_name): | ||||||
|  |         """ | ||||||
|  |         Return a wrapped function that traces a cache operation | ||||||
|  |         """ | ||||||
|  |         cache_service_name = settings.DEFAULT_CACHE_SERVICE \ | ||||||
|  |             if settings.DEFAULT_CACHE_SERVICE else settings.DEFAULT_SERVICE | ||||||
|  | 
 | ||||||
|  |         @wraps(fn) | ||||||
|  |         def wrapped(self, *args, **kwargs): | ||||||
|  |             # get the original function method | ||||||
|  |             method = getattr(self, DATADOG_NAMESPACE.format(method=method_name)) | ||||||
|  |             with tracer.trace('django.cache', span_type=SpanTypes.CACHE, service=cache_service_name) as span: | ||||||
|  |                 # update the resource name and tag the cache backend | ||||||
|  |                 span.resource = _resource_from_cache_prefix(method_name, self) | ||||||
|  |                 cache_backend = '{}.{}'.format(self.__module__, self.__class__.__name__) | ||||||
|  |                 span.set_tag(CACHE_BACKEND, cache_backend) | ||||||
|  | 
 | ||||||
|  |                 if args: | ||||||
|  |                     keys = quantize_key_values(args[0]) | ||||||
|  |                     span.set_tag(CACHE_COMMAND_KEY, keys) | ||||||
|  | 
 | ||||||
|  |                 return method(*args, **kwargs) | ||||||
|  |         return wrapped | ||||||
|  | 
 | ||||||
|  |     def _wrap_method(cls, method_name): | ||||||
|  |         """ | ||||||
|  |         For the given class, wraps the method name with a traced operation | ||||||
|  |         so that the original method is executed, while the span is properly | ||||||
|  |         created | ||||||
|  |         """ | ||||||
|  |         # check if the backend owns the given bounded method | ||||||
|  |         if not hasattr(cls, method_name): | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         # prevent patching each backend's method more than once | ||||||
|  |         if hasattr(cls, DATADOG_NAMESPACE.format(method=method_name)): | ||||||
|  |             log.debug('%s already traced', method_name) | ||||||
|  |         else: | ||||||
|  |             method = getattr(cls, method_name) | ||||||
|  |             setattr(cls, DATADOG_NAMESPACE.format(method=method_name), method) | ||||||
|  |             setattr(cls, method_name, _trace_operation(method, method_name)) | ||||||
|  | 
 | ||||||
|  |     # trace all backends | ||||||
|  |     for cache_module in cache_backends: | ||||||
|  |         cache = import_from_string(cache_module, cache_module) | ||||||
|  | 
 | ||||||
|  |         for method in TRACED_METHODS: | ||||||
|  |             _wrap_method(cache, method) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch_method(cls, method_name): | ||||||
|  |     method = getattr(cls, DATADOG_NAMESPACE.format(method=method_name), None) | ||||||
|  |     if method is None: | ||||||
|  |         log.debug('nothing to do, the class is not patched') | ||||||
|  |         return | ||||||
|  |     setattr(cls, method_name, method) | ||||||
|  |     delattr(cls, DATADOG_NAMESPACE.format(method=method_name)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch_cache(): | ||||||
|  |     cache_backends = set([cache['BACKEND'] for cache in django_settings.CACHES.values()]) | ||||||
|  |     for cache_module in cache_backends: | ||||||
|  |         cache = import_from_string(cache_module, cache_module) | ||||||
|  | 
 | ||||||
|  |         for method in TRACED_METHODS: | ||||||
|  |             unpatch_method(cache, method) | ||||||
|  | @ -0,0 +1,27 @@ | ||||||
|  | import django | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if django.VERSION >= (1, 10, 1): | ||||||
|  |     from django.urls import get_resolver | ||||||
|  | 
 | ||||||
|  |     def user_is_authenticated(user): | ||||||
|  |         # Explicit comparison due to the following bug | ||||||
|  |         # https://code.djangoproject.com/ticket/26988 | ||||||
|  |         return user.is_authenticated == True  # noqa E712 | ||||||
|  | else: | ||||||
|  |     from django.conf import settings | ||||||
|  |     from django.core import urlresolvers | ||||||
|  | 
 | ||||||
|  |     def user_is_authenticated(user): | ||||||
|  |         return user.is_authenticated() | ||||||
|  | 
 | ||||||
|  |     if django.VERSION >= (1, 9, 0): | ||||||
|  |         def get_resolver(urlconf=None): | ||||||
|  |             urlconf = urlconf or settings.ROOT_URLCONF | ||||||
|  |             urlresolvers.set_urlconf(urlconf) | ||||||
|  |             return urlresolvers.get_resolver(urlconf) | ||||||
|  |     else: | ||||||
|  |         def get_resolver(urlconf=None): | ||||||
|  |             urlconf = urlconf or settings.ROOT_URLCONF | ||||||
|  |             urlresolvers.set_urlconf(urlconf) | ||||||
|  |             return urlresolvers.RegexURLResolver(r'^/', urlconf) | ||||||
|  | @ -0,0 +1,163 @@ | ||||||
|  | """ | ||||||
|  | Settings for Datadog tracer are all namespaced in the DATADOG_TRACE setting. | ||||||
|  | For example your project's `settings.py` file might look like this:: | ||||||
|  | 
 | ||||||
|  |     DATADOG_TRACE = { | ||||||
|  |         'TRACER': 'myapp.tracer', | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | This module provides the `setting` object, that is used to access | ||||||
|  | Datadog settings, checking for user settings first, then falling | ||||||
|  | back to the defaults. | ||||||
|  | """ | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | 
 | ||||||
|  | import os | ||||||
|  | import importlib | ||||||
|  | 
 | ||||||
|  | from django.conf import settings as django_settings | ||||||
|  | 
 | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | # List of available settings with their defaults | ||||||
|  | DEFAULTS = { | ||||||
|  |     'AGENT_HOSTNAME': 'localhost', | ||||||
|  |     'AGENT_PORT': 8126, | ||||||
|  |     'AUTO_INSTRUMENT': True, | ||||||
|  |     'INSTRUMENT_CACHE': True, | ||||||
|  |     'INSTRUMENT_DATABASE': True, | ||||||
|  |     'INSTRUMENT_TEMPLATE': True, | ||||||
|  |     'DEFAULT_DATABASE_PREFIX': '', | ||||||
|  |     'DEFAULT_SERVICE': 'django', | ||||||
|  |     'DEFAULT_CACHE_SERVICE': '', | ||||||
|  |     'ENABLED': True, | ||||||
|  |     'DISTRIBUTED_TRACING': True, | ||||||
|  |     'ANALYTICS_ENABLED': None, | ||||||
|  |     'ANALYTICS_SAMPLE_RATE': True, | ||||||
|  |     'TRACE_QUERY_STRING': None, | ||||||
|  |     'TAGS': {}, | ||||||
|  |     'TRACER': 'ddtrace.tracer', | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | # List of settings that may be in string import notation. | ||||||
|  | IMPORT_STRINGS = ( | ||||||
|  |     'TRACER', | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | # List of settings that have been removed | ||||||
|  | REMOVED_SETTINGS = () | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def import_from_string(val, setting_name): | ||||||
|  |     """ | ||||||
|  |     Attempt to import a class from a string representation. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         # Nod to tastypie's use of importlib. | ||||||
|  |         parts = val.split('.') | ||||||
|  |         module_path, class_name = '.'.join(parts[:-1]), parts[-1] | ||||||
|  |         module = importlib.import_module(module_path) | ||||||
|  |         return getattr(module, class_name) | ||||||
|  |     except (ImportError, AttributeError) as e: | ||||||
|  |         msg = 'Could not import "{}" for setting "{}". {}: {}.'.format( | ||||||
|  |             val, | ||||||
|  |             setting_name, | ||||||
|  |             e.__class__.__name__, | ||||||
|  |             e, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         raise ImportError(msg) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class DatadogSettings(object): | ||||||
|  |     """ | ||||||
|  |     A settings object, that allows Datadog settings to be accessed as properties. | ||||||
|  |     For example: | ||||||
|  | 
 | ||||||
|  |         from ddtrace.contrib.django.conf import settings | ||||||
|  | 
 | ||||||
|  |         tracer = settings.TRACER | ||||||
|  | 
 | ||||||
|  |     Any setting with string import paths will be automatically resolved | ||||||
|  |     and return the class, rather than the string literal. | ||||||
|  |     """ | ||||||
|  |     def __init__(self, user_settings=None, defaults=None, import_strings=None): | ||||||
|  |         if user_settings: | ||||||
|  |             self._user_settings = self.__check_user_settings(user_settings) | ||||||
|  | 
 | ||||||
|  |         self.defaults = defaults or DEFAULTS | ||||||
|  |         if os.environ.get('DATADOG_ENV'): | ||||||
|  |             self.defaults['TAGS'].update({'env': os.environ.get('DATADOG_ENV')}) | ||||||
|  |         if os.environ.get('DATADOG_SERVICE_NAME'): | ||||||
|  |             self.defaults['DEFAULT_SERVICE'] = os.environ.get('DATADOG_SERVICE_NAME') | ||||||
|  | 
 | ||||||
|  |         host = os.environ.get('DD_AGENT_HOST', os.environ.get('DATADOG_TRACE_AGENT_HOSTNAME')) | ||||||
|  |         if host: | ||||||
|  |             self.defaults['AGENT_HOSTNAME'] = host | ||||||
|  | 
 | ||||||
|  |         port = os.environ.get('DD_TRACE_AGENT_PORT', os.environ.get('DATADOG_TRACE_AGENT_PORT')) | ||||||
|  |         if port: | ||||||
|  |             # if the agent port is a string, the underlying library that creates the socket | ||||||
|  |             # stops working | ||||||
|  |             try: | ||||||
|  |                 port = int(port) | ||||||
|  |             except ValueError: | ||||||
|  |                 log.warning('DD_TRACE_AGENT_PORT is not an integer value; default to 8126') | ||||||
|  |             else: | ||||||
|  |                 self.defaults['AGENT_PORT'] = port | ||||||
|  | 
 | ||||||
|  |         self.import_strings = import_strings or IMPORT_STRINGS | ||||||
|  | 
 | ||||||
|  |     @property | ||||||
|  |     def user_settings(self): | ||||||
|  |         if not hasattr(self, '_user_settings'): | ||||||
|  |             self._user_settings = getattr(django_settings, 'DATADOG_TRACE', {}) | ||||||
|  | 
 | ||||||
|  |         # TODO[manu]: prevents docs import errors; provide a better implementation | ||||||
|  |         if 'ENABLED' not in self._user_settings: | ||||||
|  |             self._user_settings['ENABLED'] = not django_settings.DEBUG | ||||||
|  |         return self._user_settings | ||||||
|  | 
 | ||||||
|  |     def __getattr__(self, attr): | ||||||
|  |         if attr not in self.defaults: | ||||||
|  |             raise AttributeError('Invalid setting: "{}"'.format(attr)) | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             # Check if present in user settings | ||||||
|  |             val = self.user_settings[attr] | ||||||
|  |         except KeyError: | ||||||
|  |             # Otherwise, fall back to defaults | ||||||
|  |             val = self.defaults[attr] | ||||||
|  | 
 | ||||||
|  |         # Coerce import strings into classes | ||||||
|  |         if attr in self.import_strings: | ||||||
|  |             val = import_from_string(val, attr) | ||||||
|  | 
 | ||||||
|  |         # Cache the result | ||||||
|  |         setattr(self, attr, val) | ||||||
|  |         return val | ||||||
|  | 
 | ||||||
|  |     def __check_user_settings(self, user_settings): | ||||||
|  |         SETTINGS_DOC = 'http://pypi.datadoghq.com/trace/docs/#module-ddtrace.contrib.django' | ||||||
|  |         for setting in REMOVED_SETTINGS: | ||||||
|  |             if setting in user_settings: | ||||||
|  |                 raise RuntimeError( | ||||||
|  |                     'The "{}" setting has been removed, check "{}".'.format(setting, SETTINGS_DOC) | ||||||
|  |                 ) | ||||||
|  |         return user_settings | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | settings = DatadogSettings(None, DEFAULTS, IMPORT_STRINGS) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def reload_settings(*args, **kwargs): | ||||||
|  |     """ | ||||||
|  |     Triggers a reload when Django emits the reloading signal | ||||||
|  |     """ | ||||||
|  |     global settings | ||||||
|  |     setting, value = kwargs['setting'], kwargs['value'] | ||||||
|  |     if setting == 'DATADOG_TRACE': | ||||||
|  |         settings = DatadogSettings(value, DEFAULTS, IMPORT_STRINGS) | ||||||
|  | @ -0,0 +1,76 @@ | ||||||
|  | from django.db import connections | ||||||
|  | 
 | ||||||
|  | # project | ||||||
|  | from ...ext import sql as sqlx | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | from ...pin import Pin | ||||||
|  | 
 | ||||||
|  | from .conf import settings | ||||||
|  | from ..dbapi import TracedCursor as DbApiTracedCursor | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | CURSOR_ATTR = '_datadog_original_cursor' | ||||||
|  | ALL_CONNS_ATTR = '_datadog_original_connections_all' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch_db(tracer): | ||||||
|  |     if hasattr(connections, ALL_CONNS_ATTR): | ||||||
|  |         log.debug('db already patched') | ||||||
|  |         return | ||||||
|  |     setattr(connections, ALL_CONNS_ATTR, connections.all) | ||||||
|  | 
 | ||||||
|  |     def all_connections(self): | ||||||
|  |         conns = getattr(self, ALL_CONNS_ATTR)() | ||||||
|  |         for conn in conns: | ||||||
|  |             patch_conn(tracer, conn) | ||||||
|  |         return conns | ||||||
|  | 
 | ||||||
|  |     connections.all = all_connections.__get__(connections, type(connections)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch_db(): | ||||||
|  |     for c in connections.all(): | ||||||
|  |         unpatch_conn(c) | ||||||
|  | 
 | ||||||
|  |     all_connections = getattr(connections, ALL_CONNS_ATTR, None) | ||||||
|  |     if all_connections is None: | ||||||
|  |         log.debug('nothing to do, the db is not patched') | ||||||
|  |         return | ||||||
|  |     connections.all = all_connections | ||||||
|  |     delattr(connections, ALL_CONNS_ATTR) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch_conn(tracer, conn): | ||||||
|  |     if hasattr(conn, CURSOR_ATTR): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     setattr(conn, CURSOR_ATTR, conn.cursor) | ||||||
|  | 
 | ||||||
|  |     def cursor(): | ||||||
|  |         database_prefix = ( | ||||||
|  |             '{}-'.format(settings.DEFAULT_DATABASE_PREFIX) | ||||||
|  |             if settings.DEFAULT_DATABASE_PREFIX else '' | ||||||
|  |         ) | ||||||
|  |         alias = getattr(conn, 'alias', 'default') | ||||||
|  |         service = '{}{}{}'.format(database_prefix, alias, 'db') | ||||||
|  |         vendor = getattr(conn, 'vendor', 'db') | ||||||
|  |         prefix = sqlx.normalize_vendor(vendor) | ||||||
|  |         tags = { | ||||||
|  |             'django.db.vendor': vendor, | ||||||
|  |             'django.db.alias': alias, | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         pin = Pin(service, tags=tags, tracer=tracer, app=prefix) | ||||||
|  |         return DbApiTracedCursor(conn._datadog_original_cursor(), pin) | ||||||
|  | 
 | ||||||
|  |     conn.cursor = cursor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch_conn(conn): | ||||||
|  |     cursor = getattr(conn, CURSOR_ATTR, None) | ||||||
|  |     if cursor is None: | ||||||
|  |         log.debug('nothing to do, the connection is not patched') | ||||||
|  |         return | ||||||
|  |     conn.cursor = cursor | ||||||
|  |     delattr(conn, CURSOR_ATTR) | ||||||
|  | @ -0,0 +1,230 @@ | ||||||
|  | # project | ||||||
|  | from .conf import settings | ||||||
|  | from .compat import user_is_authenticated, get_resolver | ||||||
|  | from .utils import get_request_uri | ||||||
|  | 
 | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...contrib import func_name | ||||||
|  | from ...ext import SpanTypes, http | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | from ...propagation.http import HTTPPropagator | ||||||
|  | from ...settings import config | ||||||
|  | 
 | ||||||
|  | # 3p | ||||||
|  | from django.core.exceptions import MiddlewareNotUsed | ||||||
|  | from django.conf import settings as django_settings | ||||||
|  | import django | ||||||
|  | 
 | ||||||
|  | try: | ||||||
|  |     from django.utils.deprecation import MiddlewareMixin | ||||||
|  | 
 | ||||||
|  |     MiddlewareClass = MiddlewareMixin | ||||||
|  | except ImportError: | ||||||
|  |     MiddlewareClass = object | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | EXCEPTION_MIDDLEWARE = "ddtrace.contrib.django.TraceExceptionMiddleware" | ||||||
|  | TRACE_MIDDLEWARE = "ddtrace.contrib.django.TraceMiddleware" | ||||||
|  | MIDDLEWARE = "MIDDLEWARE" | ||||||
|  | MIDDLEWARE_CLASSES = "MIDDLEWARE_CLASSES" | ||||||
|  | 
 | ||||||
|  | # Default views list available from: | ||||||
|  | #   https://github.com/django/django/blob/38e2fdadfd9952e751deed662edf4c496d238f28/django/views/defaults.py | ||||||
|  | # DEV: Django doesn't call `process_view` when falling back to one of these internal error handling views | ||||||
|  | # DEV: We only use these names when `span.resource == 'unknown'` and we have one of these status codes | ||||||
|  | _django_default_views = { | ||||||
|  |     400: "django.views.defaults.bad_request", | ||||||
|  |     403: "django.views.defaults.permission_denied", | ||||||
|  |     404: "django.views.defaults.page_not_found", | ||||||
|  |     500: "django.views.defaults.server_error", | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _analytics_enabled(): | ||||||
|  |     return ( | ||||||
|  |         (config.analytics_enabled and settings.ANALYTICS_ENABLED is not False) or settings.ANALYTICS_ENABLED is True | ||||||
|  |     ) and settings.ANALYTICS_SAMPLE_RATE is not None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_middleware_insertion_point(): | ||||||
|  |     """Returns the attribute name and collection object for the Django middleware. | ||||||
|  | 
 | ||||||
|  |     If middleware cannot be found, returns None for the middleware collection. | ||||||
|  |     """ | ||||||
|  |     middleware = getattr(django_settings, MIDDLEWARE, None) | ||||||
|  |     # Prioritise MIDDLEWARE over ..._CLASSES, but only in 1.10 and later. | ||||||
|  |     if middleware is not None and django.VERSION >= (1, 10): | ||||||
|  |         return MIDDLEWARE, middleware | ||||||
|  |     return MIDDLEWARE_CLASSES, getattr(django_settings, MIDDLEWARE_CLASSES, None) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def insert_trace_middleware(): | ||||||
|  |     middleware_attribute, middleware = get_middleware_insertion_point() | ||||||
|  |     if middleware is not None and TRACE_MIDDLEWARE not in set(middleware): | ||||||
|  |         setattr(django_settings, middleware_attribute, type(middleware)((TRACE_MIDDLEWARE,)) + middleware) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def remove_trace_middleware(): | ||||||
|  |     _, middleware = get_middleware_insertion_point() | ||||||
|  |     if middleware and TRACE_MIDDLEWARE in set(middleware): | ||||||
|  |         middleware.remove(TRACE_MIDDLEWARE) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def insert_exception_middleware(): | ||||||
|  |     middleware_attribute, middleware = get_middleware_insertion_point() | ||||||
|  |     if middleware is not None and EXCEPTION_MIDDLEWARE not in set(middleware): | ||||||
|  |         setattr(django_settings, middleware_attribute, middleware + type(middleware)((EXCEPTION_MIDDLEWARE,))) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def remove_exception_middleware(): | ||||||
|  |     _, middleware = get_middleware_insertion_point() | ||||||
|  |     if middleware and EXCEPTION_MIDDLEWARE in set(middleware): | ||||||
|  |         middleware.remove(EXCEPTION_MIDDLEWARE) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class InstrumentationMixin(MiddlewareClass): | ||||||
|  |     """ | ||||||
|  |     Useful mixin base class for tracing middlewares | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def __init__(self, get_response=None): | ||||||
|  |         # disable the middleware if the tracer is not enabled | ||||||
|  |         # or if the auto instrumentation is disabled | ||||||
|  |         self.get_response = get_response | ||||||
|  |         if not settings.AUTO_INSTRUMENT: | ||||||
|  |             raise MiddlewareNotUsed | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TraceExceptionMiddleware(InstrumentationMixin): | ||||||
|  |     """ | ||||||
|  |     Middleware that traces exceptions raised | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def process_exception(self, request, exception): | ||||||
|  |         try: | ||||||
|  |             span = _get_req_span(request) | ||||||
|  |             if span: | ||||||
|  |                 span.set_tag(http.STATUS_CODE, "500") | ||||||
|  |                 span.set_traceback()  # will set the exception info | ||||||
|  |         except Exception: | ||||||
|  |             log.debug("error processing exception", exc_info=True) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TraceMiddleware(InstrumentationMixin): | ||||||
|  |     """ | ||||||
|  |     Middleware that traces Django requests | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def process_request(self, request): | ||||||
|  |         tracer = settings.TRACER | ||||||
|  |         if settings.DISTRIBUTED_TRACING: | ||||||
|  |             propagator = HTTPPropagator() | ||||||
|  |             context = propagator.extract(request.META) | ||||||
|  |             # Only need to active the new context if something was propagated | ||||||
|  |             if context.trace_id: | ||||||
|  |                 tracer.context_provider.activate(context) | ||||||
|  |         try: | ||||||
|  |             span = tracer.trace( | ||||||
|  |                 "django.request", | ||||||
|  |                 service=settings.DEFAULT_SERVICE, | ||||||
|  |                 resource="unknown",  # will be filled by process view | ||||||
|  |                 span_type=SpanTypes.WEB, | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             # set analytics sample rate | ||||||
|  |             # DEV: django is special case maintains separate configuration from config api | ||||||
|  |             if _analytics_enabled() and settings.ANALYTICS_SAMPLE_RATE is not None: | ||||||
|  |                 span.set_tag( | ||||||
|  |                     ANALYTICS_SAMPLE_RATE_KEY, settings.ANALYTICS_SAMPLE_RATE, | ||||||
|  |                 ) | ||||||
|  | 
 | ||||||
|  |             # Set HTTP Request tags | ||||||
|  |             span.set_tag(http.METHOD, request.method) | ||||||
|  |             span.set_tag(http.URL, get_request_uri(request)) | ||||||
|  |             trace_query_string = settings.TRACE_QUERY_STRING | ||||||
|  |             if trace_query_string is None: | ||||||
|  |                 trace_query_string = config.django.trace_query_string | ||||||
|  |             if trace_query_string: | ||||||
|  |                 span.set_tag(http.QUERY_STRING, request.META["QUERY_STRING"]) | ||||||
|  |             _set_req_span(request, span) | ||||||
|  |         except Exception: | ||||||
|  |             log.debug("error tracing request", exc_info=True) | ||||||
|  | 
 | ||||||
|  |     def process_view(self, request, view_func, *args, **kwargs): | ||||||
|  |         span = _get_req_span(request) | ||||||
|  |         if span: | ||||||
|  |             span.resource = func_name(view_func) | ||||||
|  | 
 | ||||||
|  |     def process_response(self, request, response): | ||||||
|  |         try: | ||||||
|  |             span = _get_req_span(request) | ||||||
|  |             if span: | ||||||
|  |                 if response.status_code < 500 and span.error: | ||||||
|  |                     # remove any existing stack trace since it must have been | ||||||
|  |                     # handled appropriately | ||||||
|  |                     span._remove_exc_info() | ||||||
|  | 
 | ||||||
|  |                 # If `process_view` was not called, try to determine the correct `span.resource` to set | ||||||
|  |                 # DEV: `process_view` won't get called if a middle `process_request` returns an HttpResponse | ||||||
|  |                 # DEV: `process_view` won't get called when internal error handlers are used (e.g. for 404 responses) | ||||||
|  |                 if span.resource == "unknown": | ||||||
|  |                     try: | ||||||
|  |                         # Attempt to lookup the view function from the url resolver | ||||||
|  |                         #   https://github.com/django/django/blob/38e2fdadfd9952e751deed662edf4c496d238f28/django/core/handlers/base.py#L104-L113  # noqa | ||||||
|  |                         urlconf = None | ||||||
|  |                         if hasattr(request, "urlconf"): | ||||||
|  |                             urlconf = request.urlconf | ||||||
|  |                         resolver = get_resolver(urlconf) | ||||||
|  | 
 | ||||||
|  |                         # Try to resolve the Django view for handling this request | ||||||
|  |                         if getattr(request, "request_match", None): | ||||||
|  |                             request_match = request.request_match | ||||||
|  |                         else: | ||||||
|  |                             # This may raise a `django.urls.exceptions.Resolver404` exception | ||||||
|  |                             request_match = resolver.resolve(request.path_info) | ||||||
|  |                         span.resource = func_name(request_match.func) | ||||||
|  |                     except Exception: | ||||||
|  |                         log.debug("error determining request view function", exc_info=True) | ||||||
|  | 
 | ||||||
|  |                         # If the view could not be found, try to set from a static list of | ||||||
|  |                         # known internal error handler views | ||||||
|  |                         span.resource = _django_default_views.get(response.status_code, "unknown") | ||||||
|  | 
 | ||||||
|  |                 span.set_tag(http.STATUS_CODE, response.status_code) | ||||||
|  |                 span = _set_auth_tags(span, request) | ||||||
|  |                 span.finish() | ||||||
|  |         except Exception: | ||||||
|  |             log.debug("error tracing request", exc_info=True) | ||||||
|  |         finally: | ||||||
|  |             return response | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _get_req_span(request): | ||||||
|  |     """ Return the datadog span from the given request. """ | ||||||
|  |     return getattr(request, "_datadog_request_span", None) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _set_req_span(request, span): | ||||||
|  |     """ Set the datadog span on the given request. """ | ||||||
|  |     return setattr(request, "_datadog_request_span", span) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _set_auth_tags(span, request): | ||||||
|  |     """ Patch any available auth tags from the request onto the span. """ | ||||||
|  |     user = getattr(request, "user", None) | ||||||
|  |     if not user: | ||||||
|  |         return span | ||||||
|  | 
 | ||||||
|  |     if hasattr(user, "is_authenticated"): | ||||||
|  |         span.set_tag("django.user.is_authenticated", user_is_authenticated(user)) | ||||||
|  | 
 | ||||||
|  |     uid = getattr(user, "pk", None) | ||||||
|  |     if uid: | ||||||
|  |         span.set_tag("django.user.id", uid) | ||||||
|  | 
 | ||||||
|  |     uname = getattr(user, "username", None) | ||||||
|  |     if uname: | ||||||
|  |         span.set_tag("django.user.name", uname) | ||||||
|  | 
 | ||||||
|  |     return span | ||||||
|  | @ -0,0 +1,94 @@ | ||||||
|  | # 3rd party | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | import django | ||||||
|  | from django.db import connections | ||||||
|  | 
 | ||||||
|  | # project | ||||||
|  | from .db import patch_db | ||||||
|  | from .conf import settings | ||||||
|  | from .cache import patch_cache | ||||||
|  | from .templates import patch_template | ||||||
|  | from .middleware import insert_exception_middleware, insert_trace_middleware | ||||||
|  | 
 | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """Patch the instrumented methods | ||||||
|  |     """ | ||||||
|  |     if getattr(django, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(django, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     _w = wrapt.wrap_function_wrapper | ||||||
|  |     _w('django', 'setup', traced_setup) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_setup(wrapped, instance, args, kwargs): | ||||||
|  |     from django.conf import settings | ||||||
|  | 
 | ||||||
|  |     if 'ddtrace.contrib.django' not in settings.INSTALLED_APPS: | ||||||
|  |         if isinstance(settings.INSTALLED_APPS, tuple): | ||||||
|  |             # INSTALLED_APPS is a tuple < 1.9 | ||||||
|  |             settings.INSTALLED_APPS = settings.INSTALLED_APPS + ('ddtrace.contrib.django', ) | ||||||
|  |         else: | ||||||
|  |             settings.INSTALLED_APPS.append('ddtrace.contrib.django') | ||||||
|  | 
 | ||||||
|  |     wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def apply_django_patches(patch_rest_framework): | ||||||
|  |     """ | ||||||
|  |     Ready is called as soon as the registry is fully populated. | ||||||
|  |     In order for all Django internals are properly configured, this | ||||||
|  |     must be called after the app is finished starting | ||||||
|  |     """ | ||||||
|  |     tracer = settings.TRACER | ||||||
|  | 
 | ||||||
|  |     if settings.TAGS: | ||||||
|  |         tracer.set_tags(settings.TAGS) | ||||||
|  | 
 | ||||||
|  |     # configure the tracer instance | ||||||
|  |     # TODO[manu]: we may use configure() but because it creates a new | ||||||
|  |     # AgentWriter, it breaks all tests. The configure() behavior must | ||||||
|  |     # be changed to use it in this integration | ||||||
|  |     tracer.enabled = settings.ENABLED | ||||||
|  |     tracer.writer.api.hostname = settings.AGENT_HOSTNAME | ||||||
|  |     tracer.writer.api.port = settings.AGENT_PORT | ||||||
|  | 
 | ||||||
|  |     if settings.AUTO_INSTRUMENT: | ||||||
|  |         # trace Django internals | ||||||
|  |         insert_trace_middleware() | ||||||
|  |         insert_exception_middleware() | ||||||
|  | 
 | ||||||
|  |         if settings.INSTRUMENT_TEMPLATE: | ||||||
|  |             try: | ||||||
|  |                 patch_template(tracer) | ||||||
|  |             except Exception: | ||||||
|  |                 log.exception('error patching Django template rendering') | ||||||
|  | 
 | ||||||
|  |         if settings.INSTRUMENT_DATABASE: | ||||||
|  |             try: | ||||||
|  |                 patch_db(tracer) | ||||||
|  |                 # This is the trigger to patch individual connections. | ||||||
|  |                 # By patching these here, all processes including | ||||||
|  |                 # management commands are also traced. | ||||||
|  |                 connections.all() | ||||||
|  |             except Exception: | ||||||
|  |                 log.exception('error patching Django database connections') | ||||||
|  | 
 | ||||||
|  |         if settings.INSTRUMENT_CACHE: | ||||||
|  |             try: | ||||||
|  |                 patch_cache(tracer) | ||||||
|  |             except Exception: | ||||||
|  |                 log.exception('error patching Django cache') | ||||||
|  | 
 | ||||||
|  |         # Instrument rest_framework app to trace custom exception handling. | ||||||
|  |         if patch_rest_framework: | ||||||
|  |             try: | ||||||
|  |                 from .restframework import patch_restframework | ||||||
|  |                 patch_restframework(tracer) | ||||||
|  |             except Exception: | ||||||
|  |                 log.exception('error patching rest_framework app') | ||||||
|  | @ -0,0 +1,42 @@ | ||||||
|  | from ddtrace.vendor.wrapt import wrap_function_wrapper as wrap | ||||||
|  | 
 | ||||||
|  | from rest_framework.views import APIView | ||||||
|  | 
 | ||||||
|  | from ...utils.wrappers import unwrap | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch_restframework(tracer): | ||||||
|  |     """ Patches rest_framework app. | ||||||
|  | 
 | ||||||
|  |     To trace exceptions occuring during view processing we currently use a TraceExceptionMiddleware. | ||||||
|  |     However the rest_framework handles exceptions before they come to our middleware. | ||||||
|  |     So we need to manually patch the rest_framework exception handler | ||||||
|  |     to set the exception stack trace in the current span. | ||||||
|  | 
 | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     def _traced_handle_exception(wrapped, instance, args, kwargs): | ||||||
|  |         """ Sets the error message, error type and exception stack trace to the current span | ||||||
|  |             before calling the original exception handler. | ||||||
|  |         """ | ||||||
|  |         span = tracer.current_span() | ||||||
|  |         if span is not None: | ||||||
|  |             span.set_traceback() | ||||||
|  | 
 | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     # do not patch if already patched | ||||||
|  |     if getattr(APIView, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     else: | ||||||
|  |         setattr(APIView, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     # trace the handle_exception method | ||||||
|  |     wrap('rest_framework.views', 'APIView.handle_exception', _traced_handle_exception) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch_restframework(): | ||||||
|  |     """ Unpatches rest_framework app.""" | ||||||
|  |     if getattr(APIView, '_datadog_patch', False): | ||||||
|  |         setattr(APIView, '_datadog_patch', False) | ||||||
|  |         unwrap(APIView, 'handle_exception') | ||||||
|  | @ -0,0 +1,48 @@ | ||||||
|  | """ | ||||||
|  | code to measure django template rendering. | ||||||
|  | """ | ||||||
|  | # project | ||||||
|  | from ...ext import SpanTypes | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | 
 | ||||||
|  | # 3p | ||||||
|  | from django.template import Template | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | RENDER_ATTR = '_datadog_original_render' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch_template(tracer): | ||||||
|  |     """ will patch django's template rendering function to include timing | ||||||
|  |         and trace information. | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     # FIXME[matt] we're patching the template class here. ideally we'd only | ||||||
|  |     # patch so we can use multiple tracers at once, but i suspect this is fine | ||||||
|  |     # in practice. | ||||||
|  |     if getattr(Template, RENDER_ATTR, None): | ||||||
|  |         log.debug('already patched') | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     setattr(Template, RENDER_ATTR, Template.render) | ||||||
|  | 
 | ||||||
|  |     def traced_render(self, context): | ||||||
|  |         with tracer.trace('django.template', span_type=SpanTypes.TEMPLATE) as span: | ||||||
|  |             try: | ||||||
|  |                 return Template._datadog_original_render(self, context) | ||||||
|  |             finally: | ||||||
|  |                 template_name = self.name or getattr(context, 'template_name', None) or 'unknown' | ||||||
|  |                 span.resource = template_name | ||||||
|  |                 span.set_tag('django.template_name', template_name) | ||||||
|  | 
 | ||||||
|  |     Template.render = traced_render | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch_template(): | ||||||
|  |     render = getattr(Template, RENDER_ATTR, None) | ||||||
|  |     if render is None: | ||||||
|  |         log.debug('nothing to do Template is already patched') | ||||||
|  |         return | ||||||
|  |     Template.render = render | ||||||
|  |     delattr(Template, RENDER_ATTR) | ||||||
|  | @ -0,0 +1,75 @@ | ||||||
|  | from ...compat import parse | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _resource_from_cache_prefix(resource, cache): | ||||||
|  |     """ | ||||||
|  |     Combine the resource name with the cache prefix (if any) | ||||||
|  |     """ | ||||||
|  |     if getattr(cache, 'key_prefix', None): | ||||||
|  |         name = '{} {}'.format(resource, cache.key_prefix) | ||||||
|  |     else: | ||||||
|  |         name = resource | ||||||
|  | 
 | ||||||
|  |     # enforce lowercase to make the output nicer to read | ||||||
|  |     return name.lower() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def quantize_key_values(key): | ||||||
|  |     """ | ||||||
|  |     Used in the Django trace operation method, it ensures that if a dict | ||||||
|  |     with values is used, we removes the values from the span meta | ||||||
|  |     attributes. For example:: | ||||||
|  | 
 | ||||||
|  |         >>> quantize_key_values({'key', 'value'}) | ||||||
|  |         # returns ['key'] | ||||||
|  |     """ | ||||||
|  |     if isinstance(key, dict): | ||||||
|  |         return key.keys() | ||||||
|  | 
 | ||||||
|  |     return key | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_request_uri(request): | ||||||
|  |     """ | ||||||
|  |     Helper to rebuild the original request url | ||||||
|  | 
 | ||||||
|  |     query string or fragments are not included. | ||||||
|  |     """ | ||||||
|  |     # DEV: We do this instead of `request.build_absolute_uri()` since | ||||||
|  |     #      an exception can get raised, we want to always build a url | ||||||
|  |     #      regardless of any exceptions raised from `request.get_host()` | ||||||
|  |     host = None | ||||||
|  |     try: | ||||||
|  |         host = request.get_host()  # this will include host:port | ||||||
|  |     except Exception: | ||||||
|  |         log.debug('Failed to get Django request host', exc_info=True) | ||||||
|  | 
 | ||||||
|  |     if not host: | ||||||
|  |         try: | ||||||
|  |             # Try to build host how Django would have | ||||||
|  |             # https://github.com/django/django/blob/e8d0d2a5efc8012dcc8bf1809dec065ebde64c81/django/http/request.py#L85-L102 | ||||||
|  |             if 'HTTP_HOST' in request.META: | ||||||
|  |                 host = request.META['HTTP_HOST'] | ||||||
|  |             else: | ||||||
|  |                 host = request.META['SERVER_NAME'] | ||||||
|  |                 port = str(request.META['SERVER_PORT']) | ||||||
|  |                 if port != ('443' if request.is_secure() else '80'): | ||||||
|  |                     host = '{0}:{1}'.format(host, port) | ||||||
|  |         except Exception: | ||||||
|  |             # This really shouldn't ever happen, but lets guard here just in case | ||||||
|  |             log.debug('Failed to build Django request host', exc_info=True) | ||||||
|  |             host = 'unknown' | ||||||
|  | 
 | ||||||
|  |     # Build request url from the information available | ||||||
|  |     # DEV: We are explicitly omitting query strings since they may contain sensitive information | ||||||
|  |     return parse.urlunparse(parse.ParseResult( | ||||||
|  |         scheme=request.scheme, | ||||||
|  |         netloc=host, | ||||||
|  |         path=request.path, | ||||||
|  |         params='', | ||||||
|  |         query='', | ||||||
|  |         fragment='', | ||||||
|  |     )) | ||||||
|  | @ -0,0 +1,48 @@ | ||||||
|  | """ | ||||||
|  | Instrument dogpile.cache__ to report all cached lookups. | ||||||
|  | 
 | ||||||
|  | This will add spans around the calls to your cache backend (eg. redis, memory, | ||||||
|  | etc). The spans will also include the following tags: | ||||||
|  | 
 | ||||||
|  | - key/keys: The key(s) dogpile passed to your backend. Note that this will be | ||||||
|  |   the output of the region's ``function_key_generator``, but before any key | ||||||
|  |   mangling is applied (ie. the region's ``key_mangler``). | ||||||
|  | - region: Name of the region. | ||||||
|  | - backend: Name of the backend class. | ||||||
|  | - hit: If the key was found in the cache. | ||||||
|  | - expired: If the key is expired. This is only relevant if the key was found. | ||||||
|  | 
 | ||||||
|  | While cache tracing will generally already have keys in tags, some caching | ||||||
|  | setups will not have useful tag values - such as when you're using consistent | ||||||
|  | hashing with memcached - the key(s) will appear as a mangled hash. | ||||||
|  | :: | ||||||
|  | 
 | ||||||
|  |     # Patch before importing dogpile.cache | ||||||
|  |     from ddtrace import patch | ||||||
|  |     patch(dogpile_cache=True) | ||||||
|  | 
 | ||||||
|  |     from dogpile.cache import make_region | ||||||
|  | 
 | ||||||
|  |     region = make_region().configure( | ||||||
|  |         "dogpile.cache.pylibmc", | ||||||
|  |         expiration_time=3600, | ||||||
|  |         arguments={"url": ["127.0.0.1"]}, | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     @region.cache_on_arguments() | ||||||
|  |     def hello(name): | ||||||
|  |         # Some complicated, slow calculation | ||||||
|  |         return "Hello, {}".format(name) | ||||||
|  | 
 | ||||||
|  | .. __: https://dogpilecache.sqlalchemy.org/ | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['dogpile.cache'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch, unpatch | ||||||
|  | 
 | ||||||
|  |         __all__ = ['patch', 'unpatch'] | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | import dogpile | ||||||
|  | 
 | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...utils.formats import asbool | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _wrap_lock_ctor(func, instance, args, kwargs): | ||||||
|  |     """ | ||||||
|  |     This seems rather odd. But to track hits, we need to patch the wrapped function that | ||||||
|  |     dogpile passes to the region and locks. Unfortunately it's a closure defined inside | ||||||
|  |     the get_or_create* methods themselves, so we can't easily patch those. | ||||||
|  |     """ | ||||||
|  |     func(*args, **kwargs) | ||||||
|  |     ori_backend_fetcher = instance.value_and_created_fn | ||||||
|  | 
 | ||||||
|  |     def wrapped_backend_fetcher(): | ||||||
|  |         pin = Pin.get_from(dogpile.cache) | ||||||
|  |         if not pin or not pin.enabled(): | ||||||
|  |             return ori_backend_fetcher() | ||||||
|  | 
 | ||||||
|  |         hit = False | ||||||
|  |         expired = True | ||||||
|  |         try: | ||||||
|  |             value, createdtime = ori_backend_fetcher() | ||||||
|  |             hit = value is not dogpile.cache.api.NO_VALUE | ||||||
|  |             # dogpile sometimes returns None, but only checks for truthiness. Coalesce | ||||||
|  |             # to minimize APM users' confusion. | ||||||
|  |             expired = instance._is_expired(createdtime) or False | ||||||
|  |             return value, createdtime | ||||||
|  |         finally: | ||||||
|  |             # Keys are checked in random order so the 'final' answer for partial hits | ||||||
|  |             # should really be false (ie. if any are 'negative', then the tag value | ||||||
|  |             # should be). This means ANDing all hit values and ORing all expired values. | ||||||
|  |             span = pin.tracer.current_span() | ||||||
|  |             span.set_tag('hit', asbool(span.get_tag('hit') or 'True') and hit) | ||||||
|  |             span.set_tag('expired', asbool(span.get_tag('expired') or 'False') or expired) | ||||||
|  |     instance.value_and_created_fn = wrapped_backend_fetcher | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | import dogpile | ||||||
|  | 
 | ||||||
|  | from ddtrace.pin import Pin, _DD_PIN_NAME, _DD_PIN_PROXY_NAME | ||||||
|  | from ddtrace.vendor.wrapt import wrap_function_wrapper as _w | ||||||
|  | 
 | ||||||
|  | from .lock import _wrap_lock_ctor | ||||||
|  | from .region import _wrap_get_create, _wrap_get_create_multi | ||||||
|  | 
 | ||||||
|  | _get_or_create = dogpile.cache.region.CacheRegion.get_or_create | ||||||
|  | _get_or_create_multi = dogpile.cache.region.CacheRegion.get_or_create_multi | ||||||
|  | _lock_ctor = dogpile.lock.Lock.__init__ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     if getattr(dogpile.cache, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(dogpile.cache, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     _w('dogpile.cache.region', 'CacheRegion.get_or_create', _wrap_get_create) | ||||||
|  |     _w('dogpile.cache.region', 'CacheRegion.get_or_create_multi', _wrap_get_create_multi) | ||||||
|  |     _w('dogpile.lock', 'Lock.__init__', _wrap_lock_ctor) | ||||||
|  | 
 | ||||||
|  |     Pin(app='dogpile.cache', service='dogpile.cache').onto(dogpile.cache) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     if not getattr(dogpile.cache, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(dogpile.cache, '_datadog_patch', False) | ||||||
|  |     # This looks silly but the unwrap util doesn't support class instance methods, even | ||||||
|  |     # though wrapt does. This was causing the patches to stack on top of each other | ||||||
|  |     # during testing. | ||||||
|  |     dogpile.cache.region.CacheRegion.get_or_create = _get_or_create | ||||||
|  |     dogpile.cache.region.CacheRegion.get_or_create_multi = _get_or_create_multi | ||||||
|  |     dogpile.lock.Lock.__init__ = _lock_ctor | ||||||
|  |     setattr(dogpile.cache, _DD_PIN_NAME, None) | ||||||
|  |     setattr(dogpile.cache, _DD_PIN_PROXY_NAME, None) | ||||||
|  | @ -0,0 +1,29 @@ | ||||||
|  | import dogpile | ||||||
|  | 
 | ||||||
|  | from ...pin import Pin | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _wrap_get_create(func, instance, args, kwargs): | ||||||
|  |     pin = Pin.get_from(dogpile.cache) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     key = args[0] | ||||||
|  |     with pin.tracer.trace('dogpile.cache', resource='get_or_create', span_type='cache') as span: | ||||||
|  |         span.set_tag('key', key) | ||||||
|  |         span.set_tag('region', instance.name) | ||||||
|  |         span.set_tag('backend', instance.actual_backend.__class__.__name__) | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _wrap_get_create_multi(func, instance, args, kwargs): | ||||||
|  |     pin = Pin.get_from(dogpile.cache) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     keys = args[0] | ||||||
|  |     with pin.tracer.trace('dogpile.cache', resource='get_or_create_multi', span_type='cache') as span: | ||||||
|  |         span.set_tag('keys', keys) | ||||||
|  |         span.set_tag('region', instance.name) | ||||||
|  |         span.set_tag('backend', instance.actual_backend.__class__.__name__) | ||||||
|  |         return func(*args, **kwargs) | ||||||
|  | @ -0,0 +1,33 @@ | ||||||
|  | """Instrument Elasticsearch to report Elasticsearch queries. | ||||||
|  | 
 | ||||||
|  | ``patch_all`` will automatically patch your Elasticsearch instance to make it work. | ||||||
|  | :: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import Pin, patch | ||||||
|  |     from elasticsearch import Elasticsearch | ||||||
|  | 
 | ||||||
|  |     # If not patched yet, you can patch elasticsearch specifically | ||||||
|  |     patch(elasticsearch=True) | ||||||
|  | 
 | ||||||
|  |     # This will report spans with the default instrumentation | ||||||
|  |     es = Elasticsearch(port=ELASTICSEARCH_CONFIG['port']) | ||||||
|  |     # Example of instrumented query | ||||||
|  |     es.indices.create(index='books', ignore=400) | ||||||
|  | 
 | ||||||
|  |     # Use a pin to specify metadata related to this client | ||||||
|  |     es = Elasticsearch(port=ELASTICSEARCH_CONFIG['port']) | ||||||
|  |     Pin.override(es.transport, service='elasticsearch-videos') | ||||||
|  |     es.indices.create(index='videos', ignore=400) | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | # DEV: We only require one of these modules to be available | ||||||
|  | required_modules = ['elasticsearch', 'elasticsearch1', 'elasticsearch2', 'elasticsearch5', 'elasticsearch6'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     # We were able to find at least one of the required modules | ||||||
|  |     if set(missing_modules) != set(required_modules): | ||||||
|  |         from .transport import get_traced_transport | ||||||
|  |         from .patch import patch | ||||||
|  | 
 | ||||||
|  |         __all__ = ['get_traced_transport', 'patch'] | ||||||
|  | @ -0,0 +1,14 @@ | ||||||
|  | from importlib import import_module | ||||||
|  | 
 | ||||||
|  | module_names = ('elasticsearch', 'elasticsearch1', 'elasticsearch2', 'elasticsearch5', 'elasticsearch6') | ||||||
|  | for module_name in module_names: | ||||||
|  |     try: | ||||||
|  |         elasticsearch = import_module(module_name) | ||||||
|  |         break | ||||||
|  |     except ImportError: | ||||||
|  |         pass | ||||||
|  | else: | ||||||
|  |     raise ImportError('could not import any of {0!r}'.format(module_names)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | __all__ = ['elasticsearch'] | ||||||
|  | @ -0,0 +1,118 @@ | ||||||
|  | from importlib import import_module | ||||||
|  | 
 | ||||||
|  | from ddtrace.vendor.wrapt import wrap_function_wrapper as _w | ||||||
|  | 
 | ||||||
|  | from .quantize import quantize | ||||||
|  | 
 | ||||||
|  | from ...compat import urlencode | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import SpanTypes, elasticsearch as metadata, http | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...utils.wrappers import unwrap as _u | ||||||
|  | from ...settings import config | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _es_modules(): | ||||||
|  |     module_names = ('elasticsearch', 'elasticsearch1', 'elasticsearch2', 'elasticsearch5', 'elasticsearch6') | ||||||
|  |     for module_name in module_names: | ||||||
|  |         try: | ||||||
|  |             yield import_module(module_name) | ||||||
|  |         except ImportError: | ||||||
|  |             pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # NB: We are patching the default elasticsearch.transport module | ||||||
|  | def patch(): | ||||||
|  |     for elasticsearch in _es_modules(): | ||||||
|  |         _patch(elasticsearch) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _patch(elasticsearch): | ||||||
|  |     if getattr(elasticsearch, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(elasticsearch, '_datadog_patch', True) | ||||||
|  |     _w(elasticsearch.transport, 'Transport.perform_request', _get_perform_request(elasticsearch)) | ||||||
|  |     Pin(service=metadata.SERVICE, app=metadata.APP).onto(elasticsearch.transport.Transport) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     for elasticsearch in _es_modules(): | ||||||
|  |         _unpatch(elasticsearch) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _unpatch(elasticsearch): | ||||||
|  |     if getattr(elasticsearch, '_datadog_patch', False): | ||||||
|  |         setattr(elasticsearch, '_datadog_patch', False) | ||||||
|  |         _u(elasticsearch.transport.Transport, 'perform_request') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _get_perform_request(elasticsearch): | ||||||
|  |     def _perform_request(func, instance, args, kwargs): | ||||||
|  |         pin = Pin.get_from(instance) | ||||||
|  |         if not pin or not pin.enabled(): | ||||||
|  |             return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         with pin.tracer.trace('elasticsearch.query', span_type=SpanTypes.ELASTICSEARCH) as span: | ||||||
|  |             # Don't instrument if the trace is not sampled | ||||||
|  |             if not span.sampled: | ||||||
|  |                 return func(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |             method, url = args | ||||||
|  |             params = kwargs.get('params') | ||||||
|  |             body = kwargs.get('body') | ||||||
|  | 
 | ||||||
|  |             span.service = pin.service | ||||||
|  |             span.set_tag(metadata.METHOD, method) | ||||||
|  |             span.set_tag(metadata.URL, url) | ||||||
|  |             span.set_tag(metadata.PARAMS, urlencode(params)) | ||||||
|  |             if config.elasticsearch.trace_query_string: | ||||||
|  |                 span.set_tag(http.QUERY_STRING, urlencode(params)) | ||||||
|  |             if method == 'GET': | ||||||
|  |                 span.set_tag(metadata.BODY, instance.serializer.dumps(body)) | ||||||
|  |             status = None | ||||||
|  | 
 | ||||||
|  |             # set analytics sample rate | ||||||
|  |             span.set_tag( | ||||||
|  |                 ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |                 config.elasticsearch.get_analytics_sample_rate() | ||||||
|  |             ) | ||||||
|  | 
 | ||||||
|  |             span = quantize(span) | ||||||
|  | 
 | ||||||
|  |             try: | ||||||
|  |                 result = func(*args, **kwargs) | ||||||
|  |             except elasticsearch.exceptions.TransportError as e: | ||||||
|  |                 span.set_tag(http.STATUS_CODE, getattr(e, 'status_code', 500)) | ||||||
|  |                 raise | ||||||
|  | 
 | ||||||
|  |             try: | ||||||
|  |                 # Optional metadata extraction with soft fail. | ||||||
|  |                 if isinstance(result, tuple) and len(result) == 2: | ||||||
|  |                     # elasticsearch<2.4; it returns both the status and the body | ||||||
|  |                     status, data = result | ||||||
|  |                 else: | ||||||
|  |                     # elasticsearch>=2.4; internal change for ``Transport.perform_request`` | ||||||
|  |                     # that just returns the body | ||||||
|  |                     data = result | ||||||
|  | 
 | ||||||
|  |                 took = data.get('took') | ||||||
|  |                 if took: | ||||||
|  |                     span.set_metric(metadata.TOOK, int(took)) | ||||||
|  |             except Exception: | ||||||
|  |                 pass | ||||||
|  | 
 | ||||||
|  |             if status: | ||||||
|  |                 span.set_tag(http.STATUS_CODE, status) | ||||||
|  | 
 | ||||||
|  |             return result | ||||||
|  |     return _perform_request | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Backwards compatibility for anyone who decided to import `ddtrace.contrib.elasticsearch.patch._perform_request` | ||||||
|  | # DEV: `_perform_request` is a `wrapt.FunctionWrapper` | ||||||
|  | try: | ||||||
|  |     # DEV: Import as `es` to not shadow loop variables above | ||||||
|  |     import elasticsearch as es | ||||||
|  |     _perform_request = _get_perform_request(es) | ||||||
|  | except ImportError: | ||||||
|  |     pass | ||||||
|  | @ -0,0 +1,37 @@ | ||||||
|  | import re | ||||||
|  | 
 | ||||||
|  | from ...ext import elasticsearch as metadata | ||||||
|  | 
 | ||||||
|  | # Replace any ID | ||||||
|  | ID_REGEXP = re.compile(r'/([0-9]+)([/\?]|$)') | ||||||
|  | ID_PLACEHOLDER = r'/?\2' | ||||||
|  | 
 | ||||||
|  | # Remove digits from potential timestamped indexes (should be an option). | ||||||
|  | # For now, let's say 2+ digits | ||||||
|  | INDEX_REGEXP = re.compile(r'[0-9]{2,}') | ||||||
|  | INDEX_PLACEHOLDER = r'?' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def quantize(span): | ||||||
|  |     """Quantize an elasticsearch span | ||||||
|  | 
 | ||||||
|  |     We want to extract a meaningful `resource` from the request. | ||||||
|  |     We do it based on the method + url, with some cleanup applied to the URL. | ||||||
|  | 
 | ||||||
|  |     The URL might a ID, but also it is common to have timestamped indexes. | ||||||
|  |     While the first is easy to catch, the second should probably be configurable. | ||||||
|  | 
 | ||||||
|  |     All of this should probably be done in the Agent. Later. | ||||||
|  |     """ | ||||||
|  |     url = span.get_tag(metadata.URL) | ||||||
|  |     method = span.get_tag(metadata.METHOD) | ||||||
|  | 
 | ||||||
|  |     quantized_url = ID_REGEXP.sub(ID_PLACEHOLDER, url) | ||||||
|  |     quantized_url = INDEX_REGEXP.sub(INDEX_PLACEHOLDER, quantized_url) | ||||||
|  | 
 | ||||||
|  |     span.resource = '{method} {url}'.format( | ||||||
|  |         method=method, | ||||||
|  |         url=quantized_url | ||||||
|  |     ) | ||||||
|  | 
 | ||||||
|  |     return span | ||||||
|  | @ -0,0 +1,66 @@ | ||||||
|  | # DEV: This will import the first available module from: | ||||||
|  | #   `elasticsearch`, `elasticsearch1`, `elasticsearch2`, `elasticsearch5`, 'elasticsearch6' | ||||||
|  | from .elasticsearch import elasticsearch | ||||||
|  | 
 | ||||||
|  | from .quantize import quantize | ||||||
|  | 
 | ||||||
|  | from ...utils.deprecation import deprecated | ||||||
|  | from ...compat import urlencode | ||||||
|  | from ...ext import SpanTypes, http, elasticsearch as metadata | ||||||
|  | from ...settings import config | ||||||
|  | 
 | ||||||
|  | DEFAULT_SERVICE = 'elasticsearch' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @deprecated(message='Use patching instead (see the docs).', version='1.0.0') | ||||||
|  | def get_traced_transport(datadog_tracer, datadog_service=DEFAULT_SERVICE): | ||||||
|  | 
 | ||||||
|  |     class TracedTransport(elasticsearch.Transport): | ||||||
|  |         """ Extend elasticseach transport layer to allow Datadog | ||||||
|  |             tracer to catch any performed request. | ||||||
|  |         """ | ||||||
|  | 
 | ||||||
|  |         _datadog_tracer = datadog_tracer | ||||||
|  |         _datadog_service = datadog_service | ||||||
|  | 
 | ||||||
|  |         def perform_request(self, method, url, params=None, body=None): | ||||||
|  |             with self._datadog_tracer.trace('elasticsearch.query', span_type=SpanTypes.ELASTICSEARCH) as s: | ||||||
|  |                 # Don't instrument if the trace is not sampled | ||||||
|  |                 if not s.sampled: | ||||||
|  |                     return super(TracedTransport, self).perform_request( | ||||||
|  |                         method, url, params=params, body=body) | ||||||
|  | 
 | ||||||
|  |                 s.service = self._datadog_service | ||||||
|  |                 s.set_tag(metadata.METHOD, method) | ||||||
|  |                 s.set_tag(metadata.URL, url) | ||||||
|  |                 s.set_tag(metadata.PARAMS, urlencode(params)) | ||||||
|  |                 if config.elasticsearch.trace_query_string: | ||||||
|  |                     s.set_tag(http.QUERY_STRING, urlencode(params)) | ||||||
|  |                 if method == 'GET': | ||||||
|  |                     s.set_tag(metadata.BODY, self.serializer.dumps(body)) | ||||||
|  |                 s = quantize(s) | ||||||
|  | 
 | ||||||
|  |                 try: | ||||||
|  |                     result = super(TracedTransport, self).perform_request(method, url, params=params, body=body) | ||||||
|  |                 except elasticsearch.exceptions.TransportError as e: | ||||||
|  |                     s.set_tag(http.STATUS_CODE, e.status_code) | ||||||
|  |                     raise | ||||||
|  | 
 | ||||||
|  |                 status = None | ||||||
|  |                 if isinstance(result, tuple) and len(result) == 2: | ||||||
|  |                     # elasticsearch<2.4; it returns both the status and the body | ||||||
|  |                     status, data = result | ||||||
|  |                 else: | ||||||
|  |                     # elasticsearch>=2.4; internal change for ``Transport.perform_request`` | ||||||
|  |                     # that just returns the body | ||||||
|  |                     data = result | ||||||
|  | 
 | ||||||
|  |                 if status: | ||||||
|  |                     s.set_tag(http.STATUS_CODE, status) | ||||||
|  | 
 | ||||||
|  |                 took = data.get('took') | ||||||
|  |                 if took: | ||||||
|  |                     s.set_metric(metadata.TOOK, int(took)) | ||||||
|  | 
 | ||||||
|  |                 return result | ||||||
|  |     return TracedTransport | ||||||
|  | @ -0,0 +1,59 @@ | ||||||
|  | """ | ||||||
|  | To trace the falcon web framework, install the trace middleware:: | ||||||
|  | 
 | ||||||
|  |     import falcon | ||||||
|  |     from ddtrace import tracer | ||||||
|  |     from ddtrace.contrib.falcon import TraceMiddleware | ||||||
|  | 
 | ||||||
|  |     mw = TraceMiddleware(tracer, 'my-falcon-app') | ||||||
|  |     falcon.API(middleware=[mw]) | ||||||
|  | 
 | ||||||
|  | You can also use the autopatching functionality:: | ||||||
|  | 
 | ||||||
|  |     import falcon | ||||||
|  |     from ddtrace import tracer, patch | ||||||
|  | 
 | ||||||
|  |     patch(falcon=True) | ||||||
|  | 
 | ||||||
|  |     app = falcon.API() | ||||||
|  | 
 | ||||||
|  | To disable distributed tracing when using autopatching, set the | ||||||
|  | ``DATADOG_FALCON_DISTRIBUTED_TRACING`` environment variable to ``False``. | ||||||
|  | 
 | ||||||
|  | To enable generating APM events for Trace Search & Analytics, set the | ||||||
|  | ``DD_FALCON_ANALYTICS_ENABLED`` environment variable to ``True``. | ||||||
|  | 
 | ||||||
|  | **Supported span hooks** | ||||||
|  | 
 | ||||||
|  | The following is a list of available tracer hooks that can be used to intercept | ||||||
|  | and modify spans created by this integration. | ||||||
|  | 
 | ||||||
|  | - ``request`` | ||||||
|  |     - Called before the response has been finished | ||||||
|  |     - ``def on_falcon_request(span, request, response)`` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Example:: | ||||||
|  | 
 | ||||||
|  |     import falcon | ||||||
|  |     from ddtrace import config, patch_all | ||||||
|  |     patch_all() | ||||||
|  | 
 | ||||||
|  |     app = falcon.API() | ||||||
|  | 
 | ||||||
|  |     @config.falcon.hooks.on('request') | ||||||
|  |     def on_falcon_request(span, request, response): | ||||||
|  |         span.set_tag('my.custom', 'tag') | ||||||
|  | 
 | ||||||
|  | :ref:`Headers tracing <http-headers-tracing>` is supported for this integration. | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | required_modules = ['falcon'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .middleware import TraceMiddleware | ||||||
|  |         from .patch import patch | ||||||
|  | 
 | ||||||
|  |         __all__ = ['TraceMiddleware', 'patch'] | ||||||
|  | @ -0,0 +1,116 @@ | ||||||
|  | import sys | ||||||
|  | 
 | ||||||
|  | from ddtrace.ext import SpanTypes, http as httpx | ||||||
|  | from ddtrace.http import store_request_headers, store_response_headers | ||||||
|  | from ddtrace.propagation.http import HTTPPropagator | ||||||
|  | 
 | ||||||
|  | from ...compat import iteritems | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...settings import config | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TraceMiddleware(object): | ||||||
|  | 
 | ||||||
|  |     def __init__(self, tracer, service='falcon', distributed_tracing=True): | ||||||
|  |         # store tracing references | ||||||
|  |         self.tracer = tracer | ||||||
|  |         self.service = service | ||||||
|  |         self._distributed_tracing = distributed_tracing | ||||||
|  | 
 | ||||||
|  |     def process_request(self, req, resp): | ||||||
|  |         if self._distributed_tracing: | ||||||
|  |             # Falcon uppercases all header names. | ||||||
|  |             headers = dict((k.lower(), v) for k, v in iteritems(req.headers)) | ||||||
|  |             propagator = HTTPPropagator() | ||||||
|  |             context = propagator.extract(headers) | ||||||
|  |             # Only activate the new context if there was a trace id extracted | ||||||
|  |             if context.trace_id: | ||||||
|  |                 self.tracer.context_provider.activate(context) | ||||||
|  | 
 | ||||||
|  |         span = self.tracer.trace( | ||||||
|  |             'falcon.request', | ||||||
|  |             service=self.service, | ||||||
|  |             span_type=SpanTypes.WEB, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # set analytics sample rate with global config enabled | ||||||
|  |         span.set_tag( | ||||||
|  |             ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |             config.falcon.get_analytics_sample_rate(use_global_config=True) | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         span.set_tag(httpx.METHOD, req.method) | ||||||
|  |         span.set_tag(httpx.URL, req.url) | ||||||
|  |         if config.falcon.trace_query_string: | ||||||
|  |             span.set_tag(httpx.QUERY_STRING, req.query_string) | ||||||
|  | 
 | ||||||
|  |         # Note: any request header set after this line will not be stored in the span | ||||||
|  |         store_request_headers(req.headers, span, config.falcon) | ||||||
|  | 
 | ||||||
|  |     def process_resource(self, req, resp, resource, params): | ||||||
|  |         span = self.tracer.current_span() | ||||||
|  |         if not span: | ||||||
|  |             return  # unexpected | ||||||
|  |         span.resource = '%s %s' % (req.method, _name(resource)) | ||||||
|  | 
 | ||||||
|  |     def process_response(self, req, resp, resource, req_succeeded=None): | ||||||
|  |         # req_succeded is not a kwarg in the API, but we need that to support | ||||||
|  |         # Falcon 1.0 that doesn't provide this argument | ||||||
|  |         span = self.tracer.current_span() | ||||||
|  |         if not span: | ||||||
|  |             return  # unexpected | ||||||
|  | 
 | ||||||
|  |         status = httpx.normalize_status_code(resp.status) | ||||||
|  | 
 | ||||||
|  |         # Note: any response header set after this line will not be stored in the span | ||||||
|  |         store_response_headers(resp._headers, span, config.falcon) | ||||||
|  | 
 | ||||||
|  |         # FIXME[matt] falcon does not map errors or unmatched routes | ||||||
|  |         # to proper status codes, so we we have to try to infer them | ||||||
|  |         # here. See https://github.com/falconry/falcon/issues/606 | ||||||
|  |         if resource is None: | ||||||
|  |             status = '404' | ||||||
|  |             span.resource = '%s 404' % req.method | ||||||
|  |             span.set_tag(httpx.STATUS_CODE, status) | ||||||
|  |             span.finish() | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         err_type = sys.exc_info()[0] | ||||||
|  |         if err_type is not None: | ||||||
|  |             if req_succeeded is None: | ||||||
|  |                 # backward-compatibility with Falcon 1.0; any version | ||||||
|  |                 # greater than 1.0 has req_succeded in [True, False] | ||||||
|  |                 # TODO[manu]: drop the support at some point | ||||||
|  |                 status = _detect_and_set_status_error(err_type, span) | ||||||
|  |             elif req_succeeded is False: | ||||||
|  |                 # Falcon 1.1+ provides that argument that is set to False | ||||||
|  |                 # if get an Exception (404 is still an exception) | ||||||
|  |                 status = _detect_and_set_status_error(err_type, span) | ||||||
|  | 
 | ||||||
|  |         span.set_tag(httpx.STATUS_CODE, status) | ||||||
|  | 
 | ||||||
|  |         # Emit span hook for this response | ||||||
|  |         # DEV: Emit before closing so they can overwrite `span.resource` if they want | ||||||
|  |         config.falcon.hooks._emit('request', span, req, resp) | ||||||
|  | 
 | ||||||
|  |         # Close the span | ||||||
|  |         span.finish() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _is_404(err_type): | ||||||
|  |     return 'HTTPNotFound' in err_type.__name__ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _detect_and_set_status_error(err_type, span): | ||||||
|  |     """Detect the HTTP status code from the current stacktrace and | ||||||
|  |     set the traceback to the given Span | ||||||
|  |     """ | ||||||
|  |     if not _is_404(err_type): | ||||||
|  |         span.set_traceback() | ||||||
|  |         return '500' | ||||||
|  |     elif _is_404(err_type): | ||||||
|  |         return '404' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _name(r): | ||||||
|  |     return '%s.%s' % (r.__module__, r.__class__.__name__) | ||||||
|  | @ -0,0 +1,31 @@ | ||||||
|  | import os | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | import falcon | ||||||
|  | 
 | ||||||
|  | from ddtrace import tracer | ||||||
|  | 
 | ||||||
|  | from .middleware import TraceMiddleware | ||||||
|  | from ...utils.formats import asbool, get_env | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """ | ||||||
|  |     Patch falcon.API to include contrib.falcon.TraceMiddleware | ||||||
|  |     by default | ||||||
|  |     """ | ||||||
|  |     if getattr(falcon, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     setattr(falcon, '_datadog_patch', True) | ||||||
|  |     wrapt.wrap_function_wrapper('falcon', 'API.__init__', traced_init) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_init(wrapped, instance, args, kwargs): | ||||||
|  |     mw = kwargs.pop('middleware', []) | ||||||
|  |     service = os.environ.get('DATADOG_SERVICE_NAME') or 'falcon' | ||||||
|  |     distributed_tracing = asbool(get_env('falcon', 'distributed_tracing', True)) | ||||||
|  | 
 | ||||||
|  |     mw.insert(0, TraceMiddleware(tracer, service, distributed_tracing)) | ||||||
|  |     kwargs['middleware'] = mw | ||||||
|  | 
 | ||||||
|  |     wrapped(*args, **kwargs) | ||||||
|  | @ -0,0 +1,113 @@ | ||||||
|  | """ | ||||||
|  | The Flask__ integration will add tracing to all requests to your Flask application. | ||||||
|  | 
 | ||||||
|  | This integration will track the entire Flask lifecycle including user-defined endpoints, hooks, | ||||||
|  | signals, and templating rendering. | ||||||
|  | 
 | ||||||
|  | To configure tracing manually:: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import patch_all | ||||||
|  |     patch_all() | ||||||
|  | 
 | ||||||
|  |     from flask import Flask | ||||||
|  | 
 | ||||||
|  |     app = Flask(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     @app.route('/') | ||||||
|  |     def index(): | ||||||
|  |         return 'hello world' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  |     if __name__ == '__main__': | ||||||
|  |         app.run() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | You may also enable Flask tracing automatically via ddtrace-run:: | ||||||
|  | 
 | ||||||
|  |     ddtrace-run python app.py | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Configuration | ||||||
|  | ~~~~~~~~~~~~~ | ||||||
|  | 
 | ||||||
|  | .. py:data:: ddtrace.config.flask['distributed_tracing_enabled'] | ||||||
|  | 
 | ||||||
|  |    Whether to parse distributed tracing headers from requests received by your Flask app. | ||||||
|  | 
 | ||||||
|  |    Default: ``True`` | ||||||
|  | 
 | ||||||
|  | .. py:data:: ddtrace.config.flask['analytics_enabled'] | ||||||
|  | 
 | ||||||
|  |    Whether to generate APM events for Flask in Trace Search & Analytics. | ||||||
|  | 
 | ||||||
|  |    Can also be enabled with the ``DD_FLASK_ANALYTICS_ENABLED`` environment variable. | ||||||
|  | 
 | ||||||
|  |    Default: ``None`` | ||||||
|  | 
 | ||||||
|  | .. py:data:: ddtrace.config.flask['service_name'] | ||||||
|  | 
 | ||||||
|  |    The service name reported for your Flask app. | ||||||
|  | 
 | ||||||
|  |    Can also be configured via the ``DATADOG_SERVICE_NAME`` environment variable. | ||||||
|  | 
 | ||||||
|  |    Default: ``'flask'`` | ||||||
|  | 
 | ||||||
|  | .. py:data:: ddtrace.config.flask['collect_view_args'] | ||||||
|  | 
 | ||||||
|  |    Whether to add request tags for view function argument values. | ||||||
|  | 
 | ||||||
|  |    Default: ``True`` | ||||||
|  | 
 | ||||||
|  | .. py:data:: ddtrace.config.flask['template_default_name'] | ||||||
|  | 
 | ||||||
|  |    The default template name to use when one does not exist. | ||||||
|  | 
 | ||||||
|  |    Default: ``<memory>`` | ||||||
|  | 
 | ||||||
|  | .. py:data:: ddtrace.config.flask['trace_signals'] | ||||||
|  | 
 | ||||||
|  |    Whether to trace Flask signals (``before_request``, ``after_request``, etc). | ||||||
|  | 
 | ||||||
|  |    Default: ``True`` | ||||||
|  | 
 | ||||||
|  | .. py:data:: ddtrace.config.flask['extra_error_codes'] | ||||||
|  | 
 | ||||||
|  |    A list of response codes that should get marked as errors. | ||||||
|  | 
 | ||||||
|  |    *5xx codes are always considered an error.* | ||||||
|  | 
 | ||||||
|  |    Default: ``[]`` | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | Example:: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import config | ||||||
|  | 
 | ||||||
|  |     # Enable distributed tracing | ||||||
|  |     config.flask['distributed_tracing_enabled'] = True | ||||||
|  | 
 | ||||||
|  |     # Override service name | ||||||
|  |     config.flask['service_name'] = 'custom-service-name' | ||||||
|  | 
 | ||||||
|  |     # Report 401, and 403 responses as errors | ||||||
|  |     config.flask['extra_error_codes'] = [401, 403] | ||||||
|  | 
 | ||||||
|  | .. __: http://flask.pocoo.org/ | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['flask'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         # DEV: We do this so we can `@mock.patch('ddtrace.contrib.flask._patch.<func>')` in tests | ||||||
|  |         from . import patch as _patch | ||||||
|  |         from .middleware import TraceMiddleware | ||||||
|  | 
 | ||||||
|  |         patch = _patch.patch | ||||||
|  |         unpatch = _patch.unpatch | ||||||
|  | 
 | ||||||
|  |         __all__ = ['TraceMiddleware', 'patch', 'unpatch'] | ||||||
|  | @ -0,0 +1,44 @@ | ||||||
|  | from ddtrace import Pin | ||||||
|  | import flask | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_current_app(): | ||||||
|  |     """Helper to get the flask.app.Flask from the current app context""" | ||||||
|  |     appctx = flask._app_ctx_stack.top | ||||||
|  |     if appctx: | ||||||
|  |         return appctx.app | ||||||
|  |     return None | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def with_instance_pin(func): | ||||||
|  |     """Helper to wrap a function wrapper and ensure an enabled pin is available for the `instance`""" | ||||||
|  |     def wrapper(wrapped, instance, args, kwargs): | ||||||
|  |         pin = Pin._find(wrapped, instance, get_current_app()) | ||||||
|  |         if not pin or not pin.enabled(): | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         return func(pin, wrapped, instance, args, kwargs) | ||||||
|  |     return wrapper | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def simple_tracer(name, span_type=None): | ||||||
|  |     """Generate a simple tracer that wraps the function call with `with tracer.trace()`""" | ||||||
|  |     @with_instance_pin | ||||||
|  |     def wrapper(pin, wrapped, instance, args, kwargs): | ||||||
|  |         with pin.tracer.trace(name, service=pin.service, span_type=span_type): | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  |     return wrapper | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_current_span(pin, root=False): | ||||||
|  |     """Helper to get the current span from the provided pins current call context""" | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     ctx = pin.tracer.get_call_context() | ||||||
|  |     if not ctx: | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     if root: | ||||||
|  |         return ctx.get_current_root_span() | ||||||
|  |     return ctx.get_current_span() | ||||||
|  | @ -0,0 +1,208 @@ | ||||||
|  | from ... import compat | ||||||
|  | from ...ext import SpanTypes, http, errors | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | from ...propagation.http import HTTPPropagator | ||||||
|  | from ...utils.deprecation import deprecated | ||||||
|  | 
 | ||||||
|  | import flask.templating | ||||||
|  | from flask import g, request, signals | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | SPAN_NAME = 'flask.request' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TraceMiddleware(object): | ||||||
|  | 
 | ||||||
|  |     @deprecated(message='Use patching instead (see the docs).', version='1.0.0') | ||||||
|  |     def __init__(self, app, tracer, service='flask', use_signals=True, distributed_tracing=False): | ||||||
|  |         self.app = app | ||||||
|  |         log.debug('flask: initializing trace middleware') | ||||||
|  | 
 | ||||||
|  |         # Attach settings to the inner application middleware. This is required if double | ||||||
|  |         # instrumentation happens (i.e. `ddtrace-run` with `TraceMiddleware`). In that | ||||||
|  |         # case, `ddtrace-run` instruments the application, but then users code is unable | ||||||
|  |         # to update settings such as `distributed_tracing` flag. This step can be removed | ||||||
|  |         # when the `Config` object is used | ||||||
|  |         self.app._tracer = tracer | ||||||
|  |         self.app._service = service | ||||||
|  |         self.app._use_distributed_tracing = distributed_tracing | ||||||
|  |         self.use_signals = use_signals | ||||||
|  | 
 | ||||||
|  |         # safe-guard to avoid double instrumentation | ||||||
|  |         if getattr(app, '__dd_instrumentation', False): | ||||||
|  |             return | ||||||
|  |         setattr(app, '__dd_instrumentation', True) | ||||||
|  | 
 | ||||||
|  |         # Install hooks which time requests. | ||||||
|  |         self.app.before_request(self._before_request) | ||||||
|  |         self.app.after_request(self._after_request) | ||||||
|  |         self.app.teardown_request(self._teardown_request) | ||||||
|  | 
 | ||||||
|  |         # Add exception handling signals. This will annotate exceptions that | ||||||
|  |         # are caught and handled in custom user code. | ||||||
|  |         # See https://github.com/DataDog/dd-trace-py/issues/390 | ||||||
|  |         if use_signals and not signals.signals_available: | ||||||
|  |             log.debug(_blinker_not_installed_msg) | ||||||
|  |         self.use_signals = use_signals and signals.signals_available | ||||||
|  |         timing_signals = { | ||||||
|  |             'got_request_exception': self._request_exception, | ||||||
|  |         } | ||||||
|  |         self._receivers = [] | ||||||
|  |         if self.use_signals and _signals_exist(timing_signals): | ||||||
|  |             self._connect(timing_signals) | ||||||
|  | 
 | ||||||
|  |         _patch_render(tracer) | ||||||
|  | 
 | ||||||
|  |     def _connect(self, signal_to_handler): | ||||||
|  |         connected = True | ||||||
|  |         for name, handler in signal_to_handler.items(): | ||||||
|  |             s = getattr(signals, name, None) | ||||||
|  |             if not s: | ||||||
|  |                 connected = False | ||||||
|  |                 log.warning('trying to instrument missing signal %s', name) | ||||||
|  |                 continue | ||||||
|  |             # we should connect to the signal without using weak references | ||||||
|  |             # otherwise they will be garbage collected and our handlers | ||||||
|  |             # will be disconnected after the first call; for more details check: | ||||||
|  |             # https://github.com/jek/blinker/blob/207446f2d97/blinker/base.py#L106-L108 | ||||||
|  |             s.connect(handler, sender=self.app, weak=False) | ||||||
|  |             self._receivers.append(handler) | ||||||
|  |         return connected | ||||||
|  | 
 | ||||||
|  |     def _before_request(self): | ||||||
|  |         """ Starts tracing the current request and stores it in the global | ||||||
|  |             request object. | ||||||
|  |         """ | ||||||
|  |         self._start_span() | ||||||
|  | 
 | ||||||
|  |     def _after_request(self, response): | ||||||
|  |         """ Runs after the server can process a response. """ | ||||||
|  |         try: | ||||||
|  |             self._process_response(response) | ||||||
|  |         except Exception: | ||||||
|  |             log.debug('flask: error tracing response', exc_info=True) | ||||||
|  |         return response | ||||||
|  | 
 | ||||||
|  |     def _teardown_request(self, exception): | ||||||
|  |         """ Runs at the end of a request. If there's an unhandled exception, it | ||||||
|  |             will be passed in. | ||||||
|  |         """ | ||||||
|  |         # when we teardown the span, ensure we have a clean slate. | ||||||
|  |         span = getattr(g, 'flask_datadog_span', None) | ||||||
|  |         setattr(g, 'flask_datadog_span', None) | ||||||
|  |         if not span: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             self._finish_span(span, exception=exception) | ||||||
|  |         except Exception: | ||||||
|  |             log.debug('flask: error finishing span', exc_info=True) | ||||||
|  | 
 | ||||||
|  |     def _start_span(self): | ||||||
|  |         if self.app._use_distributed_tracing: | ||||||
|  |             propagator = HTTPPropagator() | ||||||
|  |             context = propagator.extract(request.headers) | ||||||
|  |             # Only need to active the new context if something was propagated | ||||||
|  |             if context.trace_id: | ||||||
|  |                 self.app._tracer.context_provider.activate(context) | ||||||
|  |         try: | ||||||
|  |             g.flask_datadog_span = self.app._tracer.trace( | ||||||
|  |                 SPAN_NAME, | ||||||
|  |                 service=self.app._service, | ||||||
|  |                 span_type=SpanTypes.WEB, | ||||||
|  |             ) | ||||||
|  |         except Exception: | ||||||
|  |             log.debug('flask: error tracing request', exc_info=True) | ||||||
|  | 
 | ||||||
|  |     def _process_response(self, response): | ||||||
|  |         span = getattr(g, 'flask_datadog_span', None) | ||||||
|  |         if not (span and span.sampled): | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         code = response.status_code if response else '' | ||||||
|  |         span.set_tag(http.STATUS_CODE, code) | ||||||
|  | 
 | ||||||
|  |     def _request_exception(self, *args, **kwargs): | ||||||
|  |         exception = kwargs.get('exception', None) | ||||||
|  |         span = getattr(g, 'flask_datadog_span', None) | ||||||
|  |         if span and exception: | ||||||
|  |             _set_error_on_span(span, exception) | ||||||
|  | 
 | ||||||
|  |     def _finish_span(self, span, exception=None): | ||||||
|  |         if not span or not span.sampled: | ||||||
|  |             return | ||||||
|  | 
 | ||||||
|  |         code = span.get_tag(http.STATUS_CODE) or 0 | ||||||
|  |         try: | ||||||
|  |             code = int(code) | ||||||
|  |         except Exception: | ||||||
|  |             code = 0 | ||||||
|  | 
 | ||||||
|  |         if exception: | ||||||
|  |             # if the request has already had a code set, don't override it. | ||||||
|  |             code = code or 500 | ||||||
|  |             _set_error_on_span(span, exception) | ||||||
|  | 
 | ||||||
|  |         # the endpoint that matched the request is None if an exception | ||||||
|  |         # happened so we fallback to a common resource | ||||||
|  |         span.error = 0 if code < 500 else 1 | ||||||
|  | 
 | ||||||
|  |         # the request isn't guaranteed to exist here, so only use it carefully. | ||||||
|  |         method = '' | ||||||
|  |         endpoint = '' | ||||||
|  |         url = '' | ||||||
|  |         if request: | ||||||
|  |             method = request.method | ||||||
|  |             endpoint = request.endpoint or code | ||||||
|  |             url = request.base_url or '' | ||||||
|  | 
 | ||||||
|  |         # Let users specify their own resource in middleware if they so desire. | ||||||
|  |         # See case https://github.com/DataDog/dd-trace-py/issues/353 | ||||||
|  |         if span.resource == SPAN_NAME: | ||||||
|  |             resource = endpoint or code | ||||||
|  |             span.resource = compat.to_unicode(resource).lower() | ||||||
|  | 
 | ||||||
|  |         span.set_tag(http.URL, compat.to_unicode(url)) | ||||||
|  |         span.set_tag(http.STATUS_CODE, code) | ||||||
|  |         span.set_tag(http.METHOD, method) | ||||||
|  |         span.finish() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _set_error_on_span(span, exception): | ||||||
|  |     # The 3 next lines might not be strictly required, since `set_traceback` | ||||||
|  |     # also get the exception from the sys.exc_info (and fill the error meta). | ||||||
|  |     # Since we aren't sure it always work/for insuring no BC break, keep | ||||||
|  |     # these lines which get overridden anyway. | ||||||
|  |     span.set_tag(errors.ERROR_TYPE, type(exception)) | ||||||
|  |     span.set_tag(errors.ERROR_MSG, exception) | ||||||
|  |     # The provided `exception` object doesn't have a stack trace attached, | ||||||
|  |     # so attach the stack trace with `set_traceback`. | ||||||
|  |     span.set_traceback() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _patch_render(tracer): | ||||||
|  |     """ patch flask's render template methods with the given tracer. """ | ||||||
|  |     # fall back to patching  global method | ||||||
|  |     _render = flask.templating._render | ||||||
|  | 
 | ||||||
|  |     def _traced_render(template, context, app): | ||||||
|  |         with tracer.trace('flask.template', span_type=SpanTypes.TEMPLATE) as span: | ||||||
|  |             span.set_tag('flask.template', template.name or 'string') | ||||||
|  |             return _render(template, context, app) | ||||||
|  | 
 | ||||||
|  |     flask.templating._render = _traced_render | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _signals_exist(names): | ||||||
|  |     """ Return true if all of the given signals exist in this version of flask. | ||||||
|  |     """ | ||||||
|  |     return all(getattr(signals, n, False) for n in names) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | _blinker_not_installed_msg = ( | ||||||
|  |     'please install blinker to use flask signals. ' | ||||||
|  |     'http://flask.pocoo.org/docs/0.11/signals/' | ||||||
|  | ) | ||||||
|  | @ -0,0 +1,497 @@ | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | import flask | ||||||
|  | import werkzeug | ||||||
|  | from ddtrace.vendor.wrapt import wrap_function_wrapper as _w | ||||||
|  | 
 | ||||||
|  | from ddtrace import compat | ||||||
|  | from ddtrace import config, Pin | ||||||
|  | 
 | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import SpanTypes, http | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | from ...propagation.http import HTTPPropagator | ||||||
|  | from ...utils.wrappers import unwrap as _u | ||||||
|  | from .helpers import get_current_app, get_current_span, simple_tracer, with_instance_pin | ||||||
|  | from .wrappers import wrap_function, wrap_signal | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | FLASK_ENDPOINT = 'flask.endpoint' | ||||||
|  | FLASK_VIEW_ARGS = 'flask.view_args' | ||||||
|  | FLASK_URL_RULE = 'flask.url_rule' | ||||||
|  | FLASK_VERSION = 'flask.version' | ||||||
|  | 
 | ||||||
|  | # Configure default configuration | ||||||
|  | config._add('flask', dict( | ||||||
|  |     # Flask service configuration | ||||||
|  |     # DEV: Environment variable 'DATADOG_SERVICE_NAME' used for backwards compatibility | ||||||
|  |     service_name=os.environ.get('DATADOG_SERVICE_NAME') or 'flask', | ||||||
|  |     app='flask', | ||||||
|  | 
 | ||||||
|  |     collect_view_args=True, | ||||||
|  |     distributed_tracing_enabled=True, | ||||||
|  |     template_default_name='<memory>', | ||||||
|  |     trace_signals=True, | ||||||
|  | 
 | ||||||
|  |     # We mark 5xx responses as errors, these codes are additional status codes to mark as errors | ||||||
|  |     # DEV: This is so that if a user wants to see `401` or `403` as an error, they can configure that | ||||||
|  |     extra_error_codes=set(), | ||||||
|  | )) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Extract flask version into a tuple e.g. (0, 12, 1) or (1, 0, 2) | ||||||
|  | # DEV: This makes it so we can do `if flask_version >= (0, 12, 0):` | ||||||
|  | # DEV: Example tests: | ||||||
|  | #      (0, 10, 0) > (0, 10) | ||||||
|  | #      (0, 10, 0) >= (0, 10, 0) | ||||||
|  | #      (0, 10, 1) >= (0, 10) | ||||||
|  | #      (0, 11, 1) >= (0, 10) | ||||||
|  | #      (0, 11, 1) >= (0, 10, 2) | ||||||
|  | #      (1, 0, 0) >= (0, 10) | ||||||
|  | #      (0, 9) == (0, 9) | ||||||
|  | #      (0, 9, 0) != (0, 9) | ||||||
|  | #      (0, 8, 5) <= (0, 9) | ||||||
|  | flask_version_str = getattr(flask, '__version__', '0.0.0') | ||||||
|  | flask_version = tuple([int(i) for i in flask_version_str.split('.')]) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """ | ||||||
|  |     Patch `flask` module for tracing | ||||||
|  |     """ | ||||||
|  |     # Check to see if we have patched Flask yet or not | ||||||
|  |     if getattr(flask, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(flask, '_datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     # Attach service pin to `flask.app.Flask` | ||||||
|  |     Pin( | ||||||
|  |         service=config.flask['service_name'], | ||||||
|  |         app=config.flask['app'] | ||||||
|  |     ).onto(flask.Flask) | ||||||
|  | 
 | ||||||
|  |     # flask.app.Flask methods that have custom tracing (add metadata, wrap functions, etc) | ||||||
|  |     _w('flask', 'Flask.wsgi_app', traced_wsgi_app) | ||||||
|  |     _w('flask', 'Flask.dispatch_request', request_tracer('dispatch_request')) | ||||||
|  |     _w('flask', 'Flask.preprocess_request', request_tracer('preprocess_request')) | ||||||
|  |     _w('flask', 'Flask.add_url_rule', traced_add_url_rule) | ||||||
|  |     _w('flask', 'Flask.endpoint', traced_endpoint) | ||||||
|  |     _w('flask', 'Flask._register_error_handler', traced_register_error_handler) | ||||||
|  | 
 | ||||||
|  |     # flask.blueprints.Blueprint methods that have custom tracing (add metadata, wrap functions, etc) | ||||||
|  |     _w('flask', 'Blueprint.register', traced_blueprint_register) | ||||||
|  |     _w('flask', 'Blueprint.add_url_rule', traced_blueprint_add_url_rule) | ||||||
|  | 
 | ||||||
|  |     # flask.app.Flask traced hook decorators | ||||||
|  |     flask_hooks = [ | ||||||
|  |         'before_request', | ||||||
|  |         'before_first_request', | ||||||
|  |         'after_request', | ||||||
|  |         'teardown_request', | ||||||
|  |         'teardown_appcontext', | ||||||
|  |     ] | ||||||
|  |     for hook in flask_hooks: | ||||||
|  |         _w('flask', 'Flask.{}'.format(hook), traced_flask_hook) | ||||||
|  |     _w('flask', 'after_this_request', traced_flask_hook) | ||||||
|  | 
 | ||||||
|  |     # flask.app.Flask traced methods | ||||||
|  |     flask_app_traces = [ | ||||||
|  |         'process_response', | ||||||
|  |         'handle_exception', | ||||||
|  |         'handle_http_exception', | ||||||
|  |         'handle_user_exception', | ||||||
|  |         'try_trigger_before_first_request_functions', | ||||||
|  |         'do_teardown_request', | ||||||
|  |         'do_teardown_appcontext', | ||||||
|  |         'send_static_file', | ||||||
|  |     ] | ||||||
|  |     for name in flask_app_traces: | ||||||
|  |         _w('flask', 'Flask.{}'.format(name), simple_tracer('flask.{}'.format(name))) | ||||||
|  | 
 | ||||||
|  |     # flask static file helpers | ||||||
|  |     _w('flask', 'send_file', simple_tracer('flask.send_file')) | ||||||
|  | 
 | ||||||
|  |     # flask.json.jsonify | ||||||
|  |     _w('flask', 'jsonify', traced_jsonify) | ||||||
|  | 
 | ||||||
|  |     # flask.templating traced functions | ||||||
|  |     _w('flask.templating', '_render', traced_render) | ||||||
|  |     _w('flask', 'render_template', traced_render_template) | ||||||
|  |     _w('flask', 'render_template_string', traced_render_template_string) | ||||||
|  | 
 | ||||||
|  |     # flask.blueprints.Blueprint traced hook decorators | ||||||
|  |     bp_hooks = [ | ||||||
|  |         'after_app_request', | ||||||
|  |         'after_request', | ||||||
|  |         'before_app_first_request', | ||||||
|  |         'before_app_request', | ||||||
|  |         'before_request', | ||||||
|  |         'teardown_request', | ||||||
|  |         'teardown_app_request', | ||||||
|  |     ] | ||||||
|  |     for hook in bp_hooks: | ||||||
|  |         _w('flask', 'Blueprint.{}'.format(hook), traced_flask_hook) | ||||||
|  | 
 | ||||||
|  |     # flask.signals signals | ||||||
|  |     if config.flask['trace_signals']: | ||||||
|  |         signals = [ | ||||||
|  |             'template_rendered', | ||||||
|  |             'request_started', | ||||||
|  |             'request_finished', | ||||||
|  |             'request_tearing_down', | ||||||
|  |             'got_request_exception', | ||||||
|  |             'appcontext_tearing_down', | ||||||
|  |         ] | ||||||
|  |         # These were added in 0.11.0 | ||||||
|  |         if flask_version >= (0, 11): | ||||||
|  |             signals.append('before_render_template') | ||||||
|  | 
 | ||||||
|  |         # These were added in 0.10.0 | ||||||
|  |         if flask_version >= (0, 10): | ||||||
|  |             signals.append('appcontext_pushed') | ||||||
|  |             signals.append('appcontext_popped') | ||||||
|  |             signals.append('message_flashed') | ||||||
|  | 
 | ||||||
|  |         for signal in signals: | ||||||
|  |             module = 'flask' | ||||||
|  | 
 | ||||||
|  |             # v0.9 missed importing `appcontext_tearing_down` in `flask/__init__.py` | ||||||
|  |             #  https://github.com/pallets/flask/blob/0.9/flask/__init__.py#L35-L37 | ||||||
|  |             #  https://github.com/pallets/flask/blob/0.9/flask/signals.py#L52 | ||||||
|  |             # DEV: Version 0.9 doesn't have a patch version | ||||||
|  |             if flask_version <= (0, 9) and signal == 'appcontext_tearing_down': | ||||||
|  |                 module = 'flask.signals' | ||||||
|  | 
 | ||||||
|  |             # DEV: Patch `receivers_for` instead of `connect` to ensure we don't mess with `disconnect` | ||||||
|  |             _w(module, '{}.receivers_for'.format(signal), traced_signal_receivers_for(signal)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     if not getattr(flask, '_datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(flask, '_datadog_patch', False) | ||||||
|  | 
 | ||||||
|  |     props = [ | ||||||
|  |         # Flask | ||||||
|  |         'Flask.wsgi_app', | ||||||
|  |         'Flask.dispatch_request', | ||||||
|  |         'Flask.add_url_rule', | ||||||
|  |         'Flask.endpoint', | ||||||
|  |         'Flask._register_error_handler', | ||||||
|  | 
 | ||||||
|  |         'Flask.preprocess_request', | ||||||
|  |         'Flask.process_response', | ||||||
|  |         'Flask.handle_exception', | ||||||
|  |         'Flask.handle_http_exception', | ||||||
|  |         'Flask.handle_user_exception', | ||||||
|  |         'Flask.try_trigger_before_first_request_functions', | ||||||
|  |         'Flask.do_teardown_request', | ||||||
|  |         'Flask.do_teardown_appcontext', | ||||||
|  |         'Flask.send_static_file', | ||||||
|  | 
 | ||||||
|  |         # Flask Hooks | ||||||
|  |         'Flask.before_request', | ||||||
|  |         'Flask.before_first_request', | ||||||
|  |         'Flask.after_request', | ||||||
|  |         'Flask.teardown_request', | ||||||
|  |         'Flask.teardown_appcontext', | ||||||
|  | 
 | ||||||
|  |         # Blueprint | ||||||
|  |         'Blueprint.register', | ||||||
|  |         'Blueprint.add_url_rule', | ||||||
|  | 
 | ||||||
|  |         # Blueprint Hooks | ||||||
|  |         'Blueprint.after_app_request', | ||||||
|  |         'Blueprint.after_request', | ||||||
|  |         'Blueprint.before_app_first_request', | ||||||
|  |         'Blueprint.before_app_request', | ||||||
|  |         'Blueprint.before_request', | ||||||
|  |         'Blueprint.teardown_request', | ||||||
|  |         'Blueprint.teardown_app_request', | ||||||
|  | 
 | ||||||
|  |         # Signals | ||||||
|  |         'template_rendered.receivers_for', | ||||||
|  |         'request_started.receivers_for', | ||||||
|  |         'request_finished.receivers_for', | ||||||
|  |         'request_tearing_down.receivers_for', | ||||||
|  |         'got_request_exception.receivers_for', | ||||||
|  |         'appcontext_tearing_down.receivers_for', | ||||||
|  | 
 | ||||||
|  |         # Top level props | ||||||
|  |         'after_this_request', | ||||||
|  |         'send_file', | ||||||
|  |         'jsonify', | ||||||
|  |         'render_template', | ||||||
|  |         'render_template_string', | ||||||
|  |         'templating._render', | ||||||
|  |     ] | ||||||
|  | 
 | ||||||
|  |     # These were added in 0.11.0 | ||||||
|  |     if flask_version >= (0, 11): | ||||||
|  |         props.append('before_render_template.receivers_for') | ||||||
|  | 
 | ||||||
|  |     # These were added in 0.10.0 | ||||||
|  |     if flask_version >= (0, 10): | ||||||
|  |         props.append('appcontext_pushed.receivers_for') | ||||||
|  |         props.append('appcontext_popped.receivers_for') | ||||||
|  |         props.append('message_flashed.receivers_for') | ||||||
|  | 
 | ||||||
|  |     for prop in props: | ||||||
|  |         # Handle 'flask.request_started.receivers_for' | ||||||
|  |         obj = flask | ||||||
|  | 
 | ||||||
|  |         # v0.9.0 missed importing `appcontext_tearing_down` in `flask/__init__.py` | ||||||
|  |         #  https://github.com/pallets/flask/blob/0.9/flask/__init__.py#L35-L37 | ||||||
|  |         #  https://github.com/pallets/flask/blob/0.9/flask/signals.py#L52 | ||||||
|  |         # DEV: Version 0.9 doesn't have a patch version | ||||||
|  |         if flask_version <= (0, 9) and prop == 'appcontext_tearing_down.receivers_for': | ||||||
|  |             obj = flask.signals | ||||||
|  | 
 | ||||||
|  |         if '.' in prop: | ||||||
|  |             attr, _, prop = prop.partition('.') | ||||||
|  |             obj = getattr(obj, attr, object()) | ||||||
|  |         _u(obj, prop) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | @with_instance_pin | ||||||
|  | def traced_wsgi_app(pin, wrapped, instance, args, kwargs): | ||||||
|  |     """ | ||||||
|  |     Wrapper for flask.app.Flask.wsgi_app | ||||||
|  | 
 | ||||||
|  |     This wrapper is the starting point for all requests. | ||||||
|  |     """ | ||||||
|  |     # DEV: This is safe before this is the args for a WSGI handler | ||||||
|  |     #   https://www.python.org/dev/peps/pep-3333/ | ||||||
|  |     environ, start_response = args | ||||||
|  | 
 | ||||||
|  |     # Create a werkzeug request from the `environ` to make interacting with it easier | ||||||
|  |     # DEV: This executes before a request context is created | ||||||
|  |     request = werkzeug.Request(environ) | ||||||
|  | 
 | ||||||
|  |     # Configure distributed tracing | ||||||
|  |     if config.flask.get('distributed_tracing_enabled', False): | ||||||
|  |         propagator = HTTPPropagator() | ||||||
|  |         context = propagator.extract(request.headers) | ||||||
|  |         # Only need to activate the new context if something was propagated | ||||||
|  |         if context.trace_id: | ||||||
|  |             pin.tracer.context_provider.activate(context) | ||||||
|  | 
 | ||||||
|  |     # Default resource is method and path: | ||||||
|  |     #   GET / | ||||||
|  |     #   POST /save | ||||||
|  |     # We will override this below in `traced_dispatch_request` when we have a `RequestContext` and possibly a url rule | ||||||
|  |     resource = u'{} {}'.format(request.method, request.path) | ||||||
|  |     with pin.tracer.trace('flask.request', service=pin.service, resource=resource, span_type=SpanTypes.WEB) as s: | ||||||
|  |         # set analytics sample rate with global config enabled | ||||||
|  |         sample_rate = config.flask.get_analytics_sample_rate(use_global_config=True) | ||||||
|  |         if sample_rate is not None: | ||||||
|  |             s.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) | ||||||
|  | 
 | ||||||
|  |         s.set_tag(FLASK_VERSION, flask_version_str) | ||||||
|  | 
 | ||||||
|  |         # Wrap the `start_response` handler to extract response code | ||||||
|  |         # DEV: We tried using `Flask.finalize_request`, which seemed to work, but gave us hell during tests | ||||||
|  |         # DEV: The downside to using `start_response` is we do not have a `Flask.Response` object here, | ||||||
|  |         #   only `status_code`, and `headers` to work with | ||||||
|  |         #   On the bright side, this works in all versions of Flask (or any WSGI app actually) | ||||||
|  |         def _wrap_start_response(func): | ||||||
|  |             def traced_start_response(status_code, headers): | ||||||
|  |                 code, _, _ = status_code.partition(' ') | ||||||
|  |                 try: | ||||||
|  |                     code = int(code) | ||||||
|  |                 except ValueError: | ||||||
|  |                     pass | ||||||
|  | 
 | ||||||
|  |                 # Override root span resource name to be `<method> 404` for 404 requests | ||||||
|  |                 # DEV: We do this because we want to make it easier to see all unknown requests together | ||||||
|  |                 #      Also, we do this to reduce the cardinality on unknown urls | ||||||
|  |                 # DEV: If we have an endpoint or url rule tag, then we don't need to do this, | ||||||
|  |                 #      we still want `GET /product/<int:product_id>` grouped together, | ||||||
|  |                 #      even if it is a 404 | ||||||
|  |                 if not s.get_tag(FLASK_ENDPOINT) and not s.get_tag(FLASK_URL_RULE): | ||||||
|  |                     s.resource = u'{} {}'.format(request.method, code) | ||||||
|  | 
 | ||||||
|  |                 s.set_tag(http.STATUS_CODE, code) | ||||||
|  |                 if 500 <= code < 600: | ||||||
|  |                     s.error = 1 | ||||||
|  |                 elif code in config.flask.get('extra_error_codes', set()): | ||||||
|  |                     s.error = 1 | ||||||
|  |                 return func(status_code, headers) | ||||||
|  |             return traced_start_response | ||||||
|  |         start_response = _wrap_start_response(start_response) | ||||||
|  | 
 | ||||||
|  |         # DEV: We set response status code in `_wrap_start_response` | ||||||
|  |         # DEV: Use `request.base_url` and not `request.url` to keep from leaking any query string parameters | ||||||
|  |         s.set_tag(http.URL, request.base_url) | ||||||
|  |         s.set_tag(http.METHOD, request.method) | ||||||
|  |         if config.flask.trace_query_string: | ||||||
|  |             s.set_tag(http.QUERY_STRING, compat.to_unicode(request.query_string)) | ||||||
|  | 
 | ||||||
|  |         return wrapped(environ, start_response) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_blueprint_register(wrapped, instance, args, kwargs): | ||||||
|  |     """ | ||||||
|  |     Wrapper for flask.blueprints.Blueprint.register | ||||||
|  | 
 | ||||||
|  |     This wrapper just ensures the blueprint has a pin, either set manually on | ||||||
|  |     itself from the user or inherited from the application | ||||||
|  |     """ | ||||||
|  |     app = kwargs.get('app', args[0]) | ||||||
|  |     # Check if this Blueprint has a pin, otherwise clone the one from the app onto it | ||||||
|  |     pin = Pin.get_from(instance) | ||||||
|  |     if not pin: | ||||||
|  |         pin = Pin.get_from(app) | ||||||
|  |         if pin: | ||||||
|  |             pin.clone().onto(instance) | ||||||
|  |     return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_blueprint_add_url_rule(wrapped, instance, args, kwargs): | ||||||
|  |     pin = Pin._find(wrapped, instance) | ||||||
|  |     if not pin: | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def _wrap(rule, endpoint=None, view_func=None, **kwargs): | ||||||
|  |         if view_func: | ||||||
|  |             pin.clone().onto(view_func) | ||||||
|  |         return wrapped(rule, endpoint=endpoint, view_func=view_func, **kwargs) | ||||||
|  | 
 | ||||||
|  |     return _wrap(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_add_url_rule(wrapped, instance, args, kwargs): | ||||||
|  |     """Wrapper for flask.app.Flask.add_url_rule to wrap all views attached to this app""" | ||||||
|  |     def _wrap(rule, endpoint=None, view_func=None, **kwargs): | ||||||
|  |         if view_func: | ||||||
|  |             # TODO: `if hasattr(view_func, 'view_class')` then this was generated from a `flask.views.View` | ||||||
|  |             #   should we do something special with these views? Change the name/resource? Add tags? | ||||||
|  |             view_func = wrap_function(instance, view_func, name=endpoint, resource=rule) | ||||||
|  | 
 | ||||||
|  |         return wrapped(rule, endpoint=endpoint, view_func=view_func, **kwargs) | ||||||
|  | 
 | ||||||
|  |     return _wrap(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_endpoint(wrapped, instance, args, kwargs): | ||||||
|  |     """Wrapper for flask.app.Flask.endpoint to ensure all endpoints are wrapped""" | ||||||
|  |     endpoint = kwargs.get('endpoint', args[0]) | ||||||
|  | 
 | ||||||
|  |     def _wrapper(func): | ||||||
|  |         # DEV: `wrap_function` will call `func_name(func)` for us | ||||||
|  |         return wrapped(endpoint)(wrap_function(instance, func, resource=endpoint)) | ||||||
|  |     return _wrapper | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_flask_hook(wrapped, instance, args, kwargs): | ||||||
|  |     """Wrapper for hook functions (before_request, after_request, etc) are properly traced""" | ||||||
|  |     func = kwargs.get('f', args[0]) | ||||||
|  |     return wrapped(wrap_function(instance, func)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_render_template(wrapped, instance, args, kwargs): | ||||||
|  |     """Wrapper for flask.templating.render_template""" | ||||||
|  |     pin = Pin._find(wrapped, instance, get_current_app()) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     with pin.tracer.trace('flask.render_template', span_type=SpanTypes.TEMPLATE): | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_render_template_string(wrapped, instance, args, kwargs): | ||||||
|  |     """Wrapper for flask.templating.render_template_string""" | ||||||
|  |     pin = Pin._find(wrapped, instance, get_current_app()) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     with pin.tracer.trace('flask.render_template_string', span_type=SpanTypes.TEMPLATE): | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_render(wrapped, instance, args, kwargs): | ||||||
|  |     """ | ||||||
|  |     Wrapper for flask.templating._render | ||||||
|  | 
 | ||||||
|  |     This wrapper is used for setting template tags on the span. | ||||||
|  | 
 | ||||||
|  |     This method is called for render_template or render_template_string | ||||||
|  |     """ | ||||||
|  |     pin = Pin._find(wrapped, instance, get_current_app()) | ||||||
|  |     # DEV: `get_current_span` will verify `pin` is valid and enabled first | ||||||
|  |     span = get_current_span(pin) | ||||||
|  |     if not span: | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     def _wrap(template, context, app): | ||||||
|  |         name = getattr(template, 'name', None) or config.flask.get('template_default_name') | ||||||
|  |         span.resource = name | ||||||
|  |         span.set_tag('flask.template_name', name) | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  |     return _wrap(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_register_error_handler(wrapped, instance, args, kwargs): | ||||||
|  |     """Wrapper to trace all functions registered with flask.app.register_error_handler""" | ||||||
|  |     def _wrap(key, code_or_exception, f): | ||||||
|  |         return wrapped(key, code_or_exception, wrap_function(instance, f)) | ||||||
|  |     return _wrap(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def request_tracer(name): | ||||||
|  |     @with_instance_pin | ||||||
|  |     def _traced_request(pin, wrapped, instance, args, kwargs): | ||||||
|  |         """ | ||||||
|  |         Wrapper to trace a Flask function while trying to extract endpoint information | ||||||
|  |           (endpoint, url_rule, view_args, etc) | ||||||
|  | 
 | ||||||
|  |         This wrapper will add identifier tags to the current span from `flask.app.Flask.wsgi_app`. | ||||||
|  |         """ | ||||||
|  |         span = get_current_span(pin) | ||||||
|  |         if not span: | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             request = flask._request_ctx_stack.top.request | ||||||
|  | 
 | ||||||
|  |             # DEV: This name will include the blueprint name as well (e.g. `bp.index`) | ||||||
|  |             if not span.get_tag(FLASK_ENDPOINT) and request.endpoint: | ||||||
|  |                 span.resource = u'{} {}'.format(request.method, request.endpoint) | ||||||
|  |                 span.set_tag(FLASK_ENDPOINT, request.endpoint) | ||||||
|  | 
 | ||||||
|  |             if not span.get_tag(FLASK_URL_RULE) and request.url_rule and request.url_rule.rule: | ||||||
|  |                 span.resource = u'{} {}'.format(request.method, request.url_rule.rule) | ||||||
|  |                 span.set_tag(FLASK_URL_RULE, request.url_rule.rule) | ||||||
|  | 
 | ||||||
|  |             if not span.get_tag(FLASK_VIEW_ARGS) and request.view_args and config.flask.get('collect_view_args'): | ||||||
|  |                 for k, v in request.view_args.items(): | ||||||
|  |                     span.set_tag(u'{}.{}'.format(FLASK_VIEW_ARGS, k), v) | ||||||
|  |         except Exception: | ||||||
|  |             log.debug('failed to set tags for "flask.request" span', exc_info=True) | ||||||
|  | 
 | ||||||
|  |         with pin.tracer.trace('flask.{}'.format(name), service=pin.service): | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  |     return _traced_request | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_signal_receivers_for(signal): | ||||||
|  |     """Wrapper for flask.signals.{signal}.receivers_for to ensure all signal receivers are traced""" | ||||||
|  |     def outer(wrapped, instance, args, kwargs): | ||||||
|  |         sender = kwargs.get('sender', args[0]) | ||||||
|  |         # See if they gave us the flask.app.Flask as the sender | ||||||
|  |         app = None | ||||||
|  |         if isinstance(sender, flask.Flask): | ||||||
|  |             app = sender | ||||||
|  |         for receiver in wrapped(*args, **kwargs): | ||||||
|  |             yield wrap_signal(app, signal, receiver) | ||||||
|  |     return outer | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def traced_jsonify(wrapped, instance, args, kwargs): | ||||||
|  |     pin = Pin._find(wrapped, instance, get_current_app()) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     with pin.tracer.trace('flask.jsonify'): | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | from ddtrace.vendor.wrapt import function_wrapper | ||||||
|  | 
 | ||||||
|  | from ...pin import Pin | ||||||
|  | from ...utils.importlib import func_name | ||||||
|  | from .helpers import get_current_app | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def wrap_function(instance, func, name=None, resource=None): | ||||||
|  |     """ | ||||||
|  |     Helper function to wrap common flask.app.Flask methods. | ||||||
|  | 
 | ||||||
|  |     This helper will first ensure that a Pin is available and enabled before tracing | ||||||
|  |     """ | ||||||
|  |     if not name: | ||||||
|  |         name = func_name(func) | ||||||
|  | 
 | ||||||
|  |     @function_wrapper | ||||||
|  |     def trace_func(wrapped, _instance, args, kwargs): | ||||||
|  |         pin = Pin._find(wrapped, _instance, instance, get_current_app()) | ||||||
|  |         if not pin or not pin.enabled(): | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  |         with pin.tracer.trace(name, service=pin.service, resource=resource): | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     return trace_func(func) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def wrap_signal(app, signal, func): | ||||||
|  |     """ | ||||||
|  |     Helper used to wrap signal handlers | ||||||
|  | 
 | ||||||
|  |     We will attempt to find the pin attached to the flask.app.Flask app | ||||||
|  |     """ | ||||||
|  |     name = func_name(func) | ||||||
|  | 
 | ||||||
|  |     @function_wrapper | ||||||
|  |     def trace_func(wrapped, instance, args, kwargs): | ||||||
|  |         pin = Pin._find(wrapped, instance, app, get_current_app()) | ||||||
|  |         if not pin or not pin.enabled(): | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         with pin.tracer.trace(name, service=pin.service) as span: | ||||||
|  |             span.set_tag('flask.signal', signal) | ||||||
|  |             return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     return trace_func(func) | ||||||
|  | @ -0,0 +1,44 @@ | ||||||
|  | """ | ||||||
|  | The flask cache tracer will track any access to a cache backend. | ||||||
|  | You can use this tracer together with the Flask tracer middleware. | ||||||
|  | 
 | ||||||
|  | To install the tracer, ``from ddtrace import tracer`` needs to be added:: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import tracer | ||||||
|  |     from ddtrace.contrib.flask_cache import get_traced_cache | ||||||
|  | 
 | ||||||
|  | and the tracer needs to be initialized:: | ||||||
|  | 
 | ||||||
|  |     Cache = get_traced_cache(tracer, service='my-flask-cache-app') | ||||||
|  | 
 | ||||||
|  | Here is the end result, in a sample app:: | ||||||
|  | 
 | ||||||
|  |     from flask import Flask | ||||||
|  | 
 | ||||||
|  |     from ddtrace import tracer | ||||||
|  |     from ddtrace.contrib.flask_cache import get_traced_cache | ||||||
|  | 
 | ||||||
|  |     app = Flask(__name__) | ||||||
|  | 
 | ||||||
|  |     # get the traced Cache class | ||||||
|  |     Cache = get_traced_cache(tracer, service='my-flask-cache-app') | ||||||
|  | 
 | ||||||
|  |     # use the Cache as usual with your preferred CACHE_TYPE | ||||||
|  |     cache = Cache(app, config={'CACHE_TYPE': 'simple'}) | ||||||
|  | 
 | ||||||
|  |     def counter(): | ||||||
|  |         # this access is traced | ||||||
|  |         conn_counter = cache.get("conn_counter") | ||||||
|  | 
 | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['flask_cache'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .tracers import get_traced_cache | ||||||
|  | 
 | ||||||
|  |         __all__ = ['get_traced_cache'] | ||||||
|  | @ -0,0 +1,146 @@ | ||||||
|  | """ | ||||||
|  | Datadog trace code for flask_cache | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | # stdlib | ||||||
|  | import logging | ||||||
|  | 
 | ||||||
|  | # project | ||||||
|  | from .utils import _extract_conn_tags, _resource_from_cache_prefix | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import SpanTypes | ||||||
|  | from ...settings import config | ||||||
|  | 
 | ||||||
|  | # 3rd party | ||||||
|  | from flask.ext.cache import Cache | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | log = logging.Logger(__name__) | ||||||
|  | 
 | ||||||
|  | DEFAULT_SERVICE = 'flask-cache' | ||||||
|  | 
 | ||||||
|  | # standard tags | ||||||
|  | COMMAND_KEY = 'flask_cache.key' | ||||||
|  | CACHE_BACKEND = 'flask_cache.backend' | ||||||
|  | CONTACT_POINTS = 'flask_cache.contact_points' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def get_traced_cache(ddtracer, service=DEFAULT_SERVICE, meta=None): | ||||||
|  |     """ | ||||||
|  |     Return a traced Cache object that behaves exactly as the ``flask.ext.cache.Cache class`` | ||||||
|  |     """ | ||||||
|  | 
 | ||||||
|  |     class TracedCache(Cache): | ||||||
|  |         """ | ||||||
|  |         Traced cache backend that monitors any operations done by flask_cache. Observed actions are: | ||||||
|  |         * get, set, add, delete, clear | ||||||
|  |         * all ``many_`` operations | ||||||
|  |         """ | ||||||
|  |         _datadog_tracer = ddtracer | ||||||
|  |         _datadog_service = service | ||||||
|  |         _datadog_meta = meta | ||||||
|  | 
 | ||||||
|  |         def __trace(self, cmd): | ||||||
|  |             """ | ||||||
|  |             Start a tracing with default attributes and tags | ||||||
|  |             """ | ||||||
|  |             # create a new span | ||||||
|  |             s = self._datadog_tracer.trace( | ||||||
|  |                 cmd, | ||||||
|  |                 span_type=SpanTypes.CACHE, | ||||||
|  |                 service=self._datadog_service | ||||||
|  |             ) | ||||||
|  |             # set span tags | ||||||
|  |             s.set_tag(CACHE_BACKEND, self.config.get('CACHE_TYPE')) | ||||||
|  |             s.set_tags(self._datadog_meta) | ||||||
|  |             # set analytics sample rate | ||||||
|  |             s.set_tag( | ||||||
|  |                 ANALYTICS_SAMPLE_RATE_KEY, | ||||||
|  |                 config.flask_cache.get_analytics_sample_rate() | ||||||
|  |             ) | ||||||
|  |             # add connection meta if there is one | ||||||
|  |             if getattr(self.cache, '_client', None): | ||||||
|  |                 try: | ||||||
|  |                     s.set_tags(_extract_conn_tags(self.cache._client)) | ||||||
|  |                 except Exception: | ||||||
|  |                     log.debug('error parsing connection tags', exc_info=True) | ||||||
|  | 
 | ||||||
|  |             return s | ||||||
|  | 
 | ||||||
|  |         def get(self, *args, **kwargs): | ||||||
|  |             """ | ||||||
|  |             Track ``get`` operation | ||||||
|  |             """ | ||||||
|  |             with self.__trace('flask_cache.cmd') as span: | ||||||
|  |                 span.resource = _resource_from_cache_prefix('GET', self.config) | ||||||
|  |                 if len(args) > 0: | ||||||
|  |                     span.set_tag(COMMAND_KEY, args[0]) | ||||||
|  |                 return super(TracedCache, self).get(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         def set(self, *args, **kwargs): | ||||||
|  |             """ | ||||||
|  |             Track ``set`` operation | ||||||
|  |             """ | ||||||
|  |             with self.__trace('flask_cache.cmd') as span: | ||||||
|  |                 span.resource = _resource_from_cache_prefix('SET', self.config) | ||||||
|  |                 if len(args) > 0: | ||||||
|  |                     span.set_tag(COMMAND_KEY, args[0]) | ||||||
|  |                 return super(TracedCache, self).set(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         def add(self, *args, **kwargs): | ||||||
|  |             """ | ||||||
|  |             Track ``add`` operation | ||||||
|  |             """ | ||||||
|  |             with self.__trace('flask_cache.cmd') as span: | ||||||
|  |                 span.resource = _resource_from_cache_prefix('ADD', self.config) | ||||||
|  |                 if len(args) > 0: | ||||||
|  |                     span.set_tag(COMMAND_KEY, args[0]) | ||||||
|  |                 return super(TracedCache, self).add(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         def delete(self, *args, **kwargs): | ||||||
|  |             """ | ||||||
|  |             Track ``delete`` operation | ||||||
|  |             """ | ||||||
|  |             with self.__trace('flask_cache.cmd') as span: | ||||||
|  |                 span.resource = _resource_from_cache_prefix('DELETE', self.config) | ||||||
|  |                 if len(args) > 0: | ||||||
|  |                     span.set_tag(COMMAND_KEY, args[0]) | ||||||
|  |                 return super(TracedCache, self).delete(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         def delete_many(self, *args, **kwargs): | ||||||
|  |             """ | ||||||
|  |             Track ``delete_many`` operation | ||||||
|  |             """ | ||||||
|  |             with self.__trace('flask_cache.cmd') as span: | ||||||
|  |                 span.resource = _resource_from_cache_prefix('DELETE_MANY', self.config) | ||||||
|  |                 span.set_tag(COMMAND_KEY, list(args)) | ||||||
|  |                 return super(TracedCache, self).delete_many(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         def clear(self, *args, **kwargs): | ||||||
|  |             """ | ||||||
|  |             Track ``clear`` operation | ||||||
|  |             """ | ||||||
|  |             with self.__trace('flask_cache.cmd') as span: | ||||||
|  |                 span.resource = _resource_from_cache_prefix('CLEAR', self.config) | ||||||
|  |                 return super(TracedCache, self).clear(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         def get_many(self, *args, **kwargs): | ||||||
|  |             """ | ||||||
|  |             Track ``get_many`` operation | ||||||
|  |             """ | ||||||
|  |             with self.__trace('flask_cache.cmd') as span: | ||||||
|  |                 span.resource = _resource_from_cache_prefix('GET_MANY', self.config) | ||||||
|  |                 span.set_tag(COMMAND_KEY, list(args)) | ||||||
|  |                 return super(TracedCache, self).get_many(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         def set_many(self, *args, **kwargs): | ||||||
|  |             """ | ||||||
|  |             Track ``set_many`` operation | ||||||
|  |             """ | ||||||
|  |             with self.__trace('flask_cache.cmd') as span: | ||||||
|  |                 span.resource = _resource_from_cache_prefix('SET_MANY', self.config) | ||||||
|  |                 if len(args) > 0: | ||||||
|  |                     span.set_tag(COMMAND_KEY, list(args[0].keys())) | ||||||
|  |                 return super(TracedCache, self).set_many(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     return TracedCache | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | # project | ||||||
|  | from ...ext import net | ||||||
|  | from ..redis.util import _extract_conn_tags as extract_redis_tags | ||||||
|  | from ..pylibmc.addrs import parse_addresses | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _resource_from_cache_prefix(resource, cache): | ||||||
|  |     """ | ||||||
|  |     Combine the resource name with the cache prefix (if any) | ||||||
|  |     """ | ||||||
|  |     if getattr(cache, 'key_prefix', None): | ||||||
|  |         name = '{} {}'.format(resource, cache.key_prefix) | ||||||
|  |     else: | ||||||
|  |         name = resource | ||||||
|  | 
 | ||||||
|  |     # enforce lowercase to make the output nicer to read | ||||||
|  |     return name.lower() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _extract_conn_tags(client): | ||||||
|  |     """ | ||||||
|  |     For the given client extracts connection tags | ||||||
|  |     """ | ||||||
|  |     tags = {} | ||||||
|  | 
 | ||||||
|  |     if hasattr(client, 'servers'): | ||||||
|  |         # Memcached backend supports an address pool | ||||||
|  |         if isinstance(client.servers, list) and len(client.servers) > 0: | ||||||
|  |             # use the first address of the pool as a host because | ||||||
|  |             # the code doesn't expose more information | ||||||
|  |             contact_point = client.servers[0].address | ||||||
|  |             tags[net.TARGET_HOST] = contact_point[0] | ||||||
|  |             tags[net.TARGET_PORT] = contact_point[1] | ||||||
|  |     elif hasattr(client, 'connection_pool'): | ||||||
|  |         # Redis main connection | ||||||
|  |         redis_tags = extract_redis_tags(client.connection_pool.connection_kwargs) | ||||||
|  |         tags.update(**redis_tags) | ||||||
|  |     elif hasattr(client, 'addresses'): | ||||||
|  |         # pylibmc | ||||||
|  |         # FIXME[matt] should we memoize this? | ||||||
|  |         addrs = parse_addresses(client.addresses) | ||||||
|  |         if addrs: | ||||||
|  |             _, host, port, _ = addrs[0] | ||||||
|  |             tags[net.TARGET_PORT] = port | ||||||
|  |             tags[net.TARGET_HOST] = host | ||||||
|  |     return tags | ||||||
|  | @ -0,0 +1,29 @@ | ||||||
|  | """ | ||||||
|  | The ``futures`` integration propagates the current active Tracing Context | ||||||
|  | between threads. The integration ensures that when operations are executed | ||||||
|  | in a new thread, that thread can continue the previously generated trace. | ||||||
|  | 
 | ||||||
|  | The integration doesn't trace automatically threads execution, so manual | ||||||
|  | instrumentation or another integration must be activated. Threads propagation | ||||||
|  | is not enabled by default with the `patch_all()` method and must be activated | ||||||
|  | as follows:: | ||||||
|  | 
 | ||||||
|  |     from ddtrace import patch, patch_all | ||||||
|  | 
 | ||||||
|  |     patch(futures=True) | ||||||
|  |     # or, when instrumenting all libraries | ||||||
|  |     patch_all(futures=True) | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['concurrent.futures'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch, unpatch | ||||||
|  | 
 | ||||||
|  |         __all__ = [ | ||||||
|  |             'patch', | ||||||
|  |             'unpatch', | ||||||
|  |         ] | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | from concurrent import futures | ||||||
|  | 
 | ||||||
|  | from ddtrace.vendor.wrapt import wrap_function_wrapper as _w | ||||||
|  | 
 | ||||||
|  | from .threading import _wrap_submit | ||||||
|  | from ...utils.wrappers import unwrap as _u | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """Enables Context Propagation between threads""" | ||||||
|  |     if getattr(futures, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(futures, '__datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     _w('concurrent.futures', 'ThreadPoolExecutor.submit', _wrap_submit) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     """Disables Context Propagation between threads""" | ||||||
|  |     if not getattr(futures, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(futures, '__datadog_patch', False) | ||||||
|  | 
 | ||||||
|  |     _u(futures.ThreadPoolExecutor, 'submit') | ||||||
|  | @ -0,0 +1,46 @@ | ||||||
|  | import ddtrace | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _wrap_submit(func, instance, args, kwargs): | ||||||
|  |     """ | ||||||
|  |     Wrap `Executor` method used to submit a work executed in another | ||||||
|  |     thread. This wrapper ensures that a new `Context` is created and | ||||||
|  |     properly propagated using an intermediate function. | ||||||
|  |     """ | ||||||
|  |     # If there isn't a currently active context, then do not create one | ||||||
|  |     # DEV: Calling `.active()` when there isn't an active context will create a new context | ||||||
|  |     # DEV: We need to do this in case they are either: | ||||||
|  |     #        - Starting nested futures | ||||||
|  |     #        - Starting futures from outside of an existing context | ||||||
|  |     # | ||||||
|  |     #      In either of these cases we essentially will propagate the wrong context between futures | ||||||
|  |     # | ||||||
|  |     #      The resolution is to not create/propagate a new context if one does not exist, but let the | ||||||
|  |     #      future's thread create the context instead. | ||||||
|  |     current_ctx = None | ||||||
|  |     if ddtrace.tracer.context_provider._has_active_context(): | ||||||
|  |         current_ctx = ddtrace.tracer.context_provider.active() | ||||||
|  | 
 | ||||||
|  |         # If we have a context then make sure we clone it | ||||||
|  |         # DEV: We don't know if the future will finish executing before the parent span finishes | ||||||
|  |         #      so we clone to ensure we properly collect/report the future's spans | ||||||
|  |         current_ctx = current_ctx.clone() | ||||||
|  | 
 | ||||||
|  |     # extract the target function that must be executed in | ||||||
|  |     # a new thread and the `target` arguments | ||||||
|  |     fn = args[0] | ||||||
|  |     fn_args = args[1:] | ||||||
|  |     return func(_wrap_execution, current_ctx, fn, fn_args, kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _wrap_execution(ctx, fn, args, kwargs): | ||||||
|  |     """ | ||||||
|  |     Intermediate target function that is executed in a new thread; | ||||||
|  |     it receives the original function with arguments and keyword | ||||||
|  |     arguments, including our tracing `Context`. The current context | ||||||
|  |     provider sets the Active context in a thread local storage | ||||||
|  |     variable because it's outside the asynchronous loop. | ||||||
|  |     """ | ||||||
|  |     if ctx is not None: | ||||||
|  |         ddtrace.tracer.context_provider.activate(ctx) | ||||||
|  |     return fn(*args, **kwargs) | ||||||
|  | @ -0,0 +1,48 @@ | ||||||
|  | """ | ||||||
|  | To trace a request in a ``gevent`` environment, configure the tracer to use the greenlet | ||||||
|  | context provider, rather than the default one that relies on a thread-local storaging. | ||||||
|  | 
 | ||||||
|  | This allows the tracer to pick up a transaction exactly where it left off as greenlets | ||||||
|  | yield the context to another one. | ||||||
|  | 
 | ||||||
|  | The simplest way to trace a ``gevent`` application is to configure the tracer and | ||||||
|  | patch ``gevent`` **before importing** the library:: | ||||||
|  | 
 | ||||||
|  |     # patch before importing gevent | ||||||
|  |     from ddtrace import patch, tracer | ||||||
|  |     patch(gevent=True) | ||||||
|  | 
 | ||||||
|  |     # use gevent as usual with or without the monkey module | ||||||
|  |     from gevent import monkey; monkey.patch_thread() | ||||||
|  | 
 | ||||||
|  |     def my_parent_function(): | ||||||
|  |         with tracer.trace("web.request") as span: | ||||||
|  |             span.service = "web" | ||||||
|  |             gevent.spawn(worker_function) | ||||||
|  | 
 | ||||||
|  |     def worker_function(): | ||||||
|  |         # then trace its child | ||||||
|  |         with tracer.trace("greenlet.call") as span: | ||||||
|  |             span.service = "greenlet" | ||||||
|  |             ... | ||||||
|  | 
 | ||||||
|  |             with tracer.trace("greenlet.child_call") as child: | ||||||
|  |                 ... | ||||||
|  | """ | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | required_modules = ['gevent'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .provider import GeventContextProvider | ||||||
|  |         from .patch import patch, unpatch | ||||||
|  | 
 | ||||||
|  |         context_provider = GeventContextProvider() | ||||||
|  | 
 | ||||||
|  |         __all__ = [ | ||||||
|  |             'patch', | ||||||
|  |             'unpatch', | ||||||
|  |             'context_provider', | ||||||
|  |         ] | ||||||
|  | @ -0,0 +1,58 @@ | ||||||
|  | import gevent | ||||||
|  | import gevent.pool as gpool | ||||||
|  | 
 | ||||||
|  | from .provider import CONTEXT_ATTR | ||||||
|  | 
 | ||||||
|  | GEVENT_VERSION = gevent.version_info[0:3] | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TracingMixin(object): | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         # get the current Context if available | ||||||
|  |         current_g = gevent.getcurrent() | ||||||
|  |         ctx = getattr(current_g, CONTEXT_ATTR, None) | ||||||
|  | 
 | ||||||
|  |         # create the Greenlet as usual | ||||||
|  |         super(TracingMixin, self).__init__(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |         # the context is always available made exception of the main greenlet | ||||||
|  |         if ctx: | ||||||
|  |             # create a new context that inherits the current active span | ||||||
|  |             new_ctx = ctx.clone() | ||||||
|  |             setattr(self, CONTEXT_ATTR, new_ctx) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TracedGreenlet(TracingMixin, gevent.Greenlet): | ||||||
|  |     """ | ||||||
|  |     ``Greenlet`` class that is used to replace the original ``gevent`` | ||||||
|  |     class. This class is supposed to do ``Context`` replacing operation, so | ||||||
|  |     that any greenlet inherits the context from the parent Greenlet. | ||||||
|  |     When a new greenlet is spawned from the main greenlet, a new instance | ||||||
|  |     of ``Context`` is created. The main greenlet is not affected by this behavior. | ||||||
|  | 
 | ||||||
|  |     There is no need to inherit this class to create or optimize greenlets | ||||||
|  |     instances, because this class replaces ``gevent.greenlet.Greenlet`` | ||||||
|  |     through the ``patch()`` method. After the patch, extending the gevent | ||||||
|  |     ``Greenlet`` class means extending automatically ``TracedGreenlet``. | ||||||
|  |     """ | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         super(TracedGreenlet, self).__init__(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class TracedIMapUnordered(TracingMixin, gpool.IMapUnordered): | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         super(TracedIMapUnordered, self).__init__(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | if GEVENT_VERSION >= (1, 3) or GEVENT_VERSION < (1, 1): | ||||||
|  |     # For gevent <1.1 and >=1.3, IMap is its own class, so we derive | ||||||
|  |     # from TracingMixin | ||||||
|  |     class TracedIMap(TracingMixin, gpool.IMap): | ||||||
|  |         def __init__(self, *args, **kwargs): | ||||||
|  |             super(TracedIMap, self).__init__(*args, **kwargs) | ||||||
|  | else: | ||||||
|  |     # For gevent >=1.1 and <1.3, IMap derives from IMapUnordered, so we derive | ||||||
|  |     # from TracedIMapUnordered and get tracing that way | ||||||
|  |     class TracedIMap(gpool.IMap, TracedIMapUnordered): | ||||||
|  |         def __init__(self, *args, **kwargs): | ||||||
|  |             super(TracedIMap, self).__init__(*args, **kwargs) | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | import gevent | ||||||
|  | import gevent.pool | ||||||
|  | import ddtrace | ||||||
|  | 
 | ||||||
|  | from .greenlet import TracedGreenlet, TracedIMap, TracedIMapUnordered, GEVENT_VERSION | ||||||
|  | from .provider import GeventContextProvider | ||||||
|  | from ...provider import DefaultContextProvider | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | __Greenlet = gevent.Greenlet | ||||||
|  | __IMap = gevent.pool.IMap | ||||||
|  | __IMapUnordered = gevent.pool.IMapUnordered | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     """ | ||||||
|  |     Patch the gevent module so that all references to the | ||||||
|  |     internal ``Greenlet`` class points to the ``DatadogGreenlet`` | ||||||
|  |     class. | ||||||
|  | 
 | ||||||
|  |     This action ensures that if a user extends the ``Greenlet`` | ||||||
|  |     class, the ``TracedGreenlet`` is used as a parent class. | ||||||
|  |     """ | ||||||
|  |     _replace(TracedGreenlet, TracedIMap, TracedIMapUnordered) | ||||||
|  |     ddtrace.tracer.configure(context_provider=GeventContextProvider()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     """ | ||||||
|  |     Restore the original ``Greenlet``. This function must be invoked | ||||||
|  |     before executing application code, otherwise the ``DatadogGreenlet`` | ||||||
|  |     class may be used during initialization. | ||||||
|  |     """ | ||||||
|  |     _replace(__Greenlet, __IMap, __IMapUnordered) | ||||||
|  |     ddtrace.tracer.configure(context_provider=DefaultContextProvider()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _replace(g_class, imap_class, imap_unordered_class): | ||||||
|  |     """ | ||||||
|  |     Utility function that replace the gevent Greenlet class with the given one. | ||||||
|  |     """ | ||||||
|  |     # replace the original Greenlet classes with the new one | ||||||
|  |     gevent.greenlet.Greenlet = g_class | ||||||
|  | 
 | ||||||
|  |     if GEVENT_VERSION >= (1, 3): | ||||||
|  |         # For gevent >= 1.3.0, IMap and IMapUnordered were pulled out of | ||||||
|  |         # gevent.pool and into gevent._imap | ||||||
|  |         gevent._imap.IMap = imap_class | ||||||
|  |         gevent._imap.IMapUnordered = imap_unordered_class | ||||||
|  |         gevent.pool.IMap = gevent._imap.IMap | ||||||
|  |         gevent.pool.IMapUnordered = gevent._imap.IMapUnordered | ||||||
|  |         gevent.pool.Greenlet = gevent.greenlet.Greenlet | ||||||
|  |     else: | ||||||
|  |         # For gevent < 1.3, only patching of gevent.pool classes necessary | ||||||
|  |         gevent.pool.IMap = imap_class | ||||||
|  |         gevent.pool.IMapUnordered = imap_unordered_class | ||||||
|  | 
 | ||||||
|  |     gevent.pool.Group.greenlet_class = g_class | ||||||
|  | 
 | ||||||
|  |     # replace gevent shortcuts | ||||||
|  |     gevent.Greenlet = gevent.greenlet.Greenlet | ||||||
|  |     gevent.spawn = gevent.greenlet.Greenlet.spawn | ||||||
|  |     gevent.spawn_later = gevent.greenlet.Greenlet.spawn_later | ||||||
|  | @ -0,0 +1,55 @@ | ||||||
|  | import gevent | ||||||
|  | 
 | ||||||
|  | from ...context import Context | ||||||
|  | from ...provider import BaseContextProvider | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | # Greenlet attribute used to set/get the Context instance | ||||||
|  | CONTEXT_ATTR = '__datadog_context' | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class GeventContextProvider(BaseContextProvider): | ||||||
|  |     """ | ||||||
|  |     Context provider that retrieves all contexts for the current asynchronous | ||||||
|  |     execution. It must be used in asynchronous programming that relies | ||||||
|  |     in the ``gevent`` library. Framework instrumentation that uses the | ||||||
|  |     gevent WSGI server (or gevent in general), can use this provider. | ||||||
|  |     """ | ||||||
|  |     def _get_current_context(self): | ||||||
|  |         """Helper to get the current context from the current greenlet""" | ||||||
|  |         current_g = gevent.getcurrent() | ||||||
|  |         if current_g is not None: | ||||||
|  |             return getattr(current_g, CONTEXT_ATTR, None) | ||||||
|  |         return None | ||||||
|  | 
 | ||||||
|  |     def _has_active_context(self): | ||||||
|  |         """Helper to determine if we have a currently active context""" | ||||||
|  |         return self._get_current_context() is not None | ||||||
|  | 
 | ||||||
|  |     def activate(self, context): | ||||||
|  |         """Sets the scoped ``Context`` for the current running ``Greenlet``. | ||||||
|  |         """ | ||||||
|  |         current_g = gevent.getcurrent() | ||||||
|  |         if current_g is not None: | ||||||
|  |             setattr(current_g, CONTEXT_ATTR, context) | ||||||
|  |             return context | ||||||
|  | 
 | ||||||
|  |     def active(self): | ||||||
|  |         """ | ||||||
|  |         Returns the scoped ``Context`` for this execution flow. The ``Context`` | ||||||
|  |         uses the ``Greenlet`` class as a carrier, and everytime a greenlet | ||||||
|  |         is created it receives the "parent" context. | ||||||
|  |         """ | ||||||
|  |         ctx = self._get_current_context() | ||||||
|  |         if ctx is not None: | ||||||
|  |             # return the active Context for this greenlet (if any) | ||||||
|  |             return ctx | ||||||
|  | 
 | ||||||
|  |         # the Greenlet doesn't have a Context so it's created and attached | ||||||
|  |         # even to the main greenlet. This is required in Distributed Tracing | ||||||
|  |         # when a new arbitrary Context is provided. | ||||||
|  |         current_g = gevent.getcurrent() | ||||||
|  |         if current_g: | ||||||
|  |             ctx = Context() | ||||||
|  |             setattr(current_g, CONTEXT_ATTR, ctx) | ||||||
|  |             return ctx | ||||||
|  | @ -0,0 +1,57 @@ | ||||||
|  | """ | ||||||
|  | The gRPC integration traces the client and server using interceptor pattern. | ||||||
|  | 
 | ||||||
|  | gRPC will be automatically instrumented with ``patch_all``, or when using | ||||||
|  | the ``ddtrace-run`` command. | ||||||
|  | gRPC is instrumented on import. To instrument gRPC manually use the | ||||||
|  | ``patch`` function.:: | ||||||
|  | 
 | ||||||
|  |     import grpc | ||||||
|  |     from ddtrace import patch | ||||||
|  |     patch(grpc=True) | ||||||
|  | 
 | ||||||
|  |     # use grpc like usual | ||||||
|  | 
 | ||||||
|  | To configure the gRPC integration on an per-channel basis use the | ||||||
|  | ``Pin`` API:: | ||||||
|  | 
 | ||||||
|  |     import grpc | ||||||
|  |     from ddtrace import Pin, patch, Tracer | ||||||
|  | 
 | ||||||
|  |     patch(grpc=True) | ||||||
|  |     custom_tracer = Tracer() | ||||||
|  | 
 | ||||||
|  |     # override the pin on the client | ||||||
|  |     Pin.override(grpc.Channel, service='mygrpc', tracer=custom_tracer) | ||||||
|  |     with grpc.insecure_channel('localhost:50051') as channel: | ||||||
|  |         # create stubs and send requests | ||||||
|  |         pass | ||||||
|  | 
 | ||||||
|  | To configure the gRPC integration on the server use the ``Pin`` API:: | ||||||
|  | 
 | ||||||
|  |     import grpc | ||||||
|  |     from grpc.framework.foundation import logging_pool | ||||||
|  | 
 | ||||||
|  |     from ddtrace import Pin, patch, Tracer | ||||||
|  | 
 | ||||||
|  |     patch(grpc=True) | ||||||
|  |     custom_tracer = Tracer() | ||||||
|  | 
 | ||||||
|  |     # override the pin on the server | ||||||
|  |     Pin.override(grpc.Server, service='mygrpc', tracer=custom_tracer) | ||||||
|  |     server = grpc.server(logging_pool.pool(2)) | ||||||
|  |     server.add_insecure_port('localhost:50051') | ||||||
|  |     add_MyServicer_to_server(MyServicer(), server) | ||||||
|  |     server.start() | ||||||
|  | """ | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | from ...utils.importlib import require_modules | ||||||
|  | 
 | ||||||
|  | required_modules = ['grpc'] | ||||||
|  | 
 | ||||||
|  | with require_modules(required_modules) as missing_modules: | ||||||
|  |     if not missing_modules: | ||||||
|  |         from .patch import patch, unpatch | ||||||
|  | 
 | ||||||
|  |         __all__ = ['patch', 'unpatch'] | ||||||
|  | @ -0,0 +1,239 @@ | ||||||
|  | import collections | ||||||
|  | import grpc | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | 
 | ||||||
|  | from ddtrace import config | ||||||
|  | from ddtrace.compat import to_unicode | ||||||
|  | from ddtrace.ext import SpanTypes, errors | ||||||
|  | from ...internal.logger import get_logger | ||||||
|  | from ...propagation.http import HTTPPropagator | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from . import constants | ||||||
|  | from .utils import parse_method_path | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | log = get_logger(__name__) | ||||||
|  | 
 | ||||||
|  | # DEV: Follows Python interceptors RFC laid out in | ||||||
|  | # https://github.com/grpc/proposal/blob/master/L13-python-interceptors.md | ||||||
|  | 
 | ||||||
|  | # DEV: __version__ added in v1.21.4 | ||||||
|  | # https://github.com/grpc/grpc/commit/dd4830eae80143f5b0a9a3a1a024af4cf60e7d02 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def create_client_interceptor(pin, host, port): | ||||||
|  |     return _ClientInterceptor(pin, host, port) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def intercept_channel(wrapped, instance, args, kwargs): | ||||||
|  |     channel = args[0] | ||||||
|  |     interceptors = args[1:] | ||||||
|  |     if isinstance(getattr(channel, "_interceptor", None), _ClientInterceptor): | ||||||
|  |         dd_interceptor = channel._interceptor | ||||||
|  |         base_channel = getattr(channel, "_channel", None) | ||||||
|  |         if base_channel: | ||||||
|  |             new_channel = wrapped(channel._channel, *interceptors) | ||||||
|  |             return grpc.intercept_channel(new_channel, dd_interceptor) | ||||||
|  | 
 | ||||||
|  |     return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _ClientCallDetails( | ||||||
|  |     collections.namedtuple("_ClientCallDetails", ("method", "timeout", "metadata", "credentials")), | ||||||
|  |     grpc.ClientCallDetails, | ||||||
|  | ): | ||||||
|  |     pass | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _future_done_callback(span): | ||||||
|  |     def func(response): | ||||||
|  |         try: | ||||||
|  |             # pull out response code from gRPC response to use both for `grpc.status.code` | ||||||
|  |             # tag and the error type tag if the response is an exception | ||||||
|  |             response_code = response.code() | ||||||
|  |             # cast code to unicode for tags | ||||||
|  |             status_code = to_unicode(response_code) | ||||||
|  |             span.set_tag(constants.GRPC_STATUS_CODE_KEY, status_code) | ||||||
|  | 
 | ||||||
|  |             if response_code != grpc.StatusCode.OK: | ||||||
|  |                 _handle_error(span, response, status_code) | ||||||
|  |         finally: | ||||||
|  |             span.finish() | ||||||
|  | 
 | ||||||
|  |     return func | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _handle_response(span, response): | ||||||
|  |     if isinstance(response, grpc.Future): | ||||||
|  |         response.add_done_callback(_future_done_callback(span)) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _handle_error(span, response_error, status_code): | ||||||
|  |     # response_error should be a grpc.Future and so we expect to have cancelled(), | ||||||
|  |     # exception() and traceback() methods if a computation has resulted in an | ||||||
|  |     # exception being raised | ||||||
|  |     if ( | ||||||
|  |         not callable(getattr(response_error, "cancelled", None)) | ||||||
|  |         and not callable(getattr(response_error, "exception", None)) | ||||||
|  |         and not callable(getattr(response_error, "traceback", None)) | ||||||
|  |     ): | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     if response_error.cancelled(): | ||||||
|  |         # handle cancelled futures separately to avoid raising grpc.FutureCancelledError | ||||||
|  |         span.error = 1 | ||||||
|  |         exc_val = to_unicode(response_error.details()) | ||||||
|  |         span.set_tag(errors.ERROR_MSG, exc_val) | ||||||
|  |         span.set_tag(errors.ERROR_TYPE, status_code) | ||||||
|  |         return | ||||||
|  | 
 | ||||||
|  |     exception = response_error.exception() | ||||||
|  |     traceback = response_error.traceback() | ||||||
|  | 
 | ||||||
|  |     if exception is not None and traceback is not None: | ||||||
|  |         span.error = 1 | ||||||
|  |         if isinstance(exception, grpc.RpcError): | ||||||
|  |             # handle internal gRPC exceptions separately to get status code and | ||||||
|  |             # details as tags properly | ||||||
|  |             exc_val = to_unicode(response_error.details()) | ||||||
|  |             span.set_tag(errors.ERROR_MSG, exc_val) | ||||||
|  |             span.set_tag(errors.ERROR_TYPE, status_code) | ||||||
|  |             span.set_tag(errors.ERROR_STACK, traceback) | ||||||
|  |         else: | ||||||
|  |             exc_type = type(exception) | ||||||
|  |             span.set_exc_info(exc_type, exception, traceback) | ||||||
|  |             status_code = to_unicode(response_error.code()) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _WrappedResponseCallFuture(wrapt.ObjectProxy): | ||||||
|  |     def __init__(self, wrapped, span): | ||||||
|  |         super(_WrappedResponseCallFuture, self).__init__(wrapped) | ||||||
|  |         self._span = span | ||||||
|  | 
 | ||||||
|  |     def __iter__(self): | ||||||
|  |         return self | ||||||
|  | 
 | ||||||
|  |     def __next__(self): | ||||||
|  |         try: | ||||||
|  |             return next(self.__wrapped__) | ||||||
|  |         except StopIteration: | ||||||
|  |             # at end of iteration handle response status from wrapped future | ||||||
|  |             _handle_response(self._span, self.__wrapped__) | ||||||
|  |             raise | ||||||
|  |         except grpc.RpcError as rpc_error: | ||||||
|  |             # DEV: grpcio<1.18.0 grpc.RpcError is raised rather than returned as response | ||||||
|  |             # https://github.com/grpc/grpc/commit/8199aff7a66460fbc4e9a82ade2e95ef076fd8f9 | ||||||
|  |             # handle as a response | ||||||
|  |             _handle_response(self._span, rpc_error) | ||||||
|  |             raise | ||||||
|  |         except Exception: | ||||||
|  |             # DEV: added for safety though should not be reached since wrapped response | ||||||
|  |             log.debug("unexpected non-grpc exception raised, closing open span", exc_info=True) | ||||||
|  |             self._span.set_traceback() | ||||||
|  |             self._span.finish() | ||||||
|  |             raise | ||||||
|  | 
 | ||||||
|  |     def next(self): | ||||||
|  |         return self.__next__() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _ClientInterceptor( | ||||||
|  |     grpc.UnaryUnaryClientInterceptor, | ||||||
|  |     grpc.UnaryStreamClientInterceptor, | ||||||
|  |     grpc.StreamUnaryClientInterceptor, | ||||||
|  |     grpc.StreamStreamClientInterceptor, | ||||||
|  | ): | ||||||
|  |     def __init__(self, pin, host, port): | ||||||
|  |         self._pin = pin | ||||||
|  |         self._host = host | ||||||
|  |         self._port = port | ||||||
|  | 
 | ||||||
|  |     def _intercept_client_call(self, method_kind, client_call_details): | ||||||
|  |         tracer = self._pin.tracer | ||||||
|  | 
 | ||||||
|  |         span = tracer.trace( | ||||||
|  |             "grpc", span_type=SpanTypes.GRPC, service=self._pin.service, resource=client_call_details.method, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         # tags for method details | ||||||
|  |         method_path = client_call_details.method | ||||||
|  |         method_package, method_service, method_name = parse_method_path(method_path) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_PATH_KEY, method_path) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_PACKAGE_KEY, method_package) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_SERVICE_KEY, method_service) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_NAME_KEY, method_name) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_KIND_KEY, method_kind) | ||||||
|  |         span.set_tag(constants.GRPC_HOST_KEY, self._host) | ||||||
|  |         span.set_tag(constants.GRPC_PORT_KEY, self._port) | ||||||
|  |         span.set_tag(constants.GRPC_SPAN_KIND_KEY, constants.GRPC_SPAN_KIND_VALUE_CLIENT) | ||||||
|  | 
 | ||||||
|  |         sample_rate = config.grpc.get_analytics_sample_rate() | ||||||
|  |         if sample_rate is not None: | ||||||
|  |             span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) | ||||||
|  | 
 | ||||||
|  |         # inject tags from pin | ||||||
|  |         if self._pin.tags: | ||||||
|  |             span.set_tags(self._pin.tags) | ||||||
|  | 
 | ||||||
|  |         # propagate distributed tracing headers if available | ||||||
|  |         headers = {} | ||||||
|  |         if config.grpc.distributed_tracing_enabled: | ||||||
|  |             propagator = HTTPPropagator() | ||||||
|  |             propagator.inject(span.context, headers) | ||||||
|  | 
 | ||||||
|  |         metadata = [] | ||||||
|  |         if client_call_details.metadata is not None: | ||||||
|  |             metadata = list(client_call_details.metadata) | ||||||
|  |         metadata.extend(headers.items()) | ||||||
|  | 
 | ||||||
|  |         client_call_details = _ClientCallDetails( | ||||||
|  |             client_call_details.method, client_call_details.timeout, metadata, client_call_details.credentials, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         return span, client_call_details | ||||||
|  | 
 | ||||||
|  |     def intercept_unary_unary(self, continuation, client_call_details, request): | ||||||
|  |         span, client_call_details = self._intercept_client_call(constants.GRPC_METHOD_KIND_UNARY, client_call_details,) | ||||||
|  |         try: | ||||||
|  |             response = continuation(client_call_details, request) | ||||||
|  |             _handle_response(span, response) | ||||||
|  |         except grpc.RpcError as rpc_error: | ||||||
|  |             # DEV: grpcio<1.18.0 grpc.RpcError is raised rather than returned as response | ||||||
|  |             # https://github.com/grpc/grpc/commit/8199aff7a66460fbc4e9a82ade2e95ef076fd8f9 | ||||||
|  |             # handle as a response | ||||||
|  |             _handle_response(span, rpc_error) | ||||||
|  |             raise | ||||||
|  | 
 | ||||||
|  |         return response | ||||||
|  | 
 | ||||||
|  |     def intercept_unary_stream(self, continuation, client_call_details, request): | ||||||
|  |         span, client_call_details = self._intercept_client_call( | ||||||
|  |             constants.GRPC_METHOD_KIND_SERVER_STREAMING, client_call_details, | ||||||
|  |         ) | ||||||
|  |         response_iterator = continuation(client_call_details, request) | ||||||
|  |         response_iterator = _WrappedResponseCallFuture(response_iterator, span) | ||||||
|  |         return response_iterator | ||||||
|  | 
 | ||||||
|  |     def intercept_stream_unary(self, continuation, client_call_details, request_iterator): | ||||||
|  |         span, client_call_details = self._intercept_client_call( | ||||||
|  |             constants.GRPC_METHOD_KIND_CLIENT_STREAMING, client_call_details, | ||||||
|  |         ) | ||||||
|  |         try: | ||||||
|  |             response = continuation(client_call_details, request_iterator) | ||||||
|  |             _handle_response(span, response) | ||||||
|  |         except grpc.RpcError as rpc_error: | ||||||
|  |             # DEV: grpcio<1.18.0 grpc.RpcError is raised rather than returned as response | ||||||
|  |             # https://github.com/grpc/grpc/commit/8199aff7a66460fbc4e9a82ade2e95ef076fd8f9 | ||||||
|  |             # handle as a response | ||||||
|  |             _handle_response(span, rpc_error) | ||||||
|  |             raise | ||||||
|  | 
 | ||||||
|  |         return response | ||||||
|  | 
 | ||||||
|  |     def intercept_stream_stream(self, continuation, client_call_details, request_iterator): | ||||||
|  |         span, client_call_details = self._intercept_client_call( | ||||||
|  |             constants.GRPC_METHOD_KIND_BIDI_STREAMING, client_call_details, | ||||||
|  |         ) | ||||||
|  |         response_iterator = continuation(client_call_details, request_iterator) | ||||||
|  |         response_iterator = _WrappedResponseCallFuture(response_iterator, span) | ||||||
|  |         return response_iterator | ||||||
|  | @ -0,0 +1,24 @@ | ||||||
|  | import grpc | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | GRPC_PIN_MODULE_SERVER = grpc.Server | ||||||
|  | GRPC_PIN_MODULE_CLIENT = grpc.Channel | ||||||
|  | GRPC_METHOD_PATH_KEY = 'grpc.method.path' | ||||||
|  | GRPC_METHOD_PACKAGE_KEY = 'grpc.method.package' | ||||||
|  | GRPC_METHOD_SERVICE_KEY = 'grpc.method.service' | ||||||
|  | GRPC_METHOD_NAME_KEY = 'grpc.method.name' | ||||||
|  | GRPC_METHOD_KIND_KEY = 'grpc.method.kind' | ||||||
|  | GRPC_STATUS_CODE_KEY = 'grpc.status.code' | ||||||
|  | GRPC_REQUEST_METADATA_PREFIX_KEY = 'grpc.request.metadata.' | ||||||
|  | GRPC_RESPONSE_METADATA_PREFIX_KEY = 'grpc.response.metadata.' | ||||||
|  | GRPC_HOST_KEY = 'grpc.host' | ||||||
|  | GRPC_PORT_KEY = 'grpc.port' | ||||||
|  | GRPC_SPAN_KIND_KEY = 'span.kind' | ||||||
|  | GRPC_SPAN_KIND_VALUE_CLIENT = 'client' | ||||||
|  | GRPC_SPAN_KIND_VALUE_SERVER = 'server' | ||||||
|  | GRPC_METHOD_KIND_UNARY = 'unary' | ||||||
|  | GRPC_METHOD_KIND_CLIENT_STREAMING = 'client_streaming' | ||||||
|  | GRPC_METHOD_KIND_SERVER_STREAMING = 'server_streaming' | ||||||
|  | GRPC_METHOD_KIND_BIDI_STREAMING = 'bidi_streaming' | ||||||
|  | GRPC_SERVICE_SERVER = 'grpc-server' | ||||||
|  | GRPC_SERVICE_CLIENT = 'grpc-client' | ||||||
|  | @ -0,0 +1,126 @@ | ||||||
|  | import grpc | ||||||
|  | import os | ||||||
|  | 
 | ||||||
|  | from ddtrace.vendor.wrapt import wrap_function_wrapper as _w | ||||||
|  | from ddtrace import config, Pin | ||||||
|  | 
 | ||||||
|  | from ...utils.wrappers import unwrap as _u | ||||||
|  | 
 | ||||||
|  | from . import constants | ||||||
|  | from .client_interceptor import create_client_interceptor, intercept_channel | ||||||
|  | from .server_interceptor import create_server_interceptor | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | config._add('grpc_server', dict( | ||||||
|  |     service_name=os.environ.get('DATADOG_SERVICE_NAME', constants.GRPC_SERVICE_SERVER), | ||||||
|  |     distributed_tracing_enabled=True, | ||||||
|  | )) | ||||||
|  | 
 | ||||||
|  | # TODO[tbutt]: keeping name for client config unchanged to maintain backwards | ||||||
|  | # compatibility but should change in future | ||||||
|  | config._add('grpc', dict( | ||||||
|  |     service_name='{}-{}'.format( | ||||||
|  |         os.environ.get('DATADOG_SERVICE_NAME'), constants.GRPC_SERVICE_CLIENT | ||||||
|  |     ) if os.environ.get('DATADOG_SERVICE_NAME') else constants.GRPC_SERVICE_CLIENT, | ||||||
|  |     distributed_tracing_enabled=True, | ||||||
|  | )) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def patch(): | ||||||
|  |     _patch_client() | ||||||
|  |     _patch_server() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def unpatch(): | ||||||
|  |     _unpatch_client() | ||||||
|  |     _unpatch_server() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _patch_client(): | ||||||
|  |     if getattr(constants.GRPC_PIN_MODULE_CLIENT, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(constants.GRPC_PIN_MODULE_CLIENT, '__datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     Pin(service=config.grpc.service_name).onto(constants.GRPC_PIN_MODULE_CLIENT) | ||||||
|  | 
 | ||||||
|  |     _w('grpc', 'insecure_channel', _client_channel_interceptor) | ||||||
|  |     _w('grpc', 'secure_channel', _client_channel_interceptor) | ||||||
|  |     _w('grpc', 'intercept_channel', intercept_channel) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _unpatch_client(): | ||||||
|  |     if not getattr(constants.GRPC_PIN_MODULE_CLIENT, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(constants.GRPC_PIN_MODULE_CLIENT, '__datadog_patch', False) | ||||||
|  | 
 | ||||||
|  |     pin = Pin.get_from(constants.GRPC_PIN_MODULE_CLIENT) | ||||||
|  |     if pin: | ||||||
|  |         pin.remove_from(constants.GRPC_PIN_MODULE_CLIENT) | ||||||
|  | 
 | ||||||
|  |     _u(grpc, 'secure_channel') | ||||||
|  |     _u(grpc, 'insecure_channel') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _patch_server(): | ||||||
|  |     if getattr(constants.GRPC_PIN_MODULE_SERVER, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(constants.GRPC_PIN_MODULE_SERVER, '__datadog_patch', True) | ||||||
|  | 
 | ||||||
|  |     Pin(service=config.grpc_server.service_name).onto(constants.GRPC_PIN_MODULE_SERVER) | ||||||
|  | 
 | ||||||
|  |     _w('grpc', 'server', _server_constructor_interceptor) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _unpatch_server(): | ||||||
|  |     if not getattr(constants.GRPC_PIN_MODULE_SERVER, '__datadog_patch', False): | ||||||
|  |         return | ||||||
|  |     setattr(constants.GRPC_PIN_MODULE_SERVER, '__datadog_patch', False) | ||||||
|  | 
 | ||||||
|  |     pin = Pin.get_from(constants.GRPC_PIN_MODULE_SERVER) | ||||||
|  |     if pin: | ||||||
|  |         pin.remove_from(constants.GRPC_PIN_MODULE_SERVER) | ||||||
|  | 
 | ||||||
|  |     _u(grpc, 'server') | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _client_channel_interceptor(wrapped, instance, args, kwargs): | ||||||
|  |     channel = wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     pin = Pin.get_from(constants.GRPC_PIN_MODULE_CLIENT) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return channel | ||||||
|  | 
 | ||||||
|  |     (host, port) = _parse_target_from_arguments(args, kwargs) | ||||||
|  | 
 | ||||||
|  |     interceptor_function = create_client_interceptor(pin, host, port) | ||||||
|  |     return grpc.intercept_channel(channel, interceptor_function) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _server_constructor_interceptor(wrapped, instance, args, kwargs): | ||||||
|  |     # DEV: we clone the pin on the grpc module and configure it for the server | ||||||
|  |     # interceptor | ||||||
|  | 
 | ||||||
|  |     pin = Pin.get_from(constants.GRPC_PIN_MODULE_SERVER) | ||||||
|  |     if not pin or not pin.enabled(): | ||||||
|  |         return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |     interceptor = create_server_interceptor(pin) | ||||||
|  | 
 | ||||||
|  |     # DEV: Inject our tracing interceptor first in the list of interceptors | ||||||
|  |     if 'interceptors' in kwargs: | ||||||
|  |         kwargs['interceptors'] = (interceptor,) + tuple(kwargs['interceptors']) | ||||||
|  |     else: | ||||||
|  |         kwargs['interceptors'] = (interceptor,) | ||||||
|  | 
 | ||||||
|  |     return wrapped(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _parse_target_from_arguments(args, kwargs): | ||||||
|  |     if 'target' in kwargs: | ||||||
|  |         target = kwargs['target'] | ||||||
|  |     else: | ||||||
|  |         target = args[0] | ||||||
|  | 
 | ||||||
|  |     split = target.rsplit(':', 2) | ||||||
|  | 
 | ||||||
|  |     return (split[0], split[1] if len(split) > 1 else None) | ||||||
|  | @ -0,0 +1,146 @@ | ||||||
|  | import grpc | ||||||
|  | from ddtrace.vendor import wrapt | ||||||
|  | 
 | ||||||
|  | from ddtrace import config | ||||||
|  | from ddtrace.ext import errors | ||||||
|  | from ddtrace.compat import to_unicode | ||||||
|  | 
 | ||||||
|  | from ...constants import ANALYTICS_SAMPLE_RATE_KEY | ||||||
|  | from ...ext import SpanTypes | ||||||
|  | from ...propagation.http import HTTPPropagator | ||||||
|  | from . import constants | ||||||
|  | from .utils import parse_method_path | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def create_server_interceptor(pin): | ||||||
|  |     def interceptor_function(continuation, handler_call_details): | ||||||
|  |         if not pin.enabled: | ||||||
|  |             return continuation(handler_call_details) | ||||||
|  | 
 | ||||||
|  |         rpc_method_handler = continuation(handler_call_details) | ||||||
|  |         return _TracedRpcMethodHandler(pin, handler_call_details, rpc_method_handler) | ||||||
|  | 
 | ||||||
|  |     return _ServerInterceptor(interceptor_function) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _handle_server_exception(server_context, span): | ||||||
|  |     if server_context is not None and \ | ||||||
|  |        hasattr(server_context, '_state') and \ | ||||||
|  |        server_context._state is not None: | ||||||
|  |         code = to_unicode(server_context._state.code) | ||||||
|  |         details = to_unicode(server_context._state.details) | ||||||
|  |         span.error = 1 | ||||||
|  |         span.set_tag(errors.ERROR_MSG, details) | ||||||
|  |         span.set_tag(errors.ERROR_TYPE, code) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def _wrap_response_iterator(response_iterator, server_context, span): | ||||||
|  |     try: | ||||||
|  |         for response in response_iterator: | ||||||
|  |             yield response | ||||||
|  |     except Exception: | ||||||
|  |         span.set_traceback() | ||||||
|  |         _handle_server_exception(server_context, span) | ||||||
|  |         raise | ||||||
|  |     finally: | ||||||
|  |         span.finish() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _TracedRpcMethodHandler(wrapt.ObjectProxy): | ||||||
|  |     def __init__(self, pin, handler_call_details, wrapped): | ||||||
|  |         super(_TracedRpcMethodHandler, self).__init__(wrapped) | ||||||
|  |         self._pin = pin | ||||||
|  |         self._handler_call_details = handler_call_details | ||||||
|  | 
 | ||||||
|  |     def _fn(self, method_kind, behavior, args, kwargs): | ||||||
|  |         if config.grpc_server.distributed_tracing_enabled: | ||||||
|  |             headers = dict(self._handler_call_details.invocation_metadata) | ||||||
|  |             propagator = HTTPPropagator() | ||||||
|  |             context = propagator.extract(headers) | ||||||
|  | 
 | ||||||
|  |             if context.trace_id: | ||||||
|  |                 self._pin.tracer.context_provider.activate(context) | ||||||
|  | 
 | ||||||
|  |         tracer = self._pin.tracer | ||||||
|  | 
 | ||||||
|  |         span = tracer.trace( | ||||||
|  |             'grpc', | ||||||
|  |             span_type=SpanTypes.GRPC, | ||||||
|  |             service=self._pin.service, | ||||||
|  |             resource=self._handler_call_details.method, | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |         method_path = self._handler_call_details.method | ||||||
|  |         method_package, method_service, method_name = parse_method_path(method_path) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_PATH_KEY, method_path) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_PACKAGE_KEY, method_package) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_SERVICE_KEY, method_service) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_NAME_KEY, method_name) | ||||||
|  |         span.set_tag(constants.GRPC_METHOD_KIND_KEY, method_kind) | ||||||
|  |         span.set_tag(constants.GRPC_SPAN_KIND_KEY, constants.GRPC_SPAN_KIND_VALUE_SERVER) | ||||||
|  | 
 | ||||||
|  |         sample_rate = config.grpc_server.get_analytics_sample_rate() | ||||||
|  |         if sample_rate is not None: | ||||||
|  |             span.set_tag(ANALYTICS_SAMPLE_RATE_KEY, sample_rate) | ||||||
|  | 
 | ||||||
|  |         # access server context by taking second argument as server context | ||||||
|  |         # if not found, skip using context to tag span with server state information | ||||||
|  |         server_context = args[1] if isinstance(args[1], grpc.ServicerContext) else None | ||||||
|  | 
 | ||||||
|  |         if self._pin.tags: | ||||||
|  |             span.set_tags(self._pin.tags) | ||||||
|  | 
 | ||||||
|  |         try: | ||||||
|  |             response_or_iterator = behavior(*args, **kwargs) | ||||||
|  | 
 | ||||||
|  |             if self.__wrapped__.response_streaming: | ||||||
|  |                 response_or_iterator = _wrap_response_iterator(response_or_iterator, server_context, span) | ||||||
|  |         except Exception: | ||||||
|  |             span.set_traceback() | ||||||
|  |             _handle_server_exception(server_context, span) | ||||||
|  |             raise | ||||||
|  |         finally: | ||||||
|  |             if not self.__wrapped__.response_streaming: | ||||||
|  |                 span.finish() | ||||||
|  | 
 | ||||||
|  |         return response_or_iterator | ||||||
|  | 
 | ||||||
|  |     def unary_unary(self, *args, **kwargs): | ||||||
|  |         return self._fn( | ||||||
|  |             constants.GRPC_METHOD_KIND_UNARY, | ||||||
|  |             self.__wrapped__.unary_unary, | ||||||
|  |             args, | ||||||
|  |             kwargs | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def unary_stream(self, *args, **kwargs): | ||||||
|  |         return self._fn( | ||||||
|  |             constants.GRPC_METHOD_KIND_SERVER_STREAMING, | ||||||
|  |             self.__wrapped__.unary_stream, | ||||||
|  |             args, | ||||||
|  |             kwargs | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def stream_unary(self, *args, **kwargs): | ||||||
|  |         return self._fn( | ||||||
|  |             constants.GRPC_METHOD_KIND_CLIENT_STREAMING, | ||||||
|  |             self.__wrapped__.stream_unary, | ||||||
|  |             args, | ||||||
|  |             kwargs | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  |     def stream_stream(self, *args, **kwargs): | ||||||
|  |         return self._fn( | ||||||
|  |             constants.GRPC_METHOD_KIND_BIDI_STREAMING, | ||||||
|  |             self.__wrapped__.stream_stream, | ||||||
|  |             args, | ||||||
|  |             kwargs | ||||||
|  |         ) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | class _ServerInterceptor(grpc.ServerInterceptor): | ||||||
|  |     def __init__(self, interceptor_function): | ||||||
|  |         self._fn = interceptor_function | ||||||
|  | 
 | ||||||
|  |     def intercept_service(self, continuation, handler_call_details): | ||||||
|  |         return self._fn(continuation, handler_call_details) | ||||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue